Initial commit (secrets removed)

This commit is contained in:
Ali Arghyani
2025-11-04 09:09:29 +03:30
commit e1bc3d8adf
182 changed files with 17578 additions and 0 deletions

1
.env.example Normal file
View File

@@ -0,0 +1 @@
NUXT_PUBLIC_LOAD_PLAUSIBLE="no"

26
.gitignore vendored Normal file
View File

@@ -0,0 +1,26 @@
# Nuxt dev/build outputs
.output
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example
# MCP configuration files (may contain secrets)
.roo

7
.prettierignore Normal file
View File

@@ -0,0 +1,7 @@
.nuxt
.output
.vercel
node_modules
coverage
dist
*.log

298
.roomodes Normal file
View File

@@ -0,0 +1,298 @@
customModes:
- slug: bmad-ux-expert
name: 🎨 UX Expert
description: Design-related files
roleDefinition: You are a UX Expert specializing in ux expert tasks and responsibilities.
whenToUse: Use for UX Expert tasks
customInstructions: CRITICAL Read the full YAML from .bmad-core/agents/ux-expert.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- - edit
- fileRegex: \.(md|css|scss|html|jsx|tsx)$
description: Design-related files
source: project
- slug: bmad-sm
name: 🏃 Scrum Master
description: Process and planning docs
roleDefinition: You are a Scrum Master specializing in scrum master tasks and responsibilities.
whenToUse: Use for Scrum Master tasks
customInstructions: CRITICAL Read the full YAML from .bmad-core/agents/sm.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- - edit
- fileRegex: \.(md|txt)$
description: Process and planning docs
source: project
- slug: bmad-qa
name: 🧪 Test Architect & Quality Advisor
description: Test files and documentation
roleDefinition: You are a Test Architect & Quality Advisor specializing in test architect & quality advisor tasks and responsibilities.
whenToUse: Use for Test Architect & Quality Advisor tasks
customInstructions: CRITICAL Read the full YAML from .bmad-core/agents/qa.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- - edit
- fileRegex: \.(test|spec)\.(js|ts|jsx|tsx)$|\.md$
description: Test files and documentation
source: project
- slug: bmad-po
name: 📝 Product Owner
description: Story and requirement docs
roleDefinition: You are a Product Owner specializing in product owner tasks and responsibilities.
whenToUse: Use for Product Owner tasks
customInstructions: CRITICAL Read the full YAML from .bmad-core/agents/po.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- - edit
- fileRegex: \.(md|txt)$
description: Story and requirement docs
source: project
- slug: bmad-pm
name: 📋 Product Manager
description: Product documentation
roleDefinition: You are a Product Manager specializing in product manager tasks and responsibilities.
whenToUse: Use for Product Manager tasks
customInstructions: CRITICAL Read the full YAML from .bmad-core/agents/pm.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- - edit
- fileRegex: \.(md|txt)$
description: Product documentation
source: project
- slug: bmad-dev
name: 💻 Full Stack Developer
roleDefinition: You are a Full Stack Developer specializing in full stack developer tasks and responsibilities.
whenToUse: Use for Full Stack Developer tasks
customInstructions: CRITICAL Read the full YAML from .bmad-core/agents/dev.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project
- slug: bmad-orchestrator
name: 🎭 BMad Master Orchestrator
roleDefinition: You are a BMad Master Orchestrator specializing in bmad master orchestrator tasks and responsibilities.
whenToUse: Use for BMad Master Orchestrator tasks
customInstructions: CRITICAL Read the full YAML from .bmad-core/agents/bmad-orchestrator.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project
- slug: bmad-master
name: 🧙 BMad Master Task Executor
roleDefinition: You are a BMad Master Task Executor specializing in bmad master task executor tasks and responsibilities.
whenToUse: Use for BMad Master Task Executor tasks
customInstructions: CRITICAL Read the full YAML from .bmad-core/agents/bmad-master.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project
- slug: bmad-architect
name: 🏗️ Architect
description: Architecture docs and configs
roleDefinition: You are a Architect specializing in architect tasks and responsibilities.
whenToUse: Use for Architect tasks
customInstructions: CRITICAL Read the full YAML from .bmad-core/agents/architect.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- - edit
- fileRegex: \.(md|txt|yml|yaml|json)$
description: Architecture docs and configs
source: project
- slug: bmad-analyst
name: 📊 Business Analyst
description: Documentation and text files
roleDefinition: You are a Business Analyst specializing in business analyst tasks and responsibilities.
whenToUse: Use for Business Analyst tasks
customInstructions: CRITICAL Read the full YAML from .bmad-core/agents/analyst.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- - edit
- fileRegex: \.(md|txt)$
description: Documentation and text files
source: project
- slug: bmad-infra-devops-platform
name: 🤖 DevOps Infrastructure Specialist Platform Engineer
roleDefinition: You are a DevOps Infrastructure Specialist Platform Engineer specializing in devops infrastructure specialist platform engineer tasks and responsibilities.
whenToUse: Use for DevOps Infrastructure Specialist Platform Engineer tasks
customInstructions: CRITICAL Read the full YAML from .bmad-infrastructure-devops/agents/infra-devops-platform.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project
- slug: bmad-game-ux-expert
name: 🎮 Godot Game UX Expert
roleDefinition: You are a Godot Game UX Expert specializing in godot game ux expert tasks and responsibilities.
whenToUse: Use for Godot Game UX Expert tasks
customInstructions: CRITICAL Read the full YAML from .bmad-godot-game-dev/agents/game-ux-expert.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project
- slug: bmad-game-sm
name: 🏃♂️ Game Scrum Master/Producer
description: Game project management docs
roleDefinition: You are a Game Scrum Master/Producer specializing in game scrum master/producer tasks and responsibilities.
whenToUse: Use for Game Scrum Master/Producer tasks
customInstructions: CRITICAL Read the full YAML from .bmad-godot-game-dev/agents/game-sm.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- - edit
- fileRegex: \.(md|txt)$
description: Game project management docs
source: project
- slug: bmad-game-qa
name: 🎮🧪 Game Test Architect & TDD Enforcer (Godot)
roleDefinition: You are a Game Test Architect & TDD Enforcer (Godot) specializing in game test architect & tdd enforcer (godot) tasks and responsibilities.
whenToUse: Use for Game Test Architect & TDD Enforcer (Godot) tasks
customInstructions: CRITICAL Read the full YAML from .bmad-godot-game-dev/agents/game-qa.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project
- slug: bmad-game-po
name: 🎮 Game Product Owner
roleDefinition: You are a Game Product Owner specializing in game product owner tasks and responsibilities.
whenToUse: Use for Game Product Owner tasks
customInstructions: CRITICAL Read the full YAML from .bmad-godot-game-dev/agents/game-po.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project
- slug: bmad-game-pm
name: 📋 Godot Game Product Manager
roleDefinition: You are a Godot Game Product Manager specializing in godot game product manager tasks and responsibilities.
whenToUse: Use for Godot Game Product Manager tasks
customInstructions: CRITICAL Read the full YAML from .bmad-godot-game-dev/agents/game-pm.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project
- slug: bmad-game-developer
name: 👾 Game Developer (Godot)
roleDefinition: You are a Game Developer (Godot) specializing in game developer (godot) tasks and responsibilities.
whenToUse: Use for Game Developer (Godot) tasks
customInstructions: CRITICAL Read the full YAML from .bmad-godot-game-dev/agents/game-developer.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project
- slug: bmad-game-designer
name: 🎮 Game Design Specialist
description: Game design documents and configs
roleDefinition: You are a Game Design Specialist specializing in game design specialist tasks and responsibilities.
whenToUse: Use for Game Design Specialist tasks
customInstructions: CRITICAL Read the full YAML from .bmad-godot-game-dev/agents/game-designer.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- - edit
- fileRegex: \.(md|txt|json|yaml|yml)$
description: Game design documents and configs
source: project
- slug: bmad-game-architect
name: 🎮 Game Architect (Godot Focus)
roleDefinition: You are a Game Architect (Godot Focus) specializing in game architect (godot focus) tasks and responsibilities.
whenToUse: Use for Game Architect (Godot Focus) tasks
customInstructions: CRITICAL Read the full YAML from .bmad-godot-game-dev/agents/game-architect.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project
- slug: bmad-game-analyst
name: 📊 Game Development Analyst
roleDefinition: You are a Game Development Analyst specializing in game development analyst tasks and responsibilities.
whenToUse: Use for Game Development Analyst tasks
customInstructions: CRITICAL Read the full YAML from .bmad-godot-game-dev/agents/game-analyst.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project
- slug: bmad-world-builder
name: 🌍 Setting & Universe Designer
roleDefinition: You are a Setting & Universe Designer specializing in setting & universe designer tasks and responsibilities.
whenToUse: Use for Setting & Universe Designer tasks
customInstructions: CRITICAL Read the full YAML from .bmad-creative-writing/agents/world-builder.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project
- slug: bmad-plot-architect
name: 🏗️ Story Structure Specialist
roleDefinition: You are a Story Structure Specialist specializing in story structure specialist tasks and responsibilities.
whenToUse: Use for Story Structure Specialist tasks
customInstructions: CRITICAL Read the full YAML from .bmad-creative-writing/agents/plot-architect.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project
- slug: bmad-narrative-designer
name: 🎭 Interactive Narrative Architect
roleDefinition: You are a Interactive Narrative Architect specializing in interactive narrative architect tasks and responsibilities.
whenToUse: Use for Interactive Narrative Architect tasks
customInstructions: CRITICAL Read the full YAML from .bmad-creative-writing/agents/narrative-designer.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project
- slug: bmad-genre-specialist
name: 📚 Genre Convention Expert
roleDefinition: You are a Genre Convention Expert specializing in genre convention expert tasks and responsibilities.
whenToUse: Use for Genre Convention Expert tasks
customInstructions: CRITICAL Read the full YAML from .bmad-creative-writing/agents/genre-specialist.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project
- slug: bmad-editor
name: ✏️ Style & Structure Editor
roleDefinition: You are a Style & Structure Editor specializing in style & structure editor tasks and responsibilities.
whenToUse: Use for Style & Structure Editor tasks
customInstructions: CRITICAL Read the full YAML from .bmad-creative-writing/agents/editor.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project
- slug: bmad-dialog-specialist
name: 💬 Conversation & Voice Expert
roleDefinition: You are a Conversation & Voice Expert specializing in conversation & voice expert tasks and responsibilities.
whenToUse: Use for Conversation & Voice Expert tasks
customInstructions: CRITICAL Read the full YAML from .bmad-creative-writing/agents/dialog-specialist.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project
- slug: bmad-cover-designer
name: 🎨 Book Cover Designer & KDP Specialist
roleDefinition: You are a Book Cover Designer & KDP Specialist specializing in book cover designer & kdp specialist tasks and responsibilities.
whenToUse: Use for Book Cover Designer & KDP Specialist tasks
customInstructions: CRITICAL Read the full YAML from .bmad-creative-writing/agents/cover-designer.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project
- slug: bmad-character-psychologist
name: 🧠 Character Development Expert
roleDefinition: You are a Character Development Expert specializing in character development expert tasks and responsibilities.
whenToUse: Use for Character Development Expert tasks
customInstructions: CRITICAL Read the full YAML from .bmad-creative-writing/agents/character-psychologist.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project
- slug: bmad-book-critic
name: 📚 Renowned Literary Critic
roleDefinition: You are a Renowned Literary Critic specializing in renowned literary critic tasks and responsibilities.
whenToUse: Use for Renowned Literary Critic tasks
customInstructions: CRITICAL Read the full YAML from .bmad-creative-writing/agents/book-critic.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project
- slug: bmad-beta-reader
name: 👓 Reader Experience Simulator
roleDefinition: You are a Reader Experience Simulator specializing in reader experience simulator tasks and responsibilities.
whenToUse: Use for Reader Experience Simulator tasks
customInstructions: CRITICAL Read the full YAML from .bmad-creative-writing/agents/beta-reader.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
groups:
- read
- edit
source: project

0
.vscode/mcp.json vendored Normal file
View File

65
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,65 @@
{
//*********************************************************************
// general
//*********************************************************************
"files.insertFinalNewline": true,
"files.exclude": {
"**/node_modules": true
},
//*********************************************************************
// editor
//*********************************************************************
"editor.rulers": [
90
],
"editor.wordWrap": "wordWrapColumn",
"editor.wordWrapColumn": 90,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.detectIndentation": false,
"editor.tabSize": 2,
"editor.indentSize": "tabSize",
"editor.formatOnSave": true,
"editor.quickSuggestions": {
"strings": true
},
//*********************************************************************
// tailwind
//*********************************************************************
"css.validate": false,
"tailwindCSS.validate": true,
"tailwindCSS.emmetCompletions": true,
"tailwindCSS.classAttributes": [
"class",
"className",
"ngClass",
"ui"
],
// https://ui.nuxtlabs.com/getting-started/installation#intellisense
"tailwindCSS.experimental.classRegex": [
[
"ui:\\s*{([^)]*)\\s*}",
"[\"'`]([^\"'`]*).*?[\"'`]"
],
[
"/\\*ui\\*/\\s*{([^;]*)}",
":\\s*[\"'`]([^\"'`]*).*?[\"'`]"
]
],
"files.associations": {
"*.css": "css"
},
//*********************************************************************
// typescript
//*********************************************************************
"typescript.preferences.importModuleSpecifier": "non-relative",
"[vue]": {
"editor.defaultFormatter": "Vue.volar"
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"i18n-ally.localesPaths": [
"i18n",
"i18n/locales"
]
}

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 David Dahan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

97
README.md Normal file
View File

@@ -0,0 +1,97 @@
[![Website Preview](/public/img/page-preview.JPG)](https://aliarghyani.vercel.app)
# 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.
## 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
## 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
## Quick Start
```bash
pnpm install
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.
## 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
public/
fonts/ # Roobert font files served directly
img/ # Static imagery consumed by Nuxt Image
i18n/
locales/ # en.json and fa.json dictionaries
```
## 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).
## 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`.
## 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.
## 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`.
### Static Hosting
- Run `pnpm generate`.
- Serve the contents of `.output/public`.
## Contributing
Issues and pull requests are welcome. Please run `pnpm typecheck` and `pnpm format` before submitting changes to keep the project consistent.
## License
This project is open source under the MIT License. See [`LICENSE`](./LICENSE) for details.

33
app/app.config.ts Normal file
View File

@@ -0,0 +1,33 @@
export default defineAppConfig({
ui: {
strategy: "merge",
primary: "indigo",
gray: "slate",
colors: {
primary: 'indigo',
},
icons: {
dynamic: true,
},
tooltip: {
// removes fixed height and truncate
base: "h-auto overflow-visible text-overflow-clip whitespace-normal",
popper: { placement: "top" },
},
timeline: {
slots: {
separator: 'flex-1 rounded-full bg-gray-200 dark:bg-gray-700',
},
variants: {
color: {
primary: {
separator: 'group-data-[state=completed]:bg-primary-500 dark:group-data-[state=completed]:bg-primary-400'
}
}
}
}
} as any,
repoUrl: "https://github.com/aliarghyani/vue-cursor-rules",
myWebsiteUrl: "https://www.linkedin.com/in/aliarghyani/",
myContactUrl: "https://www.linkedin.com/in/aliarghyani/",
})

81
app/app.vue Normal file
View File

@@ -0,0 +1,81 @@
<template>
<UApp :toaster="{ expand: false }">
<NuxtLoadingIndicator color="#6366F1" :height="3" :throttle="100" />
<TopNav />
<NuxtPage />
<FooterCopyright />
<!-- <FloatingActions /> -->
</UApp>
</template>
<script setup lang="ts">
import FloatingActions from '@/components/portfolio/FloatingActions.vue'
import TopNav from '@/components/common/TopNav.vue'
import FooterCopyright from '@/components/common/FooterCopyright.vue'
import { usePortfolio } from '@/composables/usePortfolio'
import { useLocaleSwitching } from '@/composables/useLocaleSwitching'
const { locale, locales } = useI18n()
const portfolio = usePortfolio()
const { isLocaleSwitching } = useLocaleSwitching()
const activeLocale = computed(() => locales.value.find((item) => item.code === locale.value) ?? locales.value[0])
const langAttr = computed(() => (activeLocale.value as any)?.language ?? locale.value)
const dirAttr = computed(() => activeLocale.value?.dir ?? 'ltr')
const heroAvatar = computed(() => portfolio.value.profile.avatar ?? null)
const heroAssetType = computed(() => {
if (!heroAvatar.value) return null
if (heroAvatar.value.endsWith('.webp')) return 'image/webp'
if (heroAvatar.value.endsWith('.png')) return 'image/png'
if (heroAvatar.value.endsWith('.jpg') || heroAvatar.value.endsWith('.jpeg')) return 'image/jpeg'
return 'image/*'
})
const roobertRegular = '/fonts/Roobert-Regular.woff2'
const roobertMedium = '/fonts/Roobert-Medium.woff2'
const roobertSemiBold = '/fonts/Roobert-SemiBold.woff2'
const vazirVar = '/fonts/vazirmatn/webfonts/Vazirmatn[wght].woff2'
const fontPreloads = computed(() => {
const links = [
{ rel: 'preload', as: 'font', type: 'font/woff2', href: roobertRegular, crossorigin: 'anonymous' },
{ rel: 'preload', as: 'font', type: 'font/woff2', href: roobertMedium, crossorigin: 'anonymous' },
{ rel: 'preload', as: 'font', type: 'font/woff2', href: roobertSemiBold, crossorigin: 'anonymous' }
]
if (locale.value === 'fa') {
links.push({ rel: 'preload', as: 'font', type: 'font/woff2', href: vazirVar, crossorigin: 'anonymous' })
}
return links
})
const headLinks = computed(() => {
const links = [...fontPreloads.value]
if (heroAvatar.value) {
links.push({
rel: 'preload',
as: 'image',
href: heroAvatar.value,
type: heroAssetType.value ?? 'image/*',
crossorigin: 'anonymous'
})
}
return links
})
useHead(() => ({
htmlAttrs: {
lang: langAttr.value,
dir: dirAttr.value
},
link: headLinks.value as any
}))
if (import.meta.client) {
watchEffect(() => {
document.documentElement.lang = langAttr.value
document.documentElement.dir = dirAttr.value
})
}
</script>

319
app/assets/css/main.css Normal file
View File

@@ -0,0 +1,319 @@
@custom-variant dark (&:where(.dark &, :root.dark &, [data-theme="dark"] &));
@import "tailwindcss";
@import "@nuxt/ui";
@source "../../components/**/*.{vue,js,ts}";
@source "../../layouts/**/*.vue";
@source "../../pages/**/*.vue";
@source "../../app.vue";
@source "../../error.vue";
@source "../../composables/**/*.{js,ts}";
@source "../../plugins/**/*.{js,ts}";
@source "../../utils/**/*.{js,ts}";
@source "../../data/**/*.{js,ts}";
@source "../../../content/**/*.{md,json,yaml,yml}";
@theme {
--font-sans: 'Outfit', 'ui-sans-serif', 'system-ui', 'sans-serif';
--font-display: 'Fraunces', 'Roobert', 'Inter', 'serif';
}
@layer base {
@font-face {
font-family: "Roobert";
font-style: normal;
font-weight: 400;
src:
local(""),
url("/fonts/Roobert-Regular.woff2") format("woff2");
}
@font-face {
font-family: "Roobert";
font-style: normal;
font-weight: 500;
src:
local(""),
url("/fonts/Roobert-Medium.woff2") format("woff2");
}
@font-face {
font-family: "Roobert";
font-style: normal;
font-weight: 600;
src:
local(""),
url("/fonts/Roobert-SemiBold.woff2") format("woff2");
}
/* Persian variable font (Vazirmatn) — variable woff2 for best performance */
@font-face {
font-family: "Vazirmatn";
font-style: normal;
font-weight: 100 900;
font-display: swap;
src:
local(""),
url("/fonts/vazirmatn/webfonts/Vazirmatn[wght].woff2") format("woff2");
}
html {
overflow-y: scroll;
/* Avoid width variation */
}
html,
body,
#__nuxt,
#__layout {
@apply min-h-screen w-full;
background-color: #f2f5f9;
color-scheme: light;
}
.dark html,
.dark body,
.dark #__nuxt,
.dark #__layout {
/* fallback for SSR edge cases */
}
:root.dark #__nuxt,
:root.dark #__layout,
html.dark,
html.dark body,
html.dark #__nuxt,
html.dark #__layout {
background-color: #0b1220;
color-scheme: dark;
}
div,
span,
input,
textarea,
button,
select,
a {
@apply focus:outline-hidden;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/* Use the sans variable globally; switches automatically when we override variables */
html,
body {
font-family: var(--font-sans);
}
/* In Persian/RTL, prefer Vazirmatn for both body and headings via variables */
html[dir="rtl"],
html[lang^="fa"] {
--font-sans: 'Vazirmatn', 'Outfit', 'ui-sans-serif', 'system-ui', 'sans-serif';
--font-display: 'Vazirmatn', 'Roobert', 'Inter', 'serif';
}
}
@layer components {
h1,
h2,
h3 {
@apply font-display tracking-tight;
}
h1 {
@apply text-3xl sm:text-4xl;
}
h2 {
@apply text-2xl sm:text-3xl;
}
h3 {
@apply text-xl sm:text-2xl;
}
}
@layer utilities {
.primary-text {
@apply text-primary-500 dark:text-primary-400;
}
.primary-text-muted {
@apply text-slate-500 dark:text-slate-400;
}
.decorated {
@apply underline underline-offset-8 decoration-primary-500 dark:decoration-primary-400;
}
/* Soft text color helpers */
.muted {
@apply text-slate-600 dark:text-slate-300;
}
.chip-base {
@apply inline-flex items-center gap-1.5 rounded-xl px-2.5 py-1 text-xs font-medium ring-1 ring-slate-200/70 bg-white/70 text-slate-700 shadow-sm backdrop-blur-sm dark:bg-white/5 dark:text-slate-300 dark:ring-slate-700/50;
}
/* Hide scrollbars for overflow containers */
.no-scrollbar {
-ms-overflow-style: none;
/* IE and Edge */
scrollbar-width: none;
/* Firefox */
}
.no-scrollbar::-webkit-scrollbar {
display: none;
/* Chrome, Safari, Opera */
}
/* Safe area bottom padding for devices with home indicator */
.safe-bottom {
padding-bottom: env(safe-area-inset-bottom);
}
/* Global hover utilities for consistent interactions */
.hover-ring-tint {
/* Ring-only hover with subtle background tint; no scale; focus-visible support */
@apply transition-colors duration-200 ease-out ring-0 hover:ring-2 hover:ring-primary-500 dark:hover:ring-primary-400 hover:bg-primary-50/60 dark:hover:bg-primary-400/10 focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900;
}
.link-hover-clean {
/* Clean link hover with color shift and underline */
@apply transition-colors duration-200 hover:text-primary-600 dark:hover:text-primary-400 hover:underline underline-offset-4 decoration-primary-500/60 dark:decoration-primary-400/60;
}
}
@layer utilities {
/* Minimal hover: subtle bg tint, slight shadow, no ring/border change */
.hover-minimal {
@apply transition-all duration-150 ease-out hover:bg-gray-100/70 dark:hover:bg-white/5 hover:shadow-sm;
}
}
@layer utilities {
/* Minimal outlined button: persistent subtle border and light background; hover handled separately */
.btn-outline-minimal {
@apply border border-gray-300/60 dark:border-gray-600/50 bg-gray-100/30 dark:bg-white/5;
}
}
@layer utilities {
/* Chip-style icon button inspired by SkillGrid: subtle ring, soft bg, minimal hover */
.chip-button {
@apply inline-flex items-center justify-center h-10 w-10 rounded-xl ring-1 ring-slate-200/70 dark:ring-slate-700/50 bg-white/70 dark:bg-white/5 text-slate-700 dark:text-slate-200 shadow-sm backdrop-blur-sm transition-colors duration-150 ease-out hover:bg-white/80 dark:hover:bg-white/10;
}
}
/* View Transitions ripple for theme switch (best-practice) */
@supports (view-transition-name: theme) {
:root {
view-transition-name: theme;
}
/* Configure transition pseudo-elements */
::view-transition-old(theme),
::view-transition-new(theme) {
animation-duration: var(--vtx-duration, 500ms);
animation-timing-function: var(--vtx-easing, ease-in-out);
}
/* Old view: let new view reveal with ripple */
::view-transition-old(theme) {
animation: none;
}
/* New view: reveal from click origin via expanding circle */
::view-transition-new(theme) {
animation-name: vtx-reveal;
}
@keyframes vtx-reveal {
from {
clip-path: circle(0 at var(--vtx-x, 50vw) var(--vtx-y, 50vh));
}
to {
clip-path: circle(var(--vtx-end, 200vmax) at var(--vtx-x, 50vw) var(--vtx-y, 50vh));
}
}
}
/* Fallback fade when View Transitions unsupported or prefers-reduced-motion */
.vtx-fade #__nuxt {
animation: vtx-fade var(--vtx-duration, 500ms) var(--vtx-easing, ease-in-out);
}
@keyframes vtx-fade {
from {
opacity: 0.92;
}
to {
opacity: 1;
}
}
/* Reduced motion guard */
@media (prefers-reduced-motion: reduce) {
:root {
view-transition-name: none;
}
}
/* Locale switch UX: top progress bar and quick page fade */
@layer utilities {
.locale-progress {
position: fixed;
top: 0;
left: 0;
height: 3px;
width: 100%;
z-index: 1000;
pointer-events: none;
/* Use primary color gradient; adapt in dark mode */
background-image: linear-gradient(to right,
rgb(99 102 241 / 0.9),
/* indigo-500 */
rgb(139 92 246 / 0.9)
/* violet-500 */
);
transform-origin: left center;
animation: locale-progress-grow 600ms ease-out forwards;
}
@keyframes locale-progress-grow {
from {
transform: scaleX(0);
opacity: 0.85;
}
to {
transform: scaleX(1);
opacity: 1;
}
}
}
/* Quick page fade during locale switching (client-only via documentElement class) */
.locale-switching #__nuxt {
animation: locale-fade 600ms ease-out both;
}
@keyframes locale-fade {
from {
opacity: 0.92;
filter: saturate(0.98);
}
to {
opacity: 1;
filter: saturate(1);
}
}

View File

@@ -0,0 +1,66 @@
<template>
<ClientOnly>
<!-- Nuxt UI Select-based language picker -->
<USelect v-model="model" :items="items" value-key="value" size="sm" color="primary" variant="soft"
:highlight="false" arrow :trailing="true" placeholder="Language"
class="px-1 w-[64px] sm:w-[76px] rounded-full ring-1 ring-gray-200/70 dark:ring-gray-700/60 backdrop-blur-md shadow-sm h-[25px]"
:ui="{
base: 'rounded-full',
value: 'sr-only',
trailingIcon: 'text-dimmed group-data-[state=open]:rotate-180 transition-transform duration-200',
content: 'min-w-fit'
}" aria-label="Language selector">
<!-- Leading icon in trigger (already provided by :icon via selectedIcon) -->
<template #leading="{ ui }">
<UIcon :name="selectedIcon" class="text-[16px]" />
</template>
<template #item-leading="{ item }">
<UIcon :name="item.icon" class="text-[16px]" />
</template>
<template #item-label="{ item }">
<span>{{ item.label }}</span>
</template>
</USelect>
</ClientOnly>
</template>
<script setup lang="ts">
import { ref, computed, watch } from '#imports'
import { useLocaleSwitching, useLoadingIndicator } from '#imports'
const { locale, setLocale } = useI18n()
type LangValue = 'en' | 'fa'
type Item = { label: string; value: LangValue; icon: string }
const items = ref<Item[]>([
{ label: 'en', value: 'en', icon: 'i-twemoji-flag-united-states' },
{ label: 'fa', value: 'fa', icon: 'i-twemoji-flag-iran' }
])
const model = ref<LangValue>(locale.value as LangValue)
// Keep model in sync if locale changes elsewhere
watch(locale, (val) => {
if ((val as LangValue) !== model.value) {
model.value = val as LangValue
}
})
const selectedIcon = computed<string>(() => items.value.find(i => i.value === model.value)?.icon ?? 'i-twemoji-flag-united-states')
const { startLocaleSwitching } = useLocaleSwitching()
const loading = useLoadingIndicator()
// On selection change, run visual feedback and update i18n
watch(model, (val, oldVal) => {
if (val === oldVal) return
startLocaleSwitching(600)
if (loading) {
loading.start()
setTimeout(() => loading.finish(), 600)
}
setLocale(val)
})
</script>

View File

@@ -0,0 +1,57 @@
<template>
<ClientOnly>
<UTooltip v-if="!colorMode.unknown" :text="tooltip">
<UButton :class="btnClass" class="cursor-pointer" square variant="soft" color="primary" :icon="icon"
:title="label" :aria-label="label" :size="btnSize" @click="toggle($event)" />
</UTooltip>
<UButton v-else :class="btnClass" class="cursor-pointer" square variant="soft" color="primary" :icon="icon"
:title="label" :aria-label="label" :size="btnSize" @click="toggle($event)" />
</ClientOnly>
</template>
<script setup lang="ts">
import { useViewTransitionRipple } from '#imports'
const props = defineProps<{ mobileVisible?: boolean; size?: 'sm' | 'md' | 'lg' }>()
const colorMode = useColorMode()
const modeOrder = ['system', 'light', 'dark'] as const
type Mode = (typeof modeOrder)[number]
const currentPref = computed<Mode>(() => {
const pref = colorMode.preference as string | undefined
return (pref === 'light' || pref === 'dark' || pref === 'system') ? (pref as Mode) : 'system'
})
const nextMap: Record<Mode, Mode> = {
system: 'light',
light: 'dark',
dark: 'system'
}
const nextPref = computed<Mode>(() => nextMap[currentPref.value])
const { runRipple } = useViewTransitionRipple()
const toggle = (ev?: MouseEvent) => {
runRipple(ev ?? null, () => {
colorMode.preference = nextPref.value
}, { duration: 500, easing: 'ease-in-out' })
}
const icon = computed(() => {
switch (currentPref.value) {
// Use colorful Twemoji icons for better visual appeal on mobile
case 'dark': return 'i-twemoji-crescent-moon'
case 'light': return 'i-twemoji-sun'
default: return 'i-twemoji-desktop-computer'
}
})
const label = computed(() => `Switch to ${nextPref.value}`)
const tooltip = computed(() => `Theme: ${currentPref.value} — Click to switch to ${nextPref.value}`)
// Visibility and sizing — ThemeSelector itself always renders; page decides mobile/desktop visibility
const btnClass = computed(() => 'inline-flex')
const btnSize = computed(() => props.size ?? 'md')
</script>

View File

@@ -0,0 +1,61 @@
<template>
<div class="bg-blue-950 backdrop-blur-md hover:cursor-pointer" v-show="open">
<div
class="plausible-event-name=VueSchoolBanner+click flex h-16 shrink-0 items-center justify-between px-4 text-white"
@click="navigate"
>
<div></div>
<div class="hidden lg:block">
<img src="/img/vueschool/vueschool.svg" alt="VueSchool Logo" class="h-6" />
</div>
<div
class="hidden bg-gradient-to-r from-amber-400 to-pink-500 bg-clip-text text-lg font-semibold tracking-wide text-transparent sm:inline-block"
>
New Vue Masterclass
</div>
<div class="hidden lg:flex lg:gap-x-2">
<img src="/img/vueschool/vue-with-hat.svg" alt="VueJS Logo" class="h-6" />
<img src="/img/vueschool/pinia.svg" alt="Pinia Logo" class="h-6" />
<img src="/img/vueschool/vite.svg" alt="Vite Logo" class="h-6" />
<img src="/img/vueschool/vitest.svg" alt="Vitest Logo" class="h-6" />
</div>
<div class="text-center leading-5">
<span
class="inline-block bg-gradient-to-r from-emerald-300 to-sky-300 bg-clip-text font-semibold tracking-wide text-transparent"
>
Vue.js Masterclass 2024 Edition
</span>
<span class="ml-2 hidden font-semibold sm:inline">LAUNCHED</span>
<br />
<span class="text-sm text-gray-300 sm:text-base">
Code a real world app using the latest Vue.js tech
</span>
</div>
<UButton
class="plausible-event-name=VueSchoolBanner+click hidden px-4 font-bold md:inline-flex rounded-full"
label="Buy Now"
icon="i-tabler-caret-right-filled"
variant="solid"
trailing
@click="navigate"
/>
<UIcon
name="i-ph-x"
class="size-8 p-2 hover:opacity-80"
@click.stop="open = false"
/>
</div>
</div>
</template>
<script setup lang="ts">
const open = ref(true)
const affiliateId = "UILIBPIC"
const fullUrl = computed(
() => `https://vueschool.io/the-vuejs-3-master-class?friend=${affiliateId}`
)
const navigate = () => {
window.open(fullUrl.value, "_blank")
}
</script>

View File

@@ -0,0 +1,112 @@
<template>
<div v-if="!hideNav" class="fixed inset-x-0 bottom-0 z-60 md:hidden pointer-events-none">
<div class="pointer-events-auto mx-auto max-w-6xl px-4 pb-2" style="padding-bottom: env(safe-area-inset-bottom);">
<div
class="rounded-2xl bg-white/80 dark:bg-slate-900/70 backdrop-blur-md shadow-lg border border-white/30 dark:border-slate-700/50">
<nav role="navigation" aria-label="Primary bottom navigation"
class="flex items-center justify-between gap-1 sm:gap-2 px-2 sm:px-3 py-1 sm:py-2 overflow-x-auto no-scrollbar flex-nowrap snap-x snap-mandatory">
<UButton :class="[homeActive ? activePillClass : inactivePillClass]" color="emerald" variant="soft" size="md"
icon="i-twemoji-house"
class="h-10 sm:h-11 px-3 sm:px-4 rounded-full focus-visible:ring-2 focus-visible:ring-emerald-500/60 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 snap-center"
aria-label="Home" :aria-current="homeActive ? 'page' : undefined" @click="goHome">
<span class="hidden sm:inline text-sm">{{ t('nav.home') }}</span>
</UButton>
<UButton :class="[inactivePillClass]" color="emerald" variant="soft" size="md"
icon="i-twemoji-hammer-and-wrench"
class="h-10 sm:h-11 px-3 sm:px-4 rounded-full focus-visible:ring-2 focus-visible:ring-emerald-500/60 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 snap-center"
aria-label="Skills" aria-controls="skills" @click="goSkills">
<span class="hidden sm:inline text-sm">{{ t('nav.skills') }}</span>
</UButton>
<UButton :class="[blogActive ? activePillClass : inactivePillClass]" color="emerald" variant="soft" size="md"
icon="i-twemoji-newspaper"
class="h-10 sm:h-11 px-3 sm:px-4 rounded-full focus-visible:ring-2 focus-visible:ring-emerald-500/60 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 snap-center"
aria-label="Blog" :aria-current="blogActive ? 'page' : undefined" @click="goBlog">
<span class="hidden sm:inline text-sm">{{ t('nav.blog') }}</span>
</UButton>
<UButton :class="[inactivePillClass]" color="emerald" variant="soft" size="md" icon="i-twemoji-e-mail"
class="h-10 sm:h-11 px-3 sm:px-4 rounded-full focus-visible:ring-2 focus-visible:ring-emerald-500/60 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 snap-center"
aria-label="Contact" @click="goContact">
<span class="hidden sm:inline text-sm">{{ t('nav.contact') }}</span>
</UButton>
</nav>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useSectionObserver } from '@/composables/useSectionObserver'
const { t, locale } = useI18n()
const appConfig = useAppConfig()
const route = useRoute()
const router = useRouter()
const localePath = useLocalePath()
const blogIndexPath = computed(() => localePath('/blog'))
const homePath = computed(() => localePath('/'))
const isHome = computed(() => route.path === homePath.value)
const { scrollToSection } = useSectionObserver({
enabled: isHome,
headerSelector: 'nav[data-section-header]'
})
// Hide on blog detail pages, show elsewhere
const hideNav = computed(() => {
const p = route.path
// If route path contains '/blog/' segment, it's a detail page.
// This works regardless of locale prefix.
return p.includes('/blog/') && p !== blogIndexPath.value
})
// Active states
const homeActive = computed(() => route.path === homePath.value)
const blogActive = computed(() => route.path.startsWith(blogIndexPath.value) && !hideNav.value)
// Navigation actions
const goHome = () => {
if (!homeActive.value) {
router.push(homePath.value)
}
}
const goBlog = () => {
if (!blogActive.value) {
router.push(blogIndexPath.value)
}
}
const goContact = () => {
// External contact URL in new tab
if (import.meta.client) {
window.open(appConfig.myContactUrl, '_blank', 'noopener')
}
}
const goSkills = async () => {
const targetId = 'skills'
const scrollToSkills = () => {
scrollToSection(targetId)
}
if (route.path === homePath.value) {
// Already on home: smooth scroll
if (import.meta.client) scrollToSkills()
} else {
await router.push(homePath.value)
// Ensure DOM updated before scroll
if (import.meta.client) requestAnimationFrame(scrollToSkills)
}
}
// Styles
const activePillClass = 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 ring-1 ring-emerald-400/40'
const inactivePillClass = 'text-slate-700 dark:text-slate-200 hover:bg-emerald-500/10'
</script>

View File

@@ -0,0 +1,37 @@
<template>
<ClientOnly>
<UTooltip :delay-duration="0" v-bind="tooltipProps">
<slot />
<template
v-for="slotName in namedSlots"
:key="slotName"
v-slot:[slotName]
>
<slot :name="slotName" />
</template>
</UTooltip>
<template #fallback>
<slot />
</template>
</ClientOnly>
</template>
<script setup lang="ts">
import { computed, useAttrs, useSlots } from 'vue'
import type { TooltipProps } from '@nuxt/ui/components/Tooltip.vue'
const props = defineProps<TooltipProps>()
const attrs = useAttrs()
const slots = useSlots()
const tooltipProps = computed(() => ({
...attrs,
...props,
}) as TooltipProps & Record<string, unknown>)
const namedSlots = computed(() =>
Object.keys(slots).filter((name) => name !== 'default'),
)
</script>

View File

@@ -0,0 +1,34 @@
<template>
<footer class="py-10">
<UContainer>
<div class="flex flex-col items-center gap-4 text-center text-sm text-gray-600 dark:text-gray-400">
<NuxtImg :src="logoSrc" alt="Ali Arghyani logo" width="64" height="64" class="h-12 w-12" format="png"
loading="lazy" />
<p>&copy; {{ currentYear }}, <span class="font-semibold text-gray-900 dark:text-gray-100">AliArghyani</span> -
All rights reserved.</p>
<a href="https://github.com/aliarghyani" target="_blank" rel="noopener noreferrer"
class="inline-flex items-center gap-2 text-sm font-medium text-primary-600 transition hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300">
<UIcon name="i-mdi-github" class="text-base" />
<span>GitHub</span>
<UIcon name="i-mdi-open-in-new" class="text-sm opacity-80" />
</a>
</div>
</UContainer>
</footer>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const colorMode = useColorMode()
const currentYear = computed(() => new Date().getFullYear())
const logoSrc = computed(() => {
if (colorMode.unknown) {
return '/favicon/android-chrome-192x192.png'
}
return colorMode.value === 'dark'
? '/favicon/android-chrome-192x192-dark.png'
: '/favicon/android-chrome-192x192.png'
})
</script>

View File

@@ -0,0 +1,177 @@
<template>
<ClientOnly>
<UPopover v-model:open="isOpen" :teleport="true" :popper="{ strategy: 'fixed', placement: 'bottom-end', offset: 8 }"
:ui="{ content: 'z-[120]' }">
<template #default>
<UButton variant="soft" color="primary" square :icon="triggerIcon" :aria-label="t('theme.customizer')"
class="transition-colors duration-200 cursor-pointer" @click="openCustomizer" />
</template>
<template #content>
<div class="p-3 w-80 md:w-96 lg:w-[18rem] space-y-3">
<div class="text-sm font-medium">{{ t('theme.customizer') }}</div>
<div>
<div class="text-xs text-gray-500 dark:text-gray-400 mb-2">
{{ t('nav.theme') }}
</div>
<div class="flex items-center gap-2">
<UTooltip :text="t('theme.mode.system')">
<UButton :color="isModeActive('system') ? 'primary' : 'neutral'" variant="soft" size="sm" square
:class="isModeActive('system') ? 'ring-1 ring-primary-400/40 bg-primary-500/10 dark:bg-primary-400/10' : ''"
icon="i-twemoji-desktop-computer" :aria-label="t('theme.mode.system')"
@click="setMode($event, 'system')" />
</UTooltip>
<UTooltip :text="t('theme.mode.light')">
<UButton :color="isModeActive('light') ? 'primary' : 'neutral'" variant="soft" size="sm" square
:class="isModeActive('light') ? 'ring-1 ring-primary-400/40 bg-primary-500/10 dark:bg-primary-400/10' : ''"
icon="i-twemoji-sun" :aria-label="t('theme.mode.light')" @click="setMode($event, 'light')" />
</UTooltip>
<UTooltip :text="t('theme.mode.dark')">
<UButton :color="isModeActive('dark') ? 'primary' : 'neutral'" variant="soft" size="sm" square
:class="isModeActive('dark') ? 'ring-1 ring-primary-400/40 bg-primary-500/10 dark:bg-primary-400/10' : ''"
icon="i-twemoji-crescent-moon" :aria-label="t('theme.mode.dark')" @click="setMode($event, 'dark')" />
</UTooltip>
</div>
</div>
<div>
<div class="text-xs text-gray-500 dark:text-gray-400 mb-2">
{{ t('theme.primary.label') }}
</div>
<div class="grid grid-cols-3 gap-2">
<UButton v-for="p in primaryItems" :key="p.key" :color="isPrimaryActive(p.key) ? 'primary' : 'neutral'"
variant="soft" size="sm"
:class="['justify-start', isPrimaryActive(p.key) ? 'ring-1 ring-primary-400/40 bg-primary-500/10 dark:bg-primary-400/10' : '']"
:aria-label="`Primary: ${p.label}`" @click="setPrimary($event, p.key)">
<span class="inline-flex items-center gap-2">
<span class="size-3 rounded-full" :class="colorClassMap[p.key]" />
<span class="text-xs capitalize">{{ p.label }}</span>
</span>
</UButton>
</div>
</div>
</div>
</template>
</UPopover>
</ClientOnly>
</template>
<script setup lang="ts">
import { useI18n, useColorMode, useAppConfig, computed, onMounted, ref, nextTick, useViewTransitionRipple } from '#imports'
const { t } = useI18n()
const colorMode = useColorMode()
const appConfig = useAppConfig()
const isOpen = ref(false)
const openCustomizer = async () => {
isOpen.value = true
await nextTick()
}
const primaryItems: Array<{ key: PrimaryColor; label: string }> = [
{ key: 'red', label: 'Red' },
{ key: 'orange', label: 'Orange' },
{ key: 'amber', label: 'Amber' },
{ key: 'yellow', label: 'Yellow' },
{ key: 'lime', label: 'Lime' },
{ key: 'green', label: 'Green' },
{ key: 'emerald', label: 'Emerald' },
{ key: 'teal', label: 'Teal' },
{ key: 'cyan', label: 'Cyan' },
{ key: 'sky', label: 'Sky' },
{ key: 'blue', label: 'Blue' },
{ key: 'indigo', label: 'Indigo' },
{ key: 'violet', label: 'Violet' },
{ key: 'purple', label: 'Purple' },
{ key: 'fuchsia', label: 'Fuchsia' },
{ key: 'pink', label: 'Pink' },
{ key: 'rose', label: 'Rose' }
]
const triggerIcon = computed(() => 'i-twemoji-artist-palette')
type Mode = 'system' | 'light' | 'dark'
const isModeActive = (m: Mode) => (colorMode.preference as Mode) === m
const { runRipple } = useViewTransitionRipple()
const setMode = (ev: MouseEvent, m: Mode) => {
runRipple(ev, () => {
colorMode.preference = m
}, { duration: 500, easing: 'ease-in-out' })
}
const primaryOptions = [
'black',
'red',
'orange',
'amber',
'yellow',
'lime',
'green',
'emerald',
'teal',
'cyan',
'sky',
'blue',
'indigo',
'violet',
'purple',
'fuchsia',
'pink',
'rose'
] as const
type PrimaryColor = typeof primaryOptions[number]
const STORAGE_KEY = 'ui:primary'
const colorClassMap: Record<PrimaryColor, string> = {
black: 'bg-black',
red: 'bg-red-500',
orange: 'bg-orange-500',
amber: 'bg-amber-500',
yellow: 'bg-yellow-500',
lime: 'bg-lime-500',
green: 'bg-green-500',
emerald: 'bg-emerald-500',
teal: 'bg-teal-500',
cyan: 'bg-cyan-500',
sky: 'bg-sky-500',
blue: 'bg-blue-500',
indigo: 'bg-indigo-500',
violet: 'bg-violet-500',
purple: 'bg-purple-500',
fuchsia: 'bg-fuchsia-500',
pink: 'bg-pink-500',
rose: 'bg-rose-500',
}
const setPrimary = (ev: MouseEvent | null, p: PrimaryColor) => {
runRipple(ev, () => {
// Update both shortcuts supported by Nuxt UI
; (appConfig.ui as any).primary = p
; (appConfig.ui as any).colors = {
...((appConfig.ui as any).colors || {}),
primary: p,
}
if (import.meta.client) {
localStorage.setItem(STORAGE_KEY, p)
}
}, { duration: 500, easing: 'ease-in-out' })
}
onMounted(() => {
if (import.meta.client) {
const saved = (localStorage.getItem(STORAGE_KEY) as PrimaryColor | null)
if (saved && primaryOptions.includes(saved)) {
setPrimary(null, saved)
}
}
})
const isPrimaryActive = (p: PrimaryColor) =>
((appConfig.ui as any)?.primary === p) || ((appConfig.ui as any)?.colors?.primary === p)
const activeClass = 'bg-primary-500/10 text-primary-600 dark:text-primary-400'
const inactiveClass = 'text-gray-500 dark:text-gray-300 hover:text-primary-400'
</script>

View File

@@ -0,0 +1,111 @@
<template>
<ClientOnly>
<nav class="fixed inset-x-0 top-0 z-50 pointer-events-auto" data-section-header>
<div class="mx-auto max-w-6xl px-4 pt-2">
<div
class="backdrop-blur-md bg-white/80 dark:bg-slate-900/70 shadow-md rounded-2xl border border-white/30 dark:border-slate-700/50 pointer-events-auto">
<div class="flex items-center justify-between px-2 py-2">
<div class="flex items-center gap-3">
<!-- Home -->
<div class="flex items-center gap-1.5">
<UTooltip :text="t('nav.home')">
<UButton class="cursor-pointer" :class="[isActive('hero') ? activeClass : inactiveClass]"
variant="soft" square icon="i-twemoji-house" :aria-label="t('nav.home')" @click="goTo('hero')" />
</UTooltip>
<button type="button" class="hidden lg:inline-flex text-sm font-medium transition-colors"
:class="[isActive('hero') ? labelActiveClass : labelInactiveClass]" @click="goTo('hero')">
{{ t('nav.home') }}
</button>
</div>
<!-- Skills -->
<div class="flex items-center gap-1.5">
<UTooltip :text="t('sections.skills')">
<UButton class="cursor-pointer" :class="[isActive('skills') ? activeClass : inactiveClass]"
variant="soft" square icon="i-twemoji-hammer-and-wrench" :aria-label="t('sections.skills')"
@click="goTo('skills')" />
</UTooltip>
<button type="button" class="hidden lg:inline-flex text-sm font-medium transition-colors"
:class="[isActive('skills') ? labelActiveClass : labelInactiveClass]" @click="goTo('skills')">
{{ t('sections.skills') }}
</button>
</div>
<!-- Work -->
<div class="flex items-center gap-1.5">
<UTooltip :text="t('sections.work')">
<UButton class="cursor-pointer" :class="[isActive('work') ? activeClass : inactiveClass]"
variant="soft" square icon="i-twemoji-briefcase" :aria-label="t('sections.work')"
@click="goTo('work')" />
</UTooltip>
<button type="button" class="hidden lg:inline-flex text-sm font-medium transition-colors"
:class="[isActive('work') ? labelActiveClass : labelInactiveClass]" @click="goTo('work')">
{{ t('sections.work') }}
</button>
</div>
<!-- Projects -->
<div class="flex items-center gap-1.5">
<UTooltip :text="t('sections.projects')">
<UButton class="cursor-pointer" :class="[isActive('projects') ? activeClass : inactiveClass]"
variant="soft" square icon="i-twemoji-rocket" :aria-label="t('sections.projects')"
@click="goTo('projects')" />
</UTooltip>
<button type="button" class="hidden lg:inline-flex text-sm font-medium transition-colors"
:class="[isActive('projects') ? labelActiveClass : labelInactiveClass]" @click="goTo('projects')">
{{ t('sections.projects') }}
</button>
</div>
</div>
<div class="flex items-center gap-2">
<LanguageSwitcher />
<ThemeCustomizer />
</div>
</div>
</div>
</div>
</nav>
</ClientOnly>
</template>
<script setup lang="ts">
import ThemeCustomizer from '@/components/common/ThemeCustomizer.vue'
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
import { useSectionObserver, type SectionId } from '@/composables/useSectionObserver'
const { t } = useI18n()
const router = useRouter()
const route = useRoute()
const localePath = useLocalePath()
const activeClass = 'ring-1 ring-primary-400/40 bg-primary-500/15 text-primary-600 dark:text-primary-400 transform scale-105'
const inactiveClass = 'text-gray-500 dark:text-gray-300 hover:text-primary-400'
const labelActiveClass = 'text-primary-700 dark:text-primary-400'
const labelInactiveClass = 'text-gray-600 dark:text-gray-300 hover:text-primary-400'
const sectionIds = ['hero', 'skills', 'work', 'projects'] as const
type Target = typeof sectionIds[number]
const isHome = computed(() => route.path === localePath('/'))
const { activeSection, scrollToSection } = useSectionObserver({
ids: [...sectionIds] as SectionId[],
headerSelector: 'nav[data-section-header]',
offset: 80,
enabled: isHome
})
const isActive = (id: Target) => activeSection.value === id
async function goTo(id: Target) {
const homePath = localePath('/')
if (route.path !== homePath) {
await router.push(homePath)
await nextTick()
requestAnimationFrame(() => scrollToSection(id))
} else {
scrollToSection(id)
}
}
</script>

View File

@@ -0,0 +1,190 @@
<template>
<section id="ai-stack" class="py-6 scroll-mt-20">
<UContainer>
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3 mb-4">
<div class="flex items-center gap-3">
<UIcon name="twemoji:robot" class="text-2xl" />
<h2 class="text-lg font-semibold">{{ t('skills.aiStack') }}</h2>
</div>
<div
class="ms-auto w-full md:w-auto flex flex-row-reverse items-center gap-1 overflow-x-auto no-scrollbar whitespace-nowrap px-1 py-1"
role="toolbar" aria-label="AI Stack group filters">
<UButton :icon="filterButtonIcon" size="xs" :variant="filterButtonVariant" :color="filterButtonColor"
class="filter-toggle rounded-full shrink-0 mx-1"
:class="{ 'is-open': filtersOpen, 'has-selection': hasSelection }" :aria-label="filterButtonLabel"
:aria-expanded="hasSelection ? undefined : filtersOpen" aria-controls="ai-group-filter-group"
@click="handlePrimaryAction" />
<Transition name="filter-panel">
<div v-if="filtersOpen" id="ai-group-filter-group" class="flex items-center gap-1">
<TransitionGroup name="filter-chip" tag="div" class="flex flex-row-reverse items-center gap-1">
<div v-for="(opt, index) in groupOptions" :key="opt.value" class="filter-chip"
:style="transitionVars(index, groupOptions.length)">
<UTooltip :text="opt.label" :arrow="true" :delay-duration="0.5"
:content="{ side: 'bottom', sideOffset: 8, collisionPadding: 12 }">
<UButton :icon="opt.icon" size="xs" :variant="isSelected(opt.value) ? 'solid' : 'soft'"
color="primary" class="rounded-full shrink-0" :aria-pressed="isSelected(opt.value)"
:aria-label="`Filter by ${opt.label}`" :title="opt.label" @click="toggleGroup(opt.value)" />
</UTooltip>
</div>
</TransitionGroup>
</div>
</Transition>
</div>
</div>
<div class="grid gap-4 md:grid-cols-3">
<UCard class="md:col-span-3">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
{{ headerTitle }}
</h3>
</div>
</template>
<div class="flex flex-wrap gap-1.5">
<div v-for="item in filtered" :key="item.id" class="inline-flex items-stretch">
<UTooltip :arrow="true" :delay-duration="0.5" :text="item.shortWhy || item.name"
:content="{ side: 'top', sideOffset: 8, collisionPadding: 12 }">
<UBadge variant="soft" class="chip-base">
<span class="inline-flex items-center gap-1.5">
<UIcon v-if="item.icon" :name="item.icon" class="h-4 w-4 min-h-4 min-w-4 text-base" />
<span class="text-xs font-medium cursor-default">{{ item.name }}</span>
<!-- <UBadge size="xs" color="neutral" variant="subtle" class="ml-1">{{ groupLabel(item.group) }}
</UBadge> -->
</span>
</UBadge>
</UTooltip>
</div>
</div>
</UCard>
</div>
</UContainer>
</section>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { AI_GROUPS, aiStackItems, type AiGroup } from '@/data/aiStack'
const { t } = useI18n()
type GroupMeta = { labelKey: string; icon: string }
const GROUP_META: Record<AiGroup, GroupMeta> = {
ide_dev: { labelKey: 'ai_stack.group.ide_dev', icon: 'i-mdi-laptop' },
protocols: { labelKey: 'ai_stack.group.protocols', icon: 'i-mdi-server-network' },
concepts: { labelKey: 'ai_stack.group.concepts', icon: 'i-mdi-lightbulb-outline' },
approaches: { labelKey: 'ai_stack.group.approaches', icon: 'i-mdi-compass-outline' }
}
const groupOptions = computed(() => AI_GROUPS.map(g => ({
value: g,
label: t(GROUP_META[g].labelKey),
icon: GROUP_META[g].icon
})))
const selectedGroups = ref<AiGroup[]>([])
const filtersOpen = ref(true)
const isSelected = (g: AiGroup) => selectedGroups.value.includes(g)
const toggleGroup = (g: AiGroup) => {
const cur = selectedGroups.value
selectedGroups.value = isSelected(g) ? cur.filter(x => x !== g) : [...cur, g]
}
const clear = () => {
selectedGroups.value = []
filtersOpen.value = false
}
const hasSelection = computed(() => selectedGroups.value.length > 0)
const filterButtonIcon = computed(() => hasSelection.value ? 'i-mdi-filter-remove' : 'i-mdi-filter-variant')
const filterButtonLabel = computed(() =>
hasSelection.value
? 'Clear AI stack filters'
: filtersOpen.value
? 'Hide AI stack filters'
: 'Show AI stack filters'
)
const filterButtonVariant = computed(() => hasSelection.value ? 'solid' : filtersOpen.value ? 'soft' : 'ghost')
const filterButtonColor = computed(() => hasSelection.value ? 'error' : 'neutral')
const handlePrimaryAction = () => {
if (hasSelection.value) {
clear()
return
}
filtersOpen.value = !filtersOpen.value
}
const transitionVars = (index: number, total: number) =>
({
'--filter-index': `${index}`,
'--filter-order': `${Math.max(total - index, 0)}`
}) as Record<string, string>
const filtered = computed(() => {
if (selectedGroups.value.length === 0) return aiStackItems
return aiStackItems.filter(i => selectedGroups.value.includes(i.group))
})
const groupLabel = (g: AiGroup) => t(GROUP_META[g].labelKey)
const headerTitle = computed(() => {
if (selectedGroups.value.length === 1) return groupLabel(selectedGroups.value[0]!)
return t('ai_stack.subtitle', 'Methods, tools, rules, and MCPs that power my AI workflow')
})
</script>
<style scoped>
/* Adopt SkillFilters transition styles for smooth group filter UX */
.filter-toggle {
transition: transform 200ms ease, box-shadow 220ms ease, filter 220ms ease;
}
.filter-toggle.has-selection {
box-shadow: 0 12px 28px -12px rgba(220, 38, 38, 0.45);
}
.filter-toggle.is-open:not(.has-selection) :deep([class*='i-mdi']) {
transform: rotate(180deg);
transition: transform 220ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.filter-panel-enter-active,
.filter-panel-leave-active {
transition: opacity 200ms ease, transform 240ms cubic-bezier(0.34, 1.56, 0.64, 1);
transform-origin: right center;
}
.filter-panel-enter-from,
.filter-panel-leave-to {
opacity: 0;
transform: translateX(0.75rem) scale(0.92);
}
.filter-chip-enter-active,
.filter-chip-leave-active {
transition: opacity 200ms ease, transform 260ms cubic-bezier(0.34, 1.56, 0.64, 1);
transform-origin: right center;
}
.filter-chip-enter-active {
transition-delay: calc(var(--filter-index, 0) * 45ms);
}
.filter-chip-leave-active {
transition-delay: calc(var(--filter-order, 0) * 35ms);
}
.filter-chip-enter-from,
.filter-chip-leave-to {
opacity: 0;
transform: translateX(0.5rem) scale(0.85);
}
.filter-chip-move {
transition: transform 200ms ease;
}
</style>

View File

@@ -0,0 +1,38 @@
<template>
<section class="py-6">
<UContainer>
<div class="flex items-center gap-3 mb-3">
<UIcon name="i-twemoji-graduation-cap" class="text-2xl" />
<h2 class="text-lg font-semibold text-start">{{ t('sections.education') }}</h2>
</div>
<div class="space-y-4">
<UCard v-for="(edu, i) in portfolio.education" :key="i"
class="border border-gray-200/60 shadow-none dark:border-gray-700/40">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="flex items-start gap-3 space-y-1">
<div class="w-12 h-12 rounded-lg object-contain shrink-0 flex items-center justify-center">
<ClientOnly>
<NuxtImg v-if="edu.logo" :src="edu.logo" :alt="edu.school" class="max-w-full max-h-full" />
</ClientOnly>
</div>
<div>
<h3 class="text-base font-semibold">{{ edu.school }}</h3>
<p class="text-sm text-gray-600 dark:text-gray-300">{{ edu.degree }}</p>
<p class="text-xs text-gray-500">{{ edu.start }} - {{ edu.end }}</p>
</div>
</div>
<div class="flex shrink-0 flex-wrap items-center gap-1 text-primary-500 dark:text-primary-300">
<UIcon v-for="(ic, k) in edu.icons" :key="k" :name="ic" class="text-xl" />
</div>
</div>
</UCard>
</div>
</UContainer>
</section>
</template>
<script setup lang="ts">
import { usePortfolio } from '@/composables/usePortfolio'
const portfolio = usePortfolio()
const { t } = useI18n()
</script>

View File

@@ -0,0 +1,115 @@
<template>
<section class="py-6">
<UContainer>
<div class="flex items-center gap-3 mb-3">
<UIcon name="i-twemoji-briefcase" class="text-2xl" />
<h2 class="text-lg font-semibold text-start">{{ t('sections.work') }}</h2>
</div>
<div class="space-y-6">
<!-- Company cards -->
<UCard v-for="(company, i) in companies" :key="i"
class="border border-gray-200/60 shadow-none dark:border-gray-700/40">
<!-- Header: company name + meta -->
<div class="flex items-start gap-4">
<div v-if="company.logo"
class="flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-gray-200/70 dark:bg-slate-900 dark:ring-gray-700/60">
<img :src="company.logo" :alt="`${company.company} logo`" class="h-10 w-10 object-contain" loading="lazy" />
</div>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2 text-gray-800 dark:text-gray-100">
<a v-if="company.link" :href="company.link" target="_blank" :aria-label="company.company"
class="text-base font-semibold link-hover-clean">
{{ company.company }}
</a>
<h3 v-else class="text-base font-semibold">{{ company.company }}</h3>
<span v-if="company.type" class="text-xs text-gray-400"></span>
<p v-if="company.type" class="text-xs text-gray-500">{{ company.type }}</p>
</div>
<p v-if="company.location" class="mt-1 text-xs text-gray-500">{{ company.location }}</p>
</div>
</div>
<!-- Timeline -->
<div class="relative mt-4">
<div :class="[
'absolute top-2 bottom-2 w-px bg-gray-200 dark:bg-gray-700',
]" />
<div v-for="(pos, j) in company.positions" :key="j" class="relative flex items-start gap-4">
<div :class="['shrink-0', isRTL ? 'translate-x-[50%]' : 'translate-x-[-50%]']">
<div class="mt-2 h-2 w-2 rounded-full bg-primary-500 ring-4 ring-primary-100 dark:ring-primary-950">
</div>
</div>
<div :class="['min-w-0 flex-1', j !== company.positions.length - 1 ? 'pb-6' : '']">
<div class="flex flex-wrap items-center gap-2">
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ pos.title }}</p>
</div>
<p class="mt-0.5 text-xs text-gray-500">
{{ pos.start }}
<template v-if="pos.ongoing"> - {{ presentText }}</template>
<template v-else-if="pos.end"> - {{ pos.end }}</template>
</p>
<ul :class="[
'mt-2 list-disc space-y-1 text-sm text-gray-700 dark:text-gray-200',
isRTL ? 'pr-5 text-right marker:text-primary-300' : 'pl-5 text-left marker:text-primary-500'
]">
<li v-for="(line, k) in pos.description" :key="k">{{ line }}</li>
</ul>
<div v-if="pos.icons?.length"
class="mt-2 flex flex-wrap items-center gap-1 text-primary-500 dark:text-primary-300">
<UIcon v-for="(ic, k) in pos.icons" :key="k" :name="ic" class="text-xl" />
</div>
</div>
</div>
</div>
</UCard>
</div>
</UContainer>
</section>
</template>
<script setup lang="ts">
import { usePortfolio } from '@/composables/usePortfolio'
import type { CompanyExperience, Experience } from '@/types/portfolio.types'
const { t, locale } = useI18n()
const portfolio = usePortfolio()
const isRTL = computed(() => locale.value === 'fa')
const presentText = computed(() => t('common.present'))
// Normalize experiences into company groups so we can render a timeline per company
type Normalized = Required<Pick<CompanyExperience, 'company' | 'positions'>> &
Pick<CompanyExperience, 'link' | 'logo' | 'location' | 'type'>
const companies = computed<Normalized[]>(() => {
const list = portfolio.value.experiences as Array<CompanyExperience | Experience>
return list.map((exp: any) => {
if (Array.isArray(exp.positions)) {
return exp as Normalized
}
const single = exp as Experience
return {
company: single.company,
location: single.location,
type: single.type,
logo: single.logo,
positions: [
{
title: single.role,
start: single.start,
end: single.end,
ongoing: single.ongoing,
description: single.description,
icons: single.icons,
location: single.location,
type: single.type,
link: single.link,
},
],
}
})
})
</script>

View File

@@ -0,0 +1,16 @@
<template>
<div class="pointer-events-none hidden lg:block">
<div class="fixed left-4 bottom-4 z-40 pointer-events-auto">
<UButton :to="localePath('/blog')" color="primary" icon="i-twemoji-newspaper" variant="soft"
class="shadow-lg cursor-pointer">
{{ t('buttons.blog') }}
</UButton>
</div>
</div>
</template>
<script setup lang="ts">
const { t } = useI18n()
const localePath = useLocalePath()
</script>

View File

@@ -0,0 +1,189 @@
<template>
<section id="hero" class="pt-8 pb-6 scroll-mt-20 flex items-center">
<UContainer>
<div class="flex flex-col-reverse sm:flex-row items-center sm:items-center justify-center gap-4 sm:gap-8">
<div class="flex-1 max-w-2xl text-center sm:text-start">
<h1 class="text-primary text-3xl sm:text-4xl font-bold tracking-tight mb-3 sm:mb-4">
{{ portfolio.profile.name }}
</h1>
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 mb-4 sm:mb-3">
{{ portfolio.profile.summary }}
</p>
<div v-if="portfolio.profile.location || currentRole"
class="mb-6 flex flex-col items-center gap-2 text-sm text-gray-600 dark:text-gray-300 sm:items-start">
<div v-if="portfolio.profile.location" class="flex items-center gap-2">
<UIcon name="i-twemoji-round-pushpin" class="text-base text-primary-600 me-1 dark:text-primary-300" />
<span class="leading-relaxed">{{ portfolio.profile.location }}</span>
</div>
<div v-if="currentRole" class="flex items-center gap-1 text-base text-gray-700 dark:text-gray-200">
<img v-if="currentRole.companyLogo" :src="currentRole.companyLogo" :alt="`${currentRole.company} logo`"
class="h-7 w-7 rounded-md object-contain" loading="lazy" />
<span class="">{{ t('hero.currently') }}</span>
<span class="font-semibold text-primary-600 dark:text-primary-300">
<a v-if="currentRole.companyLink" :href="currentRole.companyLink" target="_blank" rel="noopener"
class="hover:underline text-primary-600 dark:text-primary-300">
{{ currentRole.company }}
</a>
<span v-else>{{ currentRole.company }}</span>
</span>
</div>
</div>
<div class="flex flex-wrap items-center justify-center gap-3 sm:justify-start">
<ClientTooltip :text="emailTooltip">
<UButton icon="i-twemoji-e-mail" :square="true" color="gray" variant="ghost"
class="chip-button cursor-pointer" aria-label="Email" title="Email" @click="copyEmail" />
</ClientTooltip>
<ClientTooltip text="GitHub" v-if="portfolio.profile.socials?.github">
<UButton :to="portfolio.profile.socials.github" target="_blank" icon="i-mdi-github" :square="true"
color="gray" variant="ghost" class="chip-button" aria-label="GitHub" title="GitHub" />
</ClientTooltip>
<ClientTooltip text="LinkedIn" v-if="portfolio.profile.socials?.linkedin">
<UButton :to="portfolio.profile.socials.linkedin" target="_blank" icon="i-logos-linkedin-icon"
:square="true" color="gray" variant="ghost" class="chip-button" aria-label="LinkedIn"
title="LinkedIn" />
</ClientTooltip>
<ClientTooltip text="X" v-if="portfolio.profile.socials?.x || portfolio.profile.socials?.twitter">
<UButton :to="portfolio.profile.socials?.x || portfolio.profile.socials?.twitter" target="_blank"
icon="i-logos-twitter" :square="true" color="gray" variant="ghost" class="chip-button" aria-label="X"
title="X" />
</ClientTooltip>
<ClientTooltip text="Telegram" v-if="portfolio.profile.socials?.telegram">
<UButton :to="portfolio.profile.socials.telegram" target="_blank" icon="i-logos-telegram" :square="true"
color="gray" variant="ghost" class="chip-button" aria-label="Telegram" title="Telegram" />
</ClientTooltip>
<ClientTooltip text="WhatsApp" v-if="portfolio.profile.socials?.whatsapp">
<UButton :to="portfolio.profile.socials.whatsapp" target="_blank" icon="i-logos-whatsapp-icon"
:square="true" color="gray" variant="ghost" class="chip-button" aria-label="WhatsApp"
title="WhatsApp" />
</ClientTooltip>
<ClientTooltip text="Spotify" v-if="portfolio.profile.socials?.spotify">
<UButton :to="portfolio.profile.socials.spotify" target="_blank" :square="true" color="gray"
variant="ghost" class="chip-button" aria-label="Spotify" title="Spotify">
<UIcon name="i-simple-icons-spotify" class="text-[#1DB954] text-lg" />
</UButton>
</ClientTooltip>
<ClientTooltip text="Bento" v-if="portfolio.profile.socials?.bento">
<UButton :to="portfolio.profile.socials.bento" target="_blank" icon="i-simple-icons-bento" :square="true"
color="gray" variant="ghost" class="chip-button" aria-label="Bento" title="Bento" />
</ClientTooltip>
<ClientTooltip text="Instagram" v-if="portfolio.profile.socials?.instagram">
<UButton :to="portfolio.profile.socials.instagram" target="_blank" :square="true" color="gray"
variant="ghost" class="chip-button" aria-label="Instagram" title="Instagram">
<span
class="inline-flex h-5 w-5 items-center justify-center overflow-hidden rounded-[6px] bg-gradient-to-br from-[#f9ce34] via-[#ee2a7b] to-[#6228d7]">
<UIcon name="i-mdi-instagram" class="text-white text-sm" />
</span>
</UButton>
</ClientTooltip>
<ClientTooltip text="Email" v-if="portfolio.profile.socials?.email">
<UButton :to="portfolio.profile.socials.email" icon="i-twemoji-e-mail" :square="true" color="gray"
variant="ghost" class="chip-button" aria-label="Email" title="Email" />
</ClientTooltip>
</div>
</div>
<div
class="block mx-auto sm:mx-0 shrink-0 w-24 h-24 sm:w-32 sm:h-32 md:w-40 md:h-40 ring-4 ring-primary-400/50 dark:ring-primary-300/40 rounded-full overflow-hidden">
<NuxtImg :src="portfolio.profile.avatar || undefined" :alt="portfolio.profile.name"
sizes="96px sm:128px md:160px" width="160" height="160" class="h-full w-full object-cover" format="webp"
preload />
</div>
</div>
</UContainer>
</section>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { usePortfolio } from '@/composables/usePortfolio'
import type { CompanyExperience, Experience } from '@/types/portfolio.types'
const { t } = useI18n()
const portfolio = usePortfolio()
const toast = useToast()
const currentRole = computed(() => {
const experiences = portfolio.value.experiences as Array<CompanyExperience | Experience>
const formatLinkLabel = (url?: string) => {
if (!url) return undefined
try {
return new URL(url).hostname.replace(/^www\./, '')
} catch {
return undefined
}
}
for (const exp of experiences) {
if ('positions' in exp) {
const active = exp.positions.find(pos => pos.ongoing)
if (active) {
const companyLink = exp.link || active.link
const projectLink = active.link || undefined
return {
company: exp.company,
title: active.title,
companyLink,
companyLogo: active.logo || exp.logo,
projectLink,
projectLabel: active.linkLabel || formatLinkLabel(projectLink),
}
}
} else if (exp.ongoing) {
return {
company: exp.company,
title: exp.role,
companyLink: exp.link,
companyLogo: exp.logo,
}
}
}
return null
})
/**
* Email copy-to-clipboard for Hero quick action
*/
const emailAddress = 'aliarghyani@gmail.com'
const emailTooltip = ref('Email')
async function copyEmail() {
try {
await navigator.clipboard.writeText(emailAddress)
emailTooltip.value = 'Copied'
setTimeout(() => { emailTooltip.value = 'Email' }, 1500)
// Nuxt UI toast: success
toast.add({
title: t('toasts.emailCopied.title'),
description: t('toasts.emailCopied.desc', { email: emailAddress }),
icon: 'i-mdi-clipboard-check',
color: 'emerald'
})
} catch {
// Nuxt UI toast: failure (clipboard not accessible)
toast.add({
title: t('toasts.copyFailed.title'),
description: t('toasts.copyFailed.desc', { email: emailAddress }),
icon: 'i-mdi-clipboard-alert',
color: 'amber'
})
// Fallback prompt if clipboard API is unavailable
const ok = typeof window !== 'undefined' && window.confirm(`Copy email:\n\n${emailAddress}`)
if (ok) {
emailTooltip.value = 'Copied'
setTimeout(() => { emailTooltip.value = 'Email' }, 1500)
// Show success toast after manual copy confirmation
toast.add({
title: t('toasts.emailCopied.title'),
description: t('toasts.emailCopied.desc', { email: emailAddress }),
icon: 'i-mdi-clipboard-check',
color: 'emerald'
})
}
}
}
</script>

View File

@@ -0,0 +1,85 @@
<template>
<section class="py-6">
<UContainer>
<div class="flex items-center gap-3 mb-4">
<UIcon name="i-twemoji-globe-showing-europe-africa" class="text-2xl" />
<h2 class="text-lg font-semibold">{{ t('sections.language') }}</h2>
</div>
<UCard>
<div class="space-y-6">
<div class="flex flex-wrap items-center justify-between gap-4">
<p class="text-sm text-gray-600 dark:text-gray-300 max-w-3xl">
{{ t('languageSection.tagline') }}
</p>
<UBadge
size="lg"
color="primary"
variant="soft"
class="inline-flex items-center gap-2"
>
<UIcon
name="simple-icons:duolingo"
class="text-xl text-[#58CC02] dark:text-[#58CC02]"
/>
<span>
{{ t('languageSection.duolingo.label') }}: {{ t('languageSection.duolingo.value') }}
</span>
</UBadge>
</div>
<div class="grid gap-4 md:grid-cols-2">
<div
v-for="item in items"
:key="item.key"
class="flex flex-col gap-2 rounded-lg border border-slate-200/80 p-3 dark:border-slate-700/60"
>
<div class="flex items-center gap-2">
<img
v-if="item.iconType === 'image'"
:src="item.icon"
:alt="item.title"
class="h-6 w-6 object-contain"
/>
<UIcon
v-else
:name="item.icon"
class="text-lg text-primary-500 dark:text-primary-400"
/>
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ item.title }}
</h3>
</div>
<p class="text-sm text-gray-600 dark:text-gray-300">
{{ item.description }}
</p>
</div>
</div>
</div>
</UCard>
</UContainer>
</section>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const { t } = useI18n()
const items = computed(() => [
{
key: 'ielts',
iconType: 'image' as const,
icon: '/img/icons8-ielts-48.png',
title: t('languageSection.items.ielts.title'),
description: t('languageSection.items.ielts.desc'),
},
{
key: 'huawei',
iconType: 'icon' as const,
icon: 'i-twemoji-globe-with-meridians',
title: t('languageSection.items.huawei.title'),
description: t('languageSection.items.huawei.desc'),
},
])
</script>

View File

@@ -0,0 +1,110 @@
<template>
<section id="projects" class="py-6 scroll-mt-20">
<UContainer>
<div class="flex items-center gap-3 mb-3">
<UIcon name="i-twemoji-rocket" class="text-2xl" />
<h2 class="text-lg font-semibold text-start">{{ t('sections.projects') }}</h2>
</div>
<div v-for="g in nonEmptyCategoryList" :key="g.cat" class="space-y-3 mb-6">
<div class="flex items-center gap-2">
<UIcon name="i-twemoji-open-book" class="text-xl" />
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">{{ tCategory(g.cat) }}</h3>
</div>
<div class="grid gap-4 md:grid-cols-2">
<UCard v-for="(p, i) in g.items" :key="`${g.cat}-${i}-${p.name}`"
class="flex h-full flex-col border border-gray-200/60 shadow-none transition hover:-translate-y-1 hover:shadow-lg dark:border-gray-700/40">
<div class="flex h-full flex-col gap-4">
<div class="flex gap-3">
<NuxtImg v-if="p.thumbnail" :src="p.thumbnail" :alt="`${p.name} logo`"
class="h-12 w-12 rounded-xl border border-gray-200/70 bg-white object-cover shadow-sm dark:border-gray-700/40 dark:bg-slate-900"
width="96" height="96" sizes="96px" format="webp" loading="lazy" />
<div v-else
class="flex h-12 w-12 items-center justify-center rounded-xl border border-gray-200/70 bg-primary-500/10 text-primary-600 shadow-sm dark:border-gray-700/40 dark:bg-primary-400/10 dark:text-primary-200">
<UIcon :name="getProjectIcon(p)" class="text-2xl" />
</div>
<div class="flex flex-1 flex-col gap-3">
<div>
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">{{ p.name }}</h3>
<div v-if="p.status || p.opensource" class="mt-2 flex flex-wrap items-center gap-2">
<UBadge v-if="p.status" color="primary" variant="soft" class="rounded-full capitalize">
{{ p.status }}
</UBadge>
<UBadge v-if="p.opensource" color="emerald" variant="soft" class="rounded-full">
<UIcon name="i-mdi-source-branch" class="mr-1" />
{{ t('projectLabels.openSource') }}
</UBadge>
</div>
</div>
<p class="text-sm text-gray-700 dark:text-gray-200">{{ p.description }}</p>
<div class="flex flex-1 flex-col gap-3 pt-1">
<div v-if="p.icons?.length"
class="flex flex-wrap items-center gap-2 text-primary-500 dark:text-primary-300">
<UIcon v-for="(ic, k) in p.icons" :key="k" :name="ic" class="text-xl" />
</div>
<div v-if="p.links?.length" class="mt-auto flex flex-wrap gap-2">
<UButton v-for="(l, j) in p.links" :key="j" :to="l.to" target="_blank" size="xs" color="primary"
variant="soft" trailing-icon="i-mdi-arrow-top-right-thin" class="hover-ring-tint"
:aria-label="l.label">
<UIcon v-if="l.icon" :name="l.icon" class="mr-1" />
{{ l.label }}
</UButton>
</div>
</div>
</div>
</div>
</div>
</UCard>
</div>
</div>
</UContainer>
</section>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { usePortfolio } from '@/composables/usePortfolio'
import type { Project } from '@/types/portfolio.types'
const portfolio = usePortfolio()
const { t } = useI18n()
type Category = NonNullable<Project['category']>
const categories: Category[] = ['current', 'freelance', 'public']
const projectsByCategory = computed<Record<Category, Project[]>>(() => {
// Initialize with all categories to preserve order and allow skipping empty ones
const acc: Record<Category, Project[]> = { current: [], freelance: [], public: [] }
for (const p of portfolio.value.projects) {
const cat = (p.category ?? 'freelance') as Category
acc[cat].push(p)
}
return acc
})
const categoryList = computed<Array<{ cat: Category; items: Project[] }>>(() => {
return categories.map((c) => ({ cat: c as Category, items: projectsByCategory.value[c as Category] }))
})
const nonEmptyCategoryList = computed<Array<{ cat: Category; items: Project[] }>>(() => {
return categoryList.value.filter(g => g.items.length > 0)
})
function tCategory(cat: Category): string {
return t(`projectCategories.${cat}`)
}
function getProjectIcon(project: Project): string {
if (project.thumbnail) return ''
const hasGitLink = project.links?.some(link =>
link.icon === 'i-mdi-github' ||
/github/i.test(link.label) ||
/github\.com/i.test(link.to)
)
if (project.opensource || hasGitLink) {
return 'i-mdi-github'
}
return 'i-twemoji-rocket'
}
</script>

View File

@@ -0,0 +1,190 @@
<template>
<section ref="sectionEl" class="py-8">
<UContainer>
<div class="flex flex-wrap items-center gap-3 mb-8">
<!-- <div
class="p-2 rounded-lg bg-gradient-to-br from-blue-500/10 to-indigo-500/10 dark:from-blue-400/20 dark:to-indigo-400/20">
</div> -->
<div class="flex items-center gap-3 flex-1 min-w-0">
<UIcon name="i-twemoji-left-speech-bubble" class="text-2xl" />
<h2 class="text-2xl font-bold flex items-center gap-2 truncate">
<span>{{ t('sections.recommendations') }}</span>
</h2>
</div>
</div>
<ClientOnly>
<template v-if="shouldRenderCarousel">
<div class="relative">
<UCarousel ref="carouselRef" v-slot="{ item }" dots :auto-scroll="autoScrollOptions" loop
:align="rtl ? 'end' : 'start'" :items="recs" :ui="carouselUi" :breakpoints="carouselBreakpoints">
<div
class="w-full flex flex-col bg-white dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700 rounded-xl p-6 select-none cursor-grab active:cursor-grabbing transition-all duration-300 hover:shadow-xl hover:border-primary dark:hover:border-primary">
<!-- Header -->
<div class="flex items-start gap-3 mb-4">
<div
class="shrink-0 w-12 h-12 rounded-lg bg-gradient-to-br from-[#0A66C2] to-[#004182] flex items-center justify-center shadow-md">
<UIcon name="i-simple-icons-linkedin" class="text-2xl text-white" />
</div>
<div class="min-w-0 flex-1">
<p class="font-semibold text-sm leading-tight text-gray-900 dark:text-gray-100">{{ item.author }}
</p>
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1 line-clamp-2">{{ item.author_title }}</p>
</div>
</div>
<!-- Meta Info -->
<div
class="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-gray-500 dark:text-gray-400 mb-4 pb-3 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center gap-1">
<UIcon name="i-twemoji-briefcase" class="text-sm" />
<span>{{ item.relationship }}</span>
</div>
<span class="text-gray-300 dark:text-gray-600">|</span>
<div class="flex items-center gap-1">
<UIcon name="i-twemoji-calendar" class="text-sm" />
<span>{{ formatDate(item.date) }}</span>
</div>
</div>
<!-- Quote Content -->
<div class="flex-1 mb-4 min-h-[120px]">
<blockquote class="text-sm leading-relaxed text-gray-700 dark:text-gray-300 line-clamp-6">
{{ item.text }}
</blockquote>
</div>
<!-- Footer Buttons -->
<div class="flex items-center gap-2 pt-3 mt-auto border-t border-gray-200 dark:border-gray-700">
<UButton :to="item.linkedin_url" target="_blank" size="xs"
class="flex-1 justify-center bg-[#0A66C2] hover:bg-[#004182] text-white border-0 font-medium">
<UIcon name="i-simple-icons-linkedin" class="text-base text-white" />
<span class="ml-1.5">{{ linkedinText(item.author) }}</span>
</UButton>
<UButton :to="item.recommendation_url" target="_blank" color="gray" variant="soft" size="xs"
icon="i-lucide-external-link" class="flex-1 justify-center hover-ring-tint">
View Full
</UButton>
</div>
</div>
</UCarousel>
<!-- <div class="absolute inset-0 z-50 flex items-center justify-between pointer-events-none px-2 sm:px-4">
<UButton icon="i-lucide-chevron-left" variant="soft" color="gray" size="sm"
class="pointer-events-auto rounded-full bg-white/90 dark:bg-gray-900/70 ring-1 ring-gray-300/60 dark:ring-gray-700/60 shadow-md"
aria-label="Previous recommendation" @click="goToPrev" />
<UButton icon="i-lucide-chevron-right" variant="soft" color="gray" size="sm"
class="pointer-events-auto rounded-full bg-white/90 dark:bg-gray-900/70 ring-1 ring-gray-300/60 dark:ring-gray-700/60 shadow-md"
aria-label="Next recommendation" @click="goToNext" />
</div> -->
</div>
</template>
<template #fallback>
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
<div v-for="placeholder in 3" :key="placeholder"
class="h-full rounded-xl border border-gray-200 dark:border-gray-700 bg-white/80 dark:bg-gray-800/40 p-6 space-y-4 animate-pulse">
<div class="h-4 w-24 rounded bg-gray-200/80 dark:bg-gray-700/60" />
<div class="h-3 w-full rounded bg-gray-200/70 dark:bg-gray-700/50" />
<div class="h-3 w-3/4 rounded bg-gray-200/70 dark:bg-gray-700/50" />
<div class="h-3 w-2/4 rounded bg-gray-200/70 dark:bg-gray-700/50" />
<div class="pt-3 border-t border-gray-200 dark:border-gray-700 flex gap-2">
<div class="h-8 flex-1 rounded bg-gray-200/70 dark:bg-gray-700/50" />
<div class="h-8 flex-1 rounded bg-gray-200/70 dark:bg-gray-700/50" />
</div>
</div>
</div>
</template>
</ClientOnly>
</UContainer>
</section>
</template>
<script setup lang="ts">
import { recommendations as recs } from '@/data/recommendations'
import { useSocialText } from '@/composables/useSocialText'
import { useElementVisibility, usePreferredReducedMotion } from '@vueuse/core'
const { t, locale } = useI18n()
const rtl = computed(() => locale.value === 'fa')
const { linkedinText } = useSocialText()
const sectionEl = ref<HTMLElement | null>(null)
const hasEntered = ref(false)
const reduceMotion = import.meta.client ? usePreferredReducedMotion() : ref<'no-preference'>('no-preference')
const carouselUi = {
root: 'relative overflow-hidden pb-12 select-none',
viewport: 'overflow-visible',
container: 'py-2 !ms-0 gap-3 ps-3 sm:ps-4 pe-3 sm:pe-4 items-stretch',
item: 'basis-full sm:basis-1/2 lg:basis-1/3 flex',
/* Position left/right, centered vertically, above content */
prev: 'hidden',
next: 'hidden',
/* Ensure buttons are always visible and touch-friendly on all sizes */
prevButton: 'opacity-100 pointer-events-auto flex items-center justify-center w-9 h-9 sm:w-10 sm:h-10 rounded-full bg-white/90 dark:bg-gray-900/70 ring-1 ring-gray-300/60 dark:ring-gray-700/60 shadow-md hover:bg-white dark:hover:bg-gray-900 transition',
nextButton: 'opacity-100 pointer-events-auto flex items-center justify-center w-9 h-9 sm:w-10 sm:h-10 rounded-full bg-white/90 dark:bg-gray-900/70 ring-1 ring-gray-300/60 dark:ring-gray-700/60 shadow-md hover:bg-white dark:hover:bg-gray-900 transition',
dots: '!static mt-6 flex justify-center gap-2'
} as const
type CarouselInstance = {
emblaApi?: {
scrollPrev: () => void
scrollNext: () => void
}
}
const carouselRef = ref<CarouselInstance | null>(null)
if (import.meta.client) {
const visibility = useElementVisibility(sectionEl, { threshold: 0.25 })
watch(
visibility,
(visible) => {
if (visible) {
hasEntered.value = true
}
},
{ immediate: true }
)
}
const shouldRenderCarousel = computed(() => import.meta.client && hasEntered.value)
const autoScrollOptions = computed(() => {
if (!import.meta.client || reduceMotion.value === 'reduce') {
return false
}
return {
speed: rtl.value ? -0.55 : 0.55,
stopOnInteraction: true,
stopOnMouseEnter: true
}
})
const carouselBreakpoints = computed<Record<string, any>>(() => {
return rtl.value
? { '(min-width: 0px)': { direction: 'rtl' } }
: { '(min-width: 0px)': { direction: 'ltr' } }
})
watch([rtl, shouldRenderCarousel], async ([isRtl, canMount]) => {
if (!import.meta.client || !canMount) return
await nextTick()
const api = carouselRef.value?.emblaApi as any
if (api && typeof api.reInit === 'function') {
api.reInit({ direction: isRtl ? 'rtl' : 'ltr' })
}
}, { immediate: true })
const formatDate = (d: string) => {
try {
const dt = new Date(d)
const opts: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'short' }
return new Intl.DateTimeFormat(locale.value === 'fa' ? 'fa-IR' : 'en-US', opts).format(dt)
} catch {
return d
}
}
const goToPrev = () => carouselRef.value?.emblaApi?.scrollPrev()
const goToNext = () => carouselRef.value?.emblaApi?.scrollNext()
</script>

View File

@@ -0,0 +1,170 @@
<template>
<div
class="ms-auto w-full md:w-auto flex flex-row-reverse items-center gap-1 overflow-x-auto no-scrollbar whitespace-nowrap px-1 py-1"
role="toolbar" aria-label="Skill filters">
<UButton :icon="filterButtonIcon" size="xs" :variant="filterButtonVariant" :color="filterButtonColor"
class="filter-toggle rounded-full shrink-0 mx-1"
:class="{ 'is-open': filtersOpen, 'has-selection': hasSelection }" :aria-label="filterButtonLabel"
:aria-expanded="hasSelection ? undefined : filtersOpen" aria-controls="skill-filter-group"
@click="handlePrimaryAction" />
<Transition name="filter-panel">
<div v-if="filtersOpen" id="skill-filter-group" class="flex items-center gap-1">
<TransitionGroup name="filter-chip" tag="div" class="flex flex-row-reverse items-center gap-1">
<div v-for="(type, index) in resolvedOptions" :key="type" class="filter-chip"
:style="transitionVars(index, totalVisible)">
<UTooltip :text="meta(type).label" :arrow="true" :delay-duration="0.5"
:content="{ side: 'bottom', sideOffset: 8, collisionPadding: 12 }">
<UButton :icon="meta(type).icon" size="xs" :variant="isSelected(type) ? 'solid' : 'soft'" color="primary"
class="rounded-full shrink-0" :aria-pressed="isSelected(type)"
:aria-label="`Filter by ${meta(type).label}`" :title="meta(type).label" @click="toggle(type)" />
</UTooltip>
</div>
</TransitionGroup>
</div>
</Transition>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import type { SkillType } from '@/types/portfolio.types'
import { SKILL_TYPES } from '@/types/portfolio.types'
// Map each SkillType to an icon and label for tooltips
const TYPE_META: Record<SkillType, { icon: string; label: string }> = {
Language: { icon: 'i-mdi-code-tags', label: 'Language' },
Framework: { icon: 'i-mdi-layers', label: 'Framework' },
Library: { icon: 'i-mdi-book-outline', label: 'Library' },
Tool: { icon: 'i-mdi-wrench', label: 'Tool' },
Service: { icon: 'i-mdi-cloud-outline', label: 'Service' },
DevOps: { icon: 'i-mdi-cogs', label: 'DevOps' },
Testing: { icon: 'i-mdi-flask-outline', label: 'Testing' },
Data: { icon: 'i-mdi-database-outline', label: 'Data' }
}
const meta = (type: SkillType) => TYPE_META[type]
const props = defineProps<{
modelValue: SkillType[]
options?: readonly SkillType[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: SkillType[]): void
}>()
const resolvedOptions = computed(() => props.options ?? SKILL_TYPES)
const hasSelection = computed(() => (props.modelValue?.length ?? 0) > 0)
const filtersOpen = ref(true)
const totalVisible = computed(() => resolvedOptions.value.length)
const isSelected = (type: SkillType) => props.modelValue?.includes(type)
const toggle = (type: SkillType) => {
const current = props.modelValue ?? []
if (current.includes(type)) {
emit('update:modelValue', current.filter(t => t !== type))
} else {
emit('update:modelValue', [...current, type])
}
}
const clear = () => {
emit('update:modelValue', [])
filtersOpen.value = false
}
const handlePrimaryAction = () => {
if (hasSelection.value) {
clear()
return
}
filtersOpen.value = !filtersOpen.value
}
watch(
hasSelection,
value => {
if (value) {
filtersOpen.value = true
}
}
)
const filterButtonIcon = computed(() =>
hasSelection.value ? 'i-mdi-filter-remove' : 'i-mdi-filter-variant'
)
const filterButtonLabel = computed(() =>
hasSelection.value
? 'Clear skill filters'
: filtersOpen.value
? 'Hide skill filters'
: 'Show skill filters'
)
const filterButtonVariant = computed(() =>
hasSelection.value ? 'solid' : filtersOpen.value ? 'soft' : 'ghost'
)
const filterButtonColor = computed(() =>
hasSelection.value ? 'error' : 'neutral'
)
const transitionVars = (index: number, total: number) =>
({
'--filter-index': `${index}`,
'--filter-order': `${Math.max(total - index, 0)}`
}) as Record<string, string>
</script>
<style scoped>
/* On small screens show a single horizontal row with scroll.
On md+ the header already has enough space and this container is auto-sized. */
.filter-toggle {
transition: transform 200ms ease, box-shadow 220ms ease, filter 220ms ease;
}
.filter-toggle.has-selection {
box-shadow: 0 12px 28px -12px rgba(220, 38, 38, 0.45);
}
.filter-toggle.is-open:not(.has-selection) :deep([class*='i-mdi']) {
transform: rotate(180deg);
transition: transform 220ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.filter-panel-enter-active,
.filter-panel-leave-active {
transition: opacity 200ms ease, transform 240ms cubic-bezier(0.34, 1.56, 0.64, 1);
transform-origin: right center;
}
.filter-panel-enter-from,
.filter-panel-leave-to {
opacity: 0;
transform: translateX(0.75rem) scale(0.92);
}
.filter-chip-enter-active,
.filter-chip-leave-active {
transition: opacity 200ms ease, transform 260ms cubic-bezier(0.34, 1.56, 0.64, 1);
transform-origin: right center;
}
.filter-chip-enter-active {
transition-delay: calc(var(--filter-index, 0) * 45ms);
}
.filter-chip-leave-active {
transition-delay: calc(var(--filter-order, 0) * 35ms);
}
.filter-chip-enter-from,
.filter-chip-leave-to {
opacity: 0;
transform: translateX(0.5rem) scale(0.85);
}
.filter-chip-move {
transition: transform 200ms ease;
}
</style>

View File

@@ -0,0 +1,33 @@
<template>
<div class="flex flex-wrap gap-1.5">
<component
v-for="item in items"
:key="item.label"
:is="item.to ? 'a' : 'span'"
:href="item.to || undefined"
target="_blank"
rel="noopener noreferrer"
class="inline-block"
:aria-label="item.to ? `Open ${item.label}` : undefined"
>
<UBadge
variant="soft"
:class="[
'chip-base',
item.to ? 'cursor-pointer transition-colors hover:bg-primary-500/20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:hover:bg-primary-400/10' : ''
]"
>
<span class="inline-flex items-center gap-1.5">
<UIcon v-if="item.icon" :name="item.icon" class="h-4 w-4 min-h-4 min-w-4 text-base" />
<span class="text-xs">{{ item.label }}</span>
</span>
</UBadge>
</component>
</div>
</template>
<script setup lang="ts">
import type { Tag } from '@/types/portfolio.types'
defineProps<{ items: Tag[] }>()
</script>

View File

@@ -0,0 +1,70 @@
<template>
<section id="skills" class="py-6 scroll-mt-20">
<UContainer>
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3 mb-4">
<div class="flex items-center gap-3">
<UIcon name="i-twemoji-hammer-and-wrench" class="text-2xl" />
<h2 class="text-lg font-semibold">{{ t('sections.skills') }}</h2>
</div>
<SkillFilters v-model="selectedTypes" />
</div>
<div class="grid gap-4 md:grid-cols-3">
<UCard>
<template #header>
<h3 class="text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">{{
t('skills.expert') }}</h3>
</template>
<SkillGrid :items="filteredExpert" />
</UCard>
<UCard>
<template #header>
<h3 class="text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">{{
t('skills.proficient') }}</h3>
</template>
<SkillGrid :items="filteredProficient" />
</UCard>
<UCard>
<template #header>
<h3 class="text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">{{
t('skills.usedBefore') }}</h3>
</template>
<SkillGrid :items="filteredUsedBefore" />
</UCard>
<UCard v-if="showAiStack" class="md:col-span-3">
<template #header>
<h3 class="text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">{{
t('skills.aiStack') }}</h3>
</template>
<SkillGrid :items="aiStack" />
</UCard>
</div>
</UContainer>
</section>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { Tag, SkillType } from '@/types/portfolio.types'
import { expert, proficient, usedBefore, aiStack } from '@/data/skills'
import SkillGrid from '@/components/portfolio/SkillGrid.vue'
import SkillFilters from '@/components/portfolio/SkillFilters.vue'
const { t } = useI18n()
// Multi-select filters for Skill types
const selectedTypes = ref<SkillType[]>([])
const filterByType = (items: Tag[]) =>
selectedTypes.value.length === 0
? items
: items.filter(i => i.type && selectedTypes.value.includes(i.type))
const filteredExpert = computed(() => filterByType(expert))
const filteredProficient = computed(() => filterByType(proficient))
const filteredUsedBefore = computed(() => filterByType(usedBefore))
const showAiStack = false
</script>

View File

@@ -0,0 +1,63 @@
<template>
<section class="py-6">
<UContainer>
<div class="flex items-center gap-3 mb-4">
<UIcon name="i-twemoji-brain" class="text-2xl" />
<h2 class="text-lg font-semibold">{{ t('sections.softSkills') }}</h2>
</div>
<UCard>
<div class="flex flex-wrap gap-1.5">
<template v-for="(s, i) in resolved" :key="s.key">
<UTooltip :arrow="true" :delay-duration="0.5" :text="s.description">
<UBadge variant="soft" class="chip-base select-none" :class="chipClass(i)">
<span class="inline-flex items-center gap-1.5">
<UIcon :name="s.icon" class="text-sm" />
<span class="text-xs">{{ s.label }}</span>
</span>
</UBadge>
</UTooltip>
</template>
</div>
</UCard>
</UContainer>
</section>
</template>
<script setup lang="ts">
type SoftKey =
| 'problemSolving'
| 'attentionToDetail'
| 'teamwork'
| 'teamLeadership'
| 'communication'
| 'teamPerformance'
| 'customerFocus'
| 'projectManagement'
| 'adaptability'
import { chipTones } from '@/utils/chipTones'
const base: { key: SoftKey; icon: string }[] = [
{ key: 'problemSolving', icon: 'i-twemoji-light-bulb' },
{ key: 'attentionToDetail', icon: 'i-twemoji-magnifying-glass-tilted-left' },
{ key: 'teamwork', icon: 'i-twemoji-people-holding-hands' },
{ key: 'teamLeadership', icon: 'i-twemoji-crown' },
{ key: 'communication', icon: 'i-twemoji-speech-balloon' },
{ key: 'teamPerformance', icon: 'i-twemoji-chart-increasing' },
{ key: 'customerFocus', icon: 'i-twemoji-handshake' },
{ key: 'projectManagement', icon: 'i-twemoji-spiral-calendar' },
{ key: 'adaptability', icon: 'i-twemoji-counterclockwise-arrows-button' },
]
const { t } = useI18n()
const resolved = computed(() =>
base.map(s => ({
...s,
label: t(`softSkills.${s.key}.label`),
description: t(`softSkills.${s.key}.desc`),
}))
)
const chipClass = (i: number) => chipTones[i % chipTones.length]
</script>

View File

@@ -0,0 +1,48 @@
<template>
<UCard>
<template #header>
<div class="flex items-center gap-2">
<UIcon v-if="iconName" :name="iconName" class="text-lg" />
<h3 class="text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">{{ title ||
group.title }}</h3>
</div>
</template>
<div class="flex flex-wrap gap-2">
<template v-for="(item, idx) in group.items" :key="idx">
<ClientTooltip v-if="item.description" :text="item.description">
<UBadge variant="soft" class="chip-base cursor-default"
:class="colorful ? chipClass(idx) : neutralChipTone">
<span class="inline-flex items-center gap-1.5">
<UIcon v-if="item.icon" :name="item.icon" class="text-base sm:text-lg" />
{{ item.label }}
</span>
</UBadge>
</ClientTooltip>
<UBadge v-else variant="soft" class="chip-base cursor-default"
:class="colorful ? chipClass(idx) : neutralChipTone">
<span class="inline-flex items-center gap-1.5">
<UIcon v-if="item.icon" :name="item.icon" class="text-base sm:text-lg" />
{{ item.label }}
</span>
</UBadge>
</template>
</div>
</UCard>
</template>
<script setup lang="ts">
import type { TagGroup } from '@/types/portfolio.types'
import { chipTones, neutralChipTone } from '@/utils/chipTones'
const props = defineProps<{ group: TagGroup; title?: string; colorful?: boolean; icon?: string }>()
const iconName = computed(() => {
if (props.icon) return props.icon
const title = (props.title || props.group.title).toLowerCase()
if (title.includes('main') && title.includes('tool')) return 'i-twemoji-toolbox'
if (title.includes('role')) return 'i-twemoji-briefcase'
if (title.includes('value')) return 'i-twemoji-sparkles'
return ''
})
const chipClass = (idx: number) => chipTones[idx % chipTones.length]
</script>

View File

@@ -0,0 +1,19 @@
<template>
<section class="py-4">
<UContainer>
<div class="grid gap-4 md:grid-cols-3">
<TagGroupCard :group="portfolio.mainTools" :title="t('sections.mainTools')" />
<TagGroupCard :group="portfolio.roles" :title="t('sections.roles')" />
<TagGroupCard :group="portfolio.values" :title="t('sections.values')" />
</div>
</UContainer>
</section>
</template>
<script setup lang="ts">
import { usePortfolio } from '@/composables/usePortfolio'
import TagGroupCard from '@/components/portfolio/TagGroupCard.vue'
const portfolio = usePortfolio()
const { t } = useI18n()
</script>

View File

@@ -0,0 +1,124 @@
<template>
<section id="work" class="py-6 scroll-mt-20">
<UContainer>
<div class="flex items-center gap-3 mb-6">
<UIcon name="i-twemoji-briefcase" class="text-2xl" />
<h2 class="text-lg font-semibold">{{ t('sections.work') }}</h2>
</div>
<UTimeline :items="experiences" :default-value="0" color="primary" size="md" class="max-w-3xl">
<template #indicator="{ item }">
<img v-if="item.logo" :src="item.logo" :alt="`${item.company} logo`"
class="h-10 w-10 object-contain" loading="lazy" />
</template>
<template #title="{ item }">
<div class="flex flex-col gap-1">
<span class="font-semibold">{{ item.title }}</span>
<span class="text-sm text-gray-600 dark:text-gray-400">{{ item.company }}</span>
</div>
</template>
<template #description="{ item }">
<ul v-if="item.descriptions?.length"
class="mt-2 list-disc space-y-1 text-sm text-gray-700 dark:text-gray-300 pl-5">
<li v-for="(desc, i) in item.descriptions" :key="i">{{ desc }}</li>
</ul>
<div v-if="item.icons?.length" class="mt-3 flex flex-wrap gap-2">
<UIcon v-for="(icon, i) in item.icons" :key="i" :name="icon"
class="text-xl text-primary-500 dark:text-primary-400" />
</div>
</template>
</UTimeline>
</UContainer>
</section>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { usePortfolio } from '@/composables/usePortfolio'
import type { TimelineItem } from '@nuxt/ui'
import type { CompanyExperience, Experience } from '@/types/portfolio.types'
const { t } = useI18n()
const portfolio = usePortfolio()
const presentText = computed(() => t('common.present'))
type RichTimelineItem = TimelineItem & {
company: string
descriptions?: string[]
icons?: string[]
logo?: string
}
const experiences = computed<RichTimelineItem[]>(() => {
const list = portfolio.value.experiences as Array<CompanyExperience | Experience>
const items: RichTimelineItem[] = []
list.forEach((exp: any, index: number) => {
if (Array.isArray(exp.positions)) {
// CompanyExperience with multiple positions
const company = exp as CompanyExperience
company.positions.forEach((pos, posIndex) => {
items.push({
date: `${pos.start}${pos.ongoing ? ` - ${presentText.value}` : pos.end ? ` - ${pos.end}` : ''}`,
title: pos.title,
company: company.company,
descriptions: pos.description,
icons: pos.icons,
icon: getIconForRole(pos.title),
logo: pos.logo || company.logo || getCompanyLogo(company.company),
value: `${index}-${posIndex}`
})
})
} else {
// Single Experience
const single = exp as Experience
items.push({
date: `${single.start}${single.ongoing ? ` - ${presentText.value}` : single.end ? ` - ${single.end}` : ''}`,
title: single.role,
company: single.company,
descriptions: single.description,
icons: single.icons,
icon: getIconForRole(single.role),
logo: single.logo || getCompanyLogo(single.company),
value: index
})
}
})
return items
})
function getCompanyLogo(name: string): string | undefined {
const lower = name.toLowerCase()
if (lower.includes('huawei')) return '/img/huawei.svg'
if (lower.includes('nexaportal')) return '/img/NexaPortal1.png'
return undefined
}
function getIconForRole(position: string): string {
const role = position.toLowerCase()
if (role.includes('senior') || role.includes('lead')) {
return 'i-twemoji-crown'
}
if (role.includes('frontend') || role.includes('front-end')) {
return 'i-twemoji-laptop'
}
if (role.includes('backend') || role.includes('back-end')) {
return 'i-twemoji-server'
}
if (role.includes('fullstack') || role.includes('full-stack')) {
return 'i-twemoji-gear'
}
if (role.includes('developer') || role.includes('engineer')) {
return 'i-twemoji-technologist'
}
if (role.includes('designer')) {
return 'i-twemoji-artist-palette'
}
if (role.includes('manager')) {
return 'i-twemoji-people-holding-hands'
}
return 'i-twemoji-briefcase'
}
</script>

View File

@@ -0,0 +1,27 @@
import { useState } from '#imports'
export function useLocaleSwitching() {
const isLocaleSwitching = useState<boolean>('is-locale-switching', () => false)
let timer: number | null = null
const startLocaleSwitching = (duration = 600) => {
if (!import.meta.client) return
isLocaleSwitching.value = true
document.documentElement.classList.add('locale-switching')
if (timer) {
window.clearTimeout(timer)
timer = null
}
timer = window.setTimeout(() => {
isLocaleSwitching.value = false
document.documentElement.classList.remove('locale-switching')
timer = null
}, duration)
}
return {
isLocaleSwitching,
startLocaleSwitching
}
}

View File

@@ -0,0 +1,9 @@
import type { Ref } from 'vue'
import type { PortfolioData } from '@/types/portfolio.types'
import en from '@/data/portfolio.en'
import fa from '@/data/portfolio.fa'
export function usePortfolio(): Ref<PortfolioData> {
const { locale } = useI18n()
return computed(() => (locale.value === 'fa' ? fa : en))
}

View File

@@ -0,0 +1,240 @@
import { computed, isRef, onBeforeUnmount, onMounted, ref, watch, type Ref } from 'vue'
import { useEventListener, useIntersectionObserver } from '@vueuse/core'
const DEFAULT_SECTION_IDS = ['hero', 'skills', 'work', 'projects'] as const
export type SectionId = (typeof DEFAULT_SECTION_IDS)[number] | string
type EnabledSource = boolean | Ref<boolean> | (() => boolean)
export interface SectionObserverOptions {
ids?: SectionId[]
offset?: number
headerSelector?: string
enabled?: EnabledSource
}
type ResolvedOptions = {
ids: SectionId[]
offset: number
headerSelector?: string
}
const activeState = () => useState<SectionId | null>('section-observer:active', () => null)
const usersState = () => useState<number>('section-observer:users', () => 0)
const manualScrollState = () => useState<boolean>('section-observer:manual', () => false)
const optionsState = () =>
useState<ResolvedOptions>('section-observer:options', () => ({
ids: [...DEFAULT_SECTION_IDS],
offset: 80,
headerSelector: undefined
}))
function computeOffset(headerSelector: string | undefined, fallback: number): number {
if (!import.meta.client) return fallback
if (!headerSelector) return fallback
const el = document.querySelector<HTMLElement>(headerSelector)
const height = el?.getBoundingClientRect().height
return height && !Number.isNaN(height) ? Math.round(height) : fallback
}
function updateActiveFromScroll(options: ResolvedOptions, active: Ref<SectionId | null>) {
if (!import.meta.client) return
const offset = computeOffset(options.headerSelector, options.offset)
const topAnchor = offset + 8
let closest: { id: SectionId; distance: number } | null = null
for (const id of options.ids) {
const el = document.getElementById(id)
if (!el) continue
const rect = el.getBoundingClientRect()
const distance = Math.abs(rect.top - offset)
const aboveTop = rect.top <= topAnchor
const notPassed = rect.bottom > topAnchor
if (aboveTop && notPassed) {
if (!closest || distance < closest.distance) {
closest = { id, distance }
}
}
}
if (!closest) {
// Fallback: choose the first section below the offset when none intersect
for (const id of options.ids) {
const el = document.getElementById(id)
if (!el) continue
const rect = el.getBoundingClientRect()
if (rect.top >= offset - 40) {
closest = { id, distance: Math.abs(rect.top - offset) }
break
}
}
}
if (closest) {
active.value = closest.id
}
}
function refreshObserver(options: ResolvedOptions) {
if (!import.meta.client) return
const active = activeState()
// Stop observing previous targets
const disposables = useState<(() => void)[]>('section-observer:disposables', () => [])
disposables.value.forEach((stop) => stop())
disposables.value = []
const offset = computeOffset(options.headerSelector, options.offset)
options.ids.forEach((id) => {
const el = document.getElementById(id)
if (!el) return
const observer = useIntersectionObserver(
el,
(entries) => {
const entry = entries[0]
if (!entry) return
if (entry.isIntersecting) {
active.value = id
}
},
{
threshold: 0.1,
rootMargin: `-${offset}px 0px -55% 0px`
}
)
disposables.value.push(observer.stop)
})
requestAnimationFrame(() => updateActiveFromScroll(options, active))
}
function teardownObserver() {
if (!import.meta.client) return
const disposables = useState<(() => void)[]>('section-observer:disposables', () => [])
disposables.value.forEach((stop) => stop())
disposables.value = []
}
export function useSectionObserver(options: SectionObserverOptions = {}) {
const active = activeState()
const users = usersState()
const manual = manualScrollState()
const storedOptions = optionsState()
const ids = computed<SectionId[]>(() => options.ids ?? storedOptions.value.ids ?? [...DEFAULT_SECTION_IDS])
const offset = computed(() => options.offset ?? storedOptions.value.offset ?? 80)
const headerSelector = computed(() => options.headerSelector ?? storedOptions.value.headerSelector)
const resolveEnabled = (source?: EnabledSource): boolean => {
if (isRef(source)) return !!source.value
if (typeof source === 'function') return !!(source as () => unknown)()
return (source ?? true) === true
}
const enabled = computed(() => resolveEnabled(options.enabled))
const localEnabled = ref(false)
const scrollStop = ref<(() => void) | null>(null)
function setup() {
if (!import.meta.client) return
storedOptions.value = {
ids: [...ids.value],
offset: offset.value,
headerSelector: headerSelector.value
}
refreshObserver(storedOptions.value)
scrollStop.value = useEventListener(
window,
'scroll',
() => {
if (manual.value) return
requestAnimationFrame(() => updateActiveFromScroll(optionsState().value, activeState()))
},
{ passive: true }
)
}
function disable() {
if (!import.meta.client) return
teardownObserver()
active.value = null
scrollStop.value?.()
scrollStop.value = null
}
onMounted(() => {
watch(
[enabled, ids, offset, headerSelector],
([isEnabled]) => {
if (!import.meta.client) return
if (isEnabled) {
if (!localEnabled.value) {
localEnabled.value = true
if (users.value === 0) {
setup()
} else {
storedOptions.value = {
ids: [...ids.value],
offset: offset.value,
headerSelector: headerSelector.value
}
refreshObserver(storedOptions.value)
}
users.value += 1
} else {
storedOptions.value = {
ids: [...ids.value],
offset: offset.value,
headerSelector: headerSelector.value
}
refreshObserver(storedOptions.value)
}
} else if (localEnabled.value) {
localEnabled.value = false
users.value = Math.max(0, users.value - 1)
if (users.value === 0) {
disable()
}
}
},
{ immediate: true }
)
})
onBeforeUnmount(() => {
if (!import.meta.client) return
if (localEnabled.value) {
localEnabled.value = false
users.value = Math.max(0, users.value - 1)
if (users.value === 0) {
disable()
}
}
})
function scrollToSection(id: SectionId, behavior: ScrollBehavior = 'smooth') {
if (!import.meta.client) return
const el = document.getElementById(id)
if (!el) return
manual.value = true
active.value = id
el.scrollIntoView({ behavior, block: 'start', inline: 'nearest' })
window.setTimeout(() => {
manual.value = false
updateActiveFromScroll(storedOptions.value, active)
}, behavior === 'auto' ? 50 : 650)
}
function setActive(id: SectionId | null) {
active.value = id
}
return {
activeSection: active,
scrollToSection,
setActiveSection: setActive
}
}

View File

@@ -0,0 +1,40 @@
/**
* Social text utilities for localized, consistent labels.
* Generates LinkedIn button labels like:
* - en: "amir's linkedin" (lowercased first name with apostrophe)
* - fa: "لینکدین امیر"
*/
export function getFirstName(fullName: string): string {
if (!fullName) return ''
// Take first token, strip trailing punctuation and leading @
const first = (fullName.trim().split(/\s+/)[0] || '')
.replace(/[.,\-]+$/g, '')
.replace(/^@/, '')
return first
}
export function useSocialText() {
// i18n is auto-imported by Nuxt (via #imports)
const { locale } = useI18n()
/**
* Build a localized LinkedIn label using first name.
* - en: "{firstname}'s linkedin" all lowercase
* - fa: "لینکدین {firstname}"
*/
const linkedinText = (author: string): string => {
const first = getFirstName(author)
if (locale.value === 'fa') {
// Persian phrasing with provided first name as-is (data uses Latin names)
return `لینکدین ${first}`
}
// English lowercased per spec and with apostrophe
return `${first.toLowerCase()}'s linkedin`
}
return {
linkedinText
}
}

View File

@@ -0,0 +1,134 @@
/**
* View Transitions ripple utility (best-practice).
* - Uses document.startViewTransition when available
* - Ripple originates from click position
* - Duration defaults to 500ms, easing to 'ease-in-out'
* - Respects prefers-reduced-motion
* - Graceful fallback: runs update without animation
*
* Usage:
* const { runRipple } = useViewTransitionRipple()
* await runRipple(mouseEvent, () => { // change theme or primary here })
*/
export type RippleOptions = {
duration?: number
easing?: string
}
function isClient(): boolean {
return typeof window !== 'undefined' && typeof document !== 'undefined'
}
function supportsViewTransitions(): boolean {
return isClient() && 'startViewTransition' in document
}
function prefersReducedMotion(): boolean {
if (!isClient()) return false
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
}
/**
* Compute the end radius needed to cover the viewport from an origin point.
*/
function computeEndRadius(x: number, y: number, w: number, h: number): number {
const topLeft = Math.hypot(x, y)
const topRight = Math.hypot(w - x, y)
const bottomLeft = Math.hypot(x, h - y)
const bottomRight = Math.hypot(w - x, h - y)
return Math.max(topLeft, topRight, bottomLeft, bottomRight)
}
/**
* Set CSS variables on :root to drive the ripple animation in CSS:
* --vtx-x: origin X (px)
* --vtx-y: origin Y (px)
* --vtx-end: final circle radius (px)
* --vtx-duration: animation duration (ms)
* --vtx-easing: animation easing function
*/
function setRippleCSSVars(x: number, y: number, end: number, opts: Required<RippleOptions>) {
const root = document.documentElement
root.style.setProperty('--vtx-x', `${x}px`)
root.style.setProperty('--vtx-y', `${y}px`)
root.style.setProperty('--vtx-end', `${end}px`)
root.style.setProperty('--vtx-duration', `${opts.duration}ms`)
root.style.setProperty('--vtx-easing', opts.easing)
}
/**
* Clear the CSS variables used by the ripple.
*/
function clearRippleCSSVars() {
const root = document.documentElement
root.style.removeProperty('--vtx-x')
root.style.removeProperty('--vtx-y')
root.style.removeProperty('--vtx-end')
root.style.removeProperty('--vtx-duration')
root.style.removeProperty('--vtx-easing')
}
/**
* Run the update inside a View Transition with a ripple.
* If unsupported or reduced motion is preferred, runs update immediately.
*/
export function useViewTransitionRipple() {
async function runRipple(ev: MouseEvent | null, update: () => void, options?: RippleOptions): Promise<void> {
if (!isClient()) {
update()
return
}
const duration = options?.duration ?? 500
const easing = options?.easing ?? 'ease-in-out'
const w = window.innerWidth
const h = window.innerHeight
const x = ev?.clientX ?? Math.floor(w / 2)
const y = ev?.clientY ?? Math.floor(h / 2)
const end = computeEndRadius(x, y, w, h)
// Fallback: simple fade if unsupported or reduced motion is preferred
if (!supportsViewTransitions() || prefersReducedMotion()) {
const root = document.documentElement
// set vars for timing and easing used by CSS
root.style.setProperty('--vtx-duration', `${duration}ms`)
root.style.setProperty('--vtx-easing', easing)
root.classList.add('vtx-fade')
// Force reflow to ensure class applies before update
void (root as any).offsetWidth
update()
requestAnimationFrame(() => {
root.classList.remove('vtx-fade')
// cleanup
root.style.removeProperty('--vtx-duration')
root.style.removeProperty('--vtx-easing')
})
return
}
// Prepare CSS vars for the ripple animation
setRippleCSSVars(x, y, end, { duration, easing })
// Start the view transition and perform the update
const transition = (document as any).startViewTransition(() => {
update()
})
// Wait for the transition to finish then cleanup
try {
await transition.finished
} finally {
clearRippleCSSVars()
}
}
async function runRippleFromCenter(update: () => void, options?: RippleOptions): Promise<void> {
return runRipple(null, update, options)
}
return {
runRipple,
runRippleFromCenter,
}
}

236
app/data/aiStack.ts Normal file
View File

@@ -0,0 +1,236 @@
// AI Stack dataset: categories, item schema, and curated initial items
// This file is self-contained to avoid modifying global types.
export type AiCategory =
| 'method'
| 'ide'
| 'assistant'
| 'rule'
| 'mcp'
| 'extension'
| 'infra'
| 'evaluation'
export type AiGroup =
| 'ide_dev'
| 'protocols'
| 'concepts'
| 'approaches'
export const AI_GROUPS: readonly AiGroup[] = [
'ide_dev',
'protocols',
'concepts',
'approaches'
] as const
export type AiItemLinkset = {
setup?: string
rules?: string
example?: string
}
export type AiItem = {
id: string
name: string
category: AiCategory
group: AiGroup
icon?: string
shortWhy?: string
links?: AiItemLinkset
tags?: string[]
featured?: boolean
heat?: number
}
export const AI_CATEGORIES: readonly AiCategory[] = [
'method',
'ide',
'assistant',
'rule',
'mcp',
'extension',
'infra',
'evaluation'
] as const
export const aiStackItems: AiItem[] = [
// Concepts and methodologies
{
id: 'method-bmad',
name: 'BMAD Method',
category: 'method',
group: 'concepts',
icon: 'i-twemoji-jigsaw',
shortWhy: 'Outcome-driven orchestration with one-tool-per-step discipline',
tags: ['bmad', 'workflow', 'governance']
},
{
id: 'method-openspec',
name: 'Spec-Driven Development',
category: 'method',
group: 'concepts',
icon: 'i-twemoji-open-book',
shortWhy: 'Develop AI systems guided by specification before execution',
tags: ['spec', 'planning']
},
{
id: 'concepts-hitl',
name: 'Human-In-The-Loop',
category: 'method',
group: 'concepts',
icon: 'i-twemoji-handshake',
shortWhy: 'Integrate human oversight to improve outputs and control model decisions',
tags: ['concept', 'quality']
},
{
id: 'concepts-agent-coordination',
name: 'Agent-Based Coordination',
category: 'method',
group: 'concepts',
icon: 'i-twemoji-people-holding-hands',
shortWhy: 'Coordinate multiple agents to accomplish complex goals',
tags: ['agents', 'coordination']
},
{
id: 'concepts-token-budget',
name: 'Token Budget Management',
category: 'method',
group: 'concepts',
icon: 'i-twemoji-abacus',
shortWhy: 'Strategically manage context and token usage for LLMs',
tags: ['tokens', 'context']
},
// IDE / Development Environments
{
id: 'ide-vscode',
name: 'VSCode',
category: 'ide',
group: 'ide_dev',
icon: 'i-logos-visual-studio-code',
shortWhy: 'Popular, professional IDE for coding and AI projects',
tags: ['ide', 'vscode']
},
{
id: 'ide-kiro',
name: 'Kiro',
category: 'ide',
group: 'ide_dev',
icon: 'i-twemoji-brain',
shortWhy: 'Intelligent editor and environment for AI software development',
tags: ['ide', 'kiro']
},
{
id: 'ide-cursor',
name: 'Cursor',
category: 'ide',
group: 'ide_dev',
icon: 'i-twemoji-sparkles',
shortWhy: 'AI-native IDE with rules and repo-aware edits',
tags: ['ide', 'cursor']
},
{
id: 'ide-roocode',
name: 'RooCode',
category: 'ide',
group: 'ide_dev',
icon: 'i-twemoji-robot',
shortWhy: 'Auto-planning agents for repo-wide diffs',
tags: ['ide', 'agents']
},
{
id: 'ide-qoder',
name: 'Qoder',
category: 'ide',
group: 'ide_dev',
icon: 'i-twemoji-rocket',
shortWhy: 'Team collaboration and fast AI-powered coding environment',
tags: ['ide', 'qoder']
},
// Protocols & Standards (MCP)
{
id: 'protocols-mcp',
name: 'MCP (Machine Collaboration Protocol)',
category: 'mcp',
group: 'protocols',
icon: 'i-twemoji-globe-with-meridians',
shortWhy: 'Collaboration and coordination protocol for multi-agent AI projects',
tags: ['mcp', 'protocol']
},
{
id: 'mcp-context7',
name: 'Context7 MCP',
category: 'mcp',
group: 'protocols',
icon: 'i-twemoji-open-book',
shortWhy: 'Retrieve up-to-date docs and examples for libraries',
tags: ['mcp', 'context7']
},
{
id: 'mcp-playwright',
name: 'Playwright MCP',
category: 'mcp',
group: 'protocols',
icon: 'i-logos-playwright',
shortWhy: 'Browser automation and testing via MCP-compatible server',
tags: ['mcp', 'playwright']
},
{
id: 'mcp-chrome-devtools',
name: 'Chrome DevTools MCP',
category: 'mcp',
group: 'protocols',
icon: 'i-logos-chrome',
shortWhy: 'Control Chrome DevTools for inspection and performance via MCP',
tags: ['mcp', 'chrome']
},
{
id: 'mcp-browsertools',
name: 'BrowserTools MCP',
category: 'mcp',
group: 'protocols',
icon: 'i-twemoji-globe-with-meridians',
shortWhy: 'General browser tools MCP for automation and scraping',
tags: ['mcp', 'browser']
},
// Emerging approaches
{
id: 'approaches-context-management',
name: 'Context Management',
category: 'method',
group: 'approaches',
icon: 'i-twemoji-card-index-dividers',
shortWhy: 'Control and retain context and memory for long-running agent interactions',
tags: ['context', 'memory']
},
{
id: 'approaches-agent-orchestration',
name: 'Agent Orchestration',
category: 'method',
group: 'approaches',
icon: 'i-twemoji-ringed-planet',
shortWhy: 'Coordinate groups of agents to solve multi-step or complex tasks',
tags: ['agents', 'orchestration']
},
{
id: 'approaches-cot',
name: 'Chain-of-Thought Reasoning',
category: 'method',
group: 'approaches',
icon: 'i-twemoji-thought-balloon',
shortWhy: 'Step-by-step reasoning and analysis with language models',
tags: ['reasoning']
},
{
id: 'approaches-prompt-engineering',
name: 'Prompt Engineering Tools/Techniques',
category: 'method',
group: 'approaches',
icon: 'i-twemoji-hammer-and-wrench',
shortWhy: 'Craft and evaluate prompts to achieve precise, targeted model outputs',
tags: ['prompting']
}
]

4
app/data/portfolio.en.ts Normal file
View File

@@ -0,0 +1,4 @@
import type { PortfolioData } from '@/types/portfolio.types'
import { portfolio } from './portfolio'
export default portfolio satisfies PortfolioData

287
app/data/portfolio.fa.ts Normal file
View File

@@ -0,0 +1,287 @@
import type { PortfolioData } from '@/types/portfolio.types'
const portfolioFa: PortfolioData = {
profile: {
name: 'علی ارغیانی',
title: 'توسعه‌دهنده فرانت‌اند',
// location: 'Tehran Province - Iran , Remote - Turkey',
location: 'استان تهران، ایران',
summary:
'توسعه‌دهندهٔ فرانت‌اند با تجربه در Vue.js، Nuxt.js و TypeScript. مهاجرت از حوزهٔ رهبری تیم و تحلیل در مخابرات به توسعهٔ وب مدرن با تمرکز بر تجربهٔ توسعه‌دهنده، دسترس‌پذیری، کارایی و تحویلِ متمرکز بر نیاز مشتری.',
avatar: '/img/AliProfile.webp',
socials: {
website: 'https://www.linkedin.com/in/aliarghyani/',
github: 'https://github.com/aliarghyani',
linkedin: 'https://www.linkedin.com/in/aliarghyani/',
telegram: 'https://t.me/Ali_Argh',
whatsapp: 'https://wa.me/989123220694',
spotify: 'https://open.spotify.com/user/aliarghyani',
bento: 'https://bento.me/arghyani',
instagram: 'https://www.instagram.com/ali.arghyani/',
},
},
mainTools: {
title: 'ابزارهای اصلی',
items: [
{ label: 'Vue.js', icon: 'i-logos-vue' },
{ label: 'Nuxt.js', icon: 'i-logos-nuxt-icon' },
{ label: 'TypeScript', icon: 'i-logos-typescript-icon' },
{ label: 'Vuetify', icon: 'i-logos-vuetifyjs' },
{ label: 'Tailwind CSS', icon: 'i-logos-tailwindcss-icon' },
{ label: 'Pinia', icon: 'i-logos-pinia' },
{ label: 'Vite', icon: 'i-logos-vitejs' },
{ label: 'Git/GitHub', icon: 'i-mdi-github' },
],
},
roles: {
title: 'نقش‌ها',
items: [
{ label: 'توسعه‌دهندهٔ فرانت‌اند', icon: 'i-twemoji-laptop' },
{ label: 'SSR با Nuxt', icon: 'i-twemoji-rocket' },
{ label: 'مهندسی رابط کاربری', icon: 'i-twemoji-toolbox' },
{ label: 'DX و عملکرد', icon: 'i-twemoji-high-voltage' },
],
},
values: {
title: 'ارزش‌ها',
items: [
{ label: 'خودمختاری و مالکیت', icon: 'i-twemoji-key', description: 'پیش‌قدم و مسئولیت‌پذیر؛ تحویل کار از ابتدا تا انتها.' },
{ label: 'استانداردهای بالا', icon: 'i-twemoji-sparkles', description: 'کیفیت به‌جای میانبر؛ کار را بهتر از قبل رها کن.' },
{ label: 'تحویل متمرکز بر مشتری', icon: 'i-twemoji-handshake', description: 'هدف را بفهم، مرحله‌ای بساز، نتیجه را همسو نگه‌دار.' },
{ label: 'کار تیمی و منتورینگ', icon: 'i-twemoji-people-holding-hands', description: 'دانش را به‌اشتراک بگذار، تیم را رشد بده، قابل اتکا باش.' },
{ label: 'ارتباط شفاف', icon: 'i-twemoji-speech-balloon', description: 'چرایی/چی/چطور را بگو؛ کوتاه و ترجیحاً غیرهم‌زمان.' },
],
},
experiences: [
{
company: 'NexaPortal',
link: 'https://nexaportal.com/',
logo: '/img/NexaPortal1.png',
location: 'ازمیر، ترکیه · ریموت',
type: 'تمام‌وقت',
positions: [
{
title: 'توسعه‌دهندهٔ فرانت‌اند',
start: 'دسامبر ۲۰۲۴',
ongoing: true,
description: [
'مشارکت در پلتفرمی برای دگرگونی مدیریت کسب‌وکار گردشگری سلامت؛ ساخت فرانت‌اندهای امن و مقیاس‌پذیر.',
'پیاده‌سازی معماری Vue 3 + TypeScript با Vuetify، Vite، Pinia، RBAC و i18n؛ تمرکز بر DX، عملکرد و دسترس‌پذیری.',
'ساخت اپ کاربر و داشبورد ادمین: https://app.elaramedical.com/ · https://dashboard.elaramedical.com/',
'قابلیت‌های PWA، فلوهای فرمی سنگین، تقویم و زمان‌بندی، یکپارچه‌سازی با Google API و بلادرنگ با WebSocket.',
'دروازه‌های کیفیت: ESLint سخت‌گیرانه، تست‌های E2E با Cypress، بیلدهای سازگار با CI و کدریویو.'
],
icons: ['i-logos-vue', 'i-logos-vuetifyjs', 'i-logos-typescript-icon', 'i-logos-vitejs', 'i-logos-pinia', 'i-logos-eslint', 'i-logos-cypress'],
link: 'https://app.elaramedical.com/',
linkLabel: 'پلتفرم Elara',
},
],
},
{
company: 'Freelancer',
type: 'خویش‌فرما',
location: 'تهران، ایران · هیبرید',
positions: [
{
title: 'توسعه‌دهندهٔ فرانت‌اند | Vue.js، Nuxt.js، TailwindCSS',
start: 'سپتامبر ۲۰۲۳',
end: 'دسامبر ۲۰۲۴',
description: [
'تحویل اپ‌های SSR پرفورمنس با Nuxt 3 و Vue 3؛ بهبود سرعت و SEO.',
'طراحی سیستم‌های کامپوننتی ماژولار و نگهداشت‌پذیر؛ اطمینان از واکنش‌گرایی و دسترس‌پذیری در همه دستگاه‌ها.',
'همکاری بین‌وظیفه‌ای با Git؛ ارسال مرحله‌ای با تمرکز بر اهداف مشتری.',
'بهره‌گیری از Vuetify و VueUse برای تسریع توسعه؛ قابلیت انطباق سریع با React در صورت نیاز.',
'نمونه‌های اخیر: https://ideh.app/ · https://insho.app/ · https://laservice.ir/ · https://bamashin.net/ · https://hiloop.app/ · https://atdeloop.com/'
],
icons: ['i-logos-nuxt-icon', 'i-logos-vue', 'i-logos-tailwindcss-icon', 'i-logos-vuetifyjs', 'i-logos-typescript-icon'],
},
],
},
{
company: 'Huawei',
logo: '/img/huawei.svg',
location: 'تهران، ایران',
positions: [
{
title: 'کارشناس ارشد عملکرد و رهبر تیم',
start: 'آوریل ۲۰۲۲',
end: 'آگوست ۲۰۲۳',
description: [
'نگهداشت عملکرد و در‌دسترس‌بودن ~۱۴٬۵۰۰ سایت MTN Irancell در سراسر کشور.',
'رهبری تحلیل KPI (2G/3G/4G)، چک‌های TCHA، ریشه‌یابی و پیگیری سرتاسری رخدادها تا حل.',
'تهیه گزارش‌های هفتگی/ماهانه/فصلانه برای ذی‌نفعان؛ پایش OLA/SLA و تصعید ریسک‌ها.',
'هماهنگی پیمانکاران و زیرپیمانکاران؛ برنامه‌ریزی و رهگیری ابتکارهای پرریسک و اقدامات بازیابی.',
'تعریف محدوده، زمان‌بندی، سیاست‌ها و رویه‌ها؛ بهبود فرآیند، ممیزی و کیفیت عملیات.',
'بهینه‌سازی OPEX با حذف هزینه‌های غیرضروری؛ پیش‌بینی و بودجه‌بندی با صورت‌های مالی به‌موقع.',
'مالک ارتباط با مشتری؛ تحقق اهداف مالی و قراردادی به‌موقع.',
'تسلط بر ابزارهای OSS/MW اکوسیستم Ericsson/Huawei/Nokia؛ منتورینگ اعضای تیم.'
],
},
{
title: 'کارشناس ارشد عملکرد',
start: 'جولای ۲۰۱۸',
end: 'جولای ۲۰۲۳',
description: [
'راندن تحلیل KPI شبکه در 2G/3G/LTE؛ شناسایی روندها و فرصت‌های بهبود.',
'مشارکت در ممیزی‌ها، بهبود فرآیند و داشبوردهای عملکرد؛ پشتیبانی از فرایندهای حل رخداد.'
],
},
{
title: 'دستیار مدیر منطقه',
start: 'مارس ۲۰۱۸',
end: 'جولای ۲۰۱۸',
description: [
'نگهداشت ~۳۰۰۰ سایت BTS در استان تهران (2G/3G/4G)؛ تحقق اهداف تحویل/پذیرش و صرفه‌جویی هزینه.',
'مدیریت زیرپیمانکاران و رابط برنامه‌ریزی؛ ترجمه طرح‌های فنی به برنامه‌های اجرایی.',
'رفع موانع حین پذیرش؛ تصعید ریسک‌های خارج از محدوده برای دستیابی به نتیجه برد-برد با مشتری.'
],
},
{
title: 'رهبر تیم TCHA',
start: 'ژوئن ۲۰۱۷',
end: 'مارس ۲۰۱۸',
description: [
'ساخت داشبوردهای جامع در‌دسترس‌بودن؛ مالک اصلی هم‌راستاسازی ذی‌نفعان زیر الزامات قراردادی سخت.',
'تحلیل KPI و کانال‌های ترافیک/کنترلی؛ اقدامات راه‌دور و پیگیری تصعیدها تا حل نهایی.',
'کسب عنوان فارغ‌التحصیل برتر در گردهمایی سالانه Huawei.'
],
},
{
title: 'کارمند بک‌آفیس',
start: 'ژوئن ۲۰۱۶',
end: 'ژوئن ۲۰۱۷',
description: [
'پشتیبانی عملیات OSS، چک‌های عملکرد و گزارش‌دهی؛ کمک به کارایی تیم و رضایت مشتری.'
],
},
],
},
{
company: 'Solar Energy World',
positions: [
{
title: 'مدیر سیستم Solaris',
start: 'جولای ۲۰۱۵',
end: 'ژوئن ۲۰۱۶',
description: ['مانیتورینگ سیستم‌های خورشیدی و ادمین Solaris.'],
},
],
},
{
company: 'Adfa l آدفا',
location: 'استان تهران، ایران',
positions: [
{
title: 'ادمین',
start: 'ژوئن ۲۰۱۵',
end: 'ژوئن ۲۰۱۶',
description: [
'پشتیبانی سخت‌افزار/نرم‌افزار و امور اداری شهرداری منطقه ۳ تهران.',
],
},
],
},
],
education: [
{
school: 'دانشگاه صنعتی قم',
degree: 'کارشناسی مهندسی مخابرات',
start: '۲۰۱۰',
end: '۲۰۱۵',
icons: ['i-material-symbols-school'],
logo: '/img/qut_logo-light.jpg',
},
],
projects: [
{
name: 'vue-cursor-rules',
description: 'قواعد Cursor مبتنی بر قرارداد برای Vue 3 + TypeScript با تمرکز بر DX، دسترس‌پذیری، امنیت و خروجی قابل اتکا.',
links: [
{ label: 'GitHub', to: 'https://github.com/aliarghyani/vue-cursor-rules', icon: 'i-mdi-github' },
],
icons: ['i-logos-vue', 'i-logos-typescript-icon'],
status: 'فعال',
opensource: true,
category: 'public',
},
{
name: 'ایده — پلتفرم نوآوری',
description: 'پلتفرمی پویا و مقیاس‌پذیر برای ارزیابی ایده‌ها و بینش‌های بازار.',
thumbnail: '/img/projects/ideh.png',
status: 'فعال',
opensource: false,
links: [
{ label: 'وب‌سایت', to: 'https://ideh.app/', icon: 'i-mdi-link' },
],
icons: ['i-logos-nuxt-icon', 'i-logos-vue'],
category: 'freelance',
},
{
name: 'Insho — بازار تبلیغات',
description: 'مارکت‌پلیس رسانه برای پیوند دادن آژانس‌ها و تولیدکنندگان محتوا در کمپین‌های تبلیغاتی پراثر.',
thumbnail: '/img/projects/insho.png',
status: 'فعال',
opensource: false,
links: [
{ label: 'وب‌سایت', to: 'https://insho.app/', icon: 'i-mdi-link' },
],
icons: ['i-logos-nuxt-icon', 'i-logos-vue', 'i-logos-tailwindcss-icon'],
category: 'freelance',
},
{
name: 'باماشین — سامانه اجاره ناوگان',
description: 'پلتفرم اجاره برای رزرو خودرو، قایق، بالگرد، ون، دوچرخه و ناوگان متنوع در سراسر ایران.',
thumbnail: '/img/projects/bamashin.png',
status: 'فعال',
opensource: false,
links: [
{ label: 'وب‌سایت', to: 'https://bamashin.net/', icon: 'i-mdi-link' },
],
icons: ['i-logos-nuxt-icon', 'i-logos-vue'],
category: 'freelance',
},
{
name: 'Elara Panel',
description: 'ما بسیار فراتر از یک مجموعه گردشگری سلامت هستیم. بیماران ما ارزشمندترین و محوری‌ترین نقطه ما هستند. خوشحالی آن‌ها، دریافت راهنمایی درست و تجربه خدمات بی‌نقص برای ما حیاتی است. می‌دانیم هر بیمار انتظارات و نیازهای متفاوتی دارد. در این پنل شخصی‌سازی‌شده، به اطلاعاتی دسترسی دارید که مخصوص شماست. در هر آنچه بخواهید شخصی‌سازی کنید کنار شما هستیم. با Elara Medical، درهای یک تجربه فراموش‌نشدنی گردشگری سلامت را بگشایید.',
thumbnail: '/img/elara-logo.png',
status: 'فعال',
opensource: false,
links: [
{ label: 'وب‌سایت', to: 'https://app.elaramedical.com/', icon: 'i-mdi-link' },
],
icons: ['i-logos-vue', 'i-logos-vuetifyjs', 'i-logos-typescript-icon', 'i-logos-vitejs'],
category: 'current',
},
{
name: 'Artemis Clinics',
description: 'آرتمیس، ارائه‌دهنده قابل اعتماد خدمات پزشکی در ترکیه، شما را به طیف گسترده‌ای از خدمات سلامت باکیفیت با هزینه‌ای به‌مراتب کمتر از آمریکای شمالی و اروپا متصل می‌کند، حتی با احتساب هزینه‌های سفر.',
thumbnail: '/img/artemis-new-logo.png',
status: 'فعال',
opensource: false,
links: [
{ label: 'وب‌سایت', to: 'https://app.artemisclinics.com/', icon: 'i-mdi-link' },
],
icons: ['i-logos-nuxt-icon', 'i-logos-vue', 'i-logos-tailwindcss-icon'],
category: 'current',
},
{
name: 'NuxtUi-Portfolio-Ali',
description: 'پورتفولیو من با Nuxt 3 و Nuxt UI v4، شامل پروژه‌ها، مهارت‌ها و سوابق.',
status: 'فعال',
opensource: true,
links: [
{ label: 'GitHub', to: 'https://github.com/aliarghyani/NuxtUi-Portfolio-Ali', icon: 'i-mdi-github' },
],
icons: ['i-logos-nuxt-icon', 'i-logos-vue', 'i-logos-typescript-icon', 'i-logos-tailwindcss-icon'],
category: 'public',
},
],
}
export default portfolioFa

286
app/data/portfolio.ts Normal file
View File

@@ -0,0 +1,286 @@
import type { PortfolioData } from '@/types/portfolio.types'
export const portfolio: PortfolioData = {
profile: {
name: 'Ali Arghyani',
title: 'Frontend Developer',
location: 'Tehran Province - Iran , Remote - Turkey',
summary:
'Passionate Frontend Developer with Vue.js, Nuxt.js, and TypeScript. Migrated from telecom team lead/analyst to modern web development with focus on DX, accessibility, performance and client-centric delivery.',
avatar: '/img/AliProfile.webp',
socials: {
website: 'https://www.linkedin.com/in/aliarghyani/',
github: 'https://github.com/aliarghyani',
linkedin: 'https://www.linkedin.com/in/aliarghyani/',
telegram: 'https://t.me/Ali_Argh',
whatsapp: 'https://wa.me/989123220694',
spotify: 'https://open.spotify.com/user/aliarghyani',
bento: 'https://bento.me/arghyani',
instagram: 'https://www.instagram.com/ali.arghyani/',
},
},
mainTools: {
title: 'Main tools',
items: [
{ label: 'Vue.js', icon: 'i-logos-vue' },
{ label: 'Nuxt.js', icon: 'i-logos-nuxt-icon' },
{ label: 'TypeScript', icon: 'i-logos-typescript-icon' },
{ label: 'Vuetify', icon: 'i-logos-vuetifyjs' },
{ label: 'Tailwind CSS', icon: 'i-logos-tailwindcss-icon' },
{ label: 'Pinia', icon: 'i-logos-pinia' },
{ label: 'Vite', icon: 'i-logos-vitejs' },
{ label: 'Git/GitHub', icon: 'i-mdi-github' },
],
},
roles: {
title: 'Roles',
items: [
{ label: 'Frontend Developer', icon: 'i-twemoji-laptop' },
{ label: 'SSR with Nuxt', icon: 'i-twemoji-rocket' },
{ label: 'UI Engineering', icon: 'i-twemoji-toolbox' },
{ label: 'DX & Performance', icon: 'i-twemoji-high-voltage' },
],
},
values: {
title: 'Values',
items: [
{ label: 'Autonomy & Ownership', icon: 'i-twemoji-key', description: 'Take initiative and be accountable. Deliver end-to-end.' },
{ label: 'High standards', icon: 'i-twemoji-sparkles', description: 'Aim for quality over shortcuts. Leave things better.' },
{ label: 'Client-focused delivery', icon: 'i-twemoji-handshake', description: 'Understand goals, ship iteratively, and align outcomes.' },
{ label: 'Teamwork & Mentoring', icon: 'i-twemoji-people-holding-hands', description: 'Share knowledge, elevate teammates, be reliable.' },
{ label: 'Clear communication', icon: 'i-twemoji-speech-balloon', description: 'Explain the why/what/how. Prefer concise async updates.' },
],
},
experiences: [
{
company: 'NexaPortal',
link: 'https://nexaportal.com/',
logo: '/img/NexaPortal1.png',
location: 'İzmir, Türkiye · Remote',
type: 'Full-time',
positions: [
{
title: 'Frontend Developer',
start: 'Dec 2024',
ongoing: true,
description: [
'Contributing to a platform transforming medical tourism business management; building secure, scalable frontends.',
'Implementing Vue 3 + TypeScript architecture with Vuetify, Vite, Pinia, RBAC and i18n; focus on DX, performance and a11y.',
'Built end-user app and admin dashboard: https://app.elaramedical.com/ · https://dashboard.elaramedical.com/',
'PWA features, form-heavy flows, calendar and scheduling UX, Google API integrations, and realtime via WebSocket.',
'Quality gates: ESLint strict, E2E tests with Cypress, CIfriendly builds and code reviews.'
],
icons: ['i-logos-vue', 'i-logos-vuetifyjs', 'i-logos-typescript-icon', 'i-logos-vitejs', 'i-logos-pinia', 'i-logos-eslint', 'i-logos-cypress'],
link: 'https://app.elaramedical.com/',
linkLabel: 'Elara Platform',
},
],
},
{
company: 'Freelancer',
type: 'Self-employed',
location: 'Tehran, Iran · Hybrid',
positions: [
{
title: 'Frontend Developer | Vue.js, Nuxt.js, TailwindCSS',
start: 'Sep 2023',
end: 'Dec 2024',
description: [
'Delivered highperformance SSR apps with Nuxt 3 and Vue 3, improving speed and SEO.',
'Designed modular, maintainable component systems; ensured responsive, accessible UIs across devices.',
'Collaborated with crossfunctional teams using Git; shipped iteratively with clear clientfocused outcomes.',
'Leveraged Vuetify and VueUse to accelerate delivery; comfortable adapting to React when needed.',
'Recent: https://ideh.app/ · https://insho.app/ · https://laservice.ir/ · https://bamashin.net/ · https://hiloop.app/ · https://atdeloop.com/'
],
icons: ['i-logos-nuxt-icon', 'i-logos-vue', 'i-logos-tailwindcss-icon', 'i-logos-vuetifyjs', 'i-logos-typescript-icon'],
},
],
},
{
company: 'Huawei',
logo: '/img/huawei.svg',
location: 'Tehran, Iran',
positions: [
{
title: 'Senior Performance Team Analyst and Team Lead',
start: 'Apr 2022',
end: 'Aug 2023',
description: [
'Maintained performance and availability for ~14,500 MTN Irancell sites nationwide.',
'Led KPI analysis (2G/3G/4G), TCHA checks, rootcause analysis and endtoend incident followups to resolution.',
'Produced weekly/monthly/quarterly stakeholder reports; ensured contractual OLA/SLA compliance and risk escalation.',
'Coordinated contractors and subcontractors; planned and tracked highrisk initiatives and recovery actions.',
'Defined scope, schedules, policies and procedures; improved processes, audits and operational quality.',
'Optimized OPEX by eliminating needless costs; forecasting and budgeting with timely financial statements.',
'Owned customer communication interface; ensured financial and contractual targets were met on time.',
'Handson with OSS/MW tools across Ericsson/Huawei/Nokia ecosystems; mentored team members.'
],
},
{
title: 'Senior Performance Analyst',
start: 'Jul 2018',
end: 'Jul 2023',
description: [
'Drove network KPI analysis across 2G/3G/LTE; identified trends and improvement opportunities.',
'Contributed to audits, process improvements and performance dashboards; supported incident resolution workflows.'
],
},
{
title: 'Assistant Regional Manager',
start: 'Mar 2018',
end: 'Jul 2018',
description: [
'Maintained ~3000 BTS sites across Tehran Province (2G/3G/4G); ensured delivery/acceptance and costsaving targets.',
'Managed subcontractors and planning interface; translated technical specs into executable implementation plans.',
'Removed blockers during acceptance; escalated outofscope risks to achieve winwin outcomes with the customer.'
],
},
{
title: 'TCHA Team Lead',
start: 'Jun 2017',
end: 'Mar 2018',
description: [
'Built comprehensive availability dashboards; main owner driving stakeholder alignment under strict contracts.',
'Analyzed KPIs and traffic/control channels; performed remote actions and tracked escalations endtoend to resolution.',
'Recognized as outstanding fresh graduate in Huawei annual meeting.'
],
},
{
title: 'Back Office Employee',
start: 'Jun 2016',
end: 'Jun 2017',
description: [
'Supported OSS operations, performance checks and reporting; contributed to team efficiency and customer satisfaction.'
],
},
],
},
{
company: 'Solar Energy World',
positions: [
{
title: 'Solaris System Administrator',
start: 'Jul 2015',
end: 'Jun 2016',
description: ['Solar systems monitoring and Solaris administration.'],
},
],
},
{
company: 'Adfa l آدفا',
location: 'Tehran Province, Iran',
positions: [
{
title: 'Administrator',
start: 'Jun 2015',
end: 'Jun 2016',
description: [
'Hardware/software support and administrative tasks for Municipality of District 3, Tehran.',
],
},
],
},
],
education: [
{
school: 'Qom University of Technology',
degree: 'B.A., Telecommunications Engineering',
start: '2010',
end: '2015',
icons: ['i-material-symbols-school'],
logo: '/img/qut_logo-light.jpg',
},
],
projects: [
{
name: 'vue-cursor-rules',
description: 'Contract-driven Cursor rules for Vue 3 + TypeScript with a focus on DX, a11y, security and production-ready outputs.',
links: [
{ label: 'GitHub', to: 'https://github.com/aliarghyani/vue-cursor-rules', icon: 'i-mdi-github' },
],
icons: ['i-logos-vue', 'i-logos-typescript-icon'],
status: 'Active',
opensource: true,
category: 'public',
},
{
name: 'Ideh — Innovating Ideas Platform',
description: 'Dynamic, scalable platform for idea evaluation and market insights.',
thumbnail: '/img/projects/ideh.png',
status: 'Active',
opensource: false,
links: [
{ label: 'Website', to: 'https://ideh.app/', icon: 'i-mdi-link' },
],
icons: ['i-logos-nuxt-icon', 'i-logos-vue'],
category: 'freelance',
},
{
name: 'Insho Advertising Marketplace',
description: 'Media marketplace that helps agencies and creators collaborate on high-impact advertising campaigns.',
thumbnail: '/img/projects/insho.png',
status: 'Active',
opensource: false,
links: [
{ label: 'Website', to: 'https://insho.app/', icon: 'i-mdi-link' },
],
icons: ['i-logos-nuxt-icon', 'i-logos-vue', 'i-logos-tailwindcss-icon'],
category: 'freelance',
},
{
name: 'BaMashin Mobility Rentals',
description: 'Rental platform for booking cars, boats, helicopters, vans, bikes, and more across Iran.',
thumbnail: '/img/projects/bamashin.png',
status: 'Active',
opensource: false,
links: [
{ label: 'Website', to: 'https://bamashin.net/', icon: 'i-mdi-link' },
],
icons: ['i-logos-nuxt-icon', 'i-logos-vue'],
category: 'freelance',
},
{
name: 'Elara Panel',
description: 'More than a medical tourism platform — a personalized experience where each patient accesses tailored information and flawless service. With Elara Medical, unlock an unforgettable medical tourism journey.',
thumbnail: '/img/elara-logo.png',
status: 'Active',
opensource: false,
links: [
{ label: 'Website', to: 'https://app.elaramedical.com/', icon: 'i-mdi-link' },
],
icons: ['i-logos-vue', 'i-logos-vuetifyjs', 'i-logos-typescript-icon', 'i-logos-vitejs'],
category: 'current',
},
{
name: 'Artemis Clinics',
description: 'Your trusted provider for medical services in Turkey, connecting you to top-quality health services at significantly lower costs than North America and Europe — even including travel expenses.',
thumbnail: '/img/artemis-new-logo.png',
status: 'Active',
opensource: false,
links: [
{ label: 'Website', to: 'https://app.artemisclinics.com/', icon: 'i-mdi-link' },
],
icons: ['i-logos-nuxt-icon', 'i-logos-vue', 'i-logos-tailwindcss-icon'],
category: 'current',
},
{
name: 'NuxtUi-Portfolio-Ali',
description: 'My portfolio built with Nuxt 3 and Nuxt UI v4, showcasing projects, skills, and experiences.',
status: 'Active',
opensource: true,
links: [
{ label: 'GitHub', to: 'https://github.com/aliarghyani/NuxtUi-Portfolio-Ali', icon: 'i-mdi-github' },
],
icons: ['i-logos-nuxt-icon', 'i-logos-vue', 'i-logos-typescript-icon', 'i-logos-tailwindcss-icon'],
category: 'public',
},
],
}
export default portfolio

133
app/data/recommendations.ts Normal file
View File

@@ -0,0 +1,133 @@
import type { Recommendation } from '@/types/recommendation.types'
export const recommendations: Recommendation[] = [
{
author: "Amir P.",
author_title:
"Project, Program and Operations Management | Telecom, IT, Infrastructure, Engineering and Construction | Incident Management | Continuous Improvement | Soccer Coach and Referee",
relationship: "Worked with Ali but on different teams",
date: "2024-12-05",
linkedin_url: "https://www.linkedin.com/in/amir-p-30b64450",
recommendation_url:
"https://www.linkedin.com/in/aliarghyani/details/recommendations/?detailScreenTabIndex=0",
text:
"I am pleased to write this recommendation for Ali, who has been an exceptional engineer and a valued member of our team. Over the course of his career, he has consistently demonstrated growth, commitment, and a strong work ethic. His dedication to continuous improvement and delivering high-quality results has set him apart. One of Ali's most admirable traits is his curiosity. He is never afraid to ask insightful questions to fully understand the complexities of a task. This proactive approach to learning has enabled him to tackle challenges with confidence and precision. His ability to absorb new information and apply it effectively has been a key factor in his successful contributions to our projects. In addition to his technical skills, Ali has shown an outstanding attitude towards his work and colleagues. His positive mindset, coupled with his strong interpersonal skills, makes him a natural fit in a multicultural work environment. He communicates well, listens actively, and is always willing to collaborate with others to achieve the best possible outcome. I am confident that Ali will continue to excel and make a significant impact in his career. His dedication, attitude, and approach to problem-solving will make him a valuable asset to any team or organization. I highly recommend him without reservation.",
},
{
author: "Sohrab Amini",
author_title: "Vice President - Digital Transformation & AI Platforms",
relationship: "Was senior to Ali but didnt manage Ali directly",
date: "2024-11-23",
linkedin_url: "https://www.linkedin.com/in/sohrab-amini-67849a141",
recommendation_url:
"https://www.linkedin.com/in/aliarghyani/details/recommendations/?detailScreenTabIndex=0",
text:
"I have worked with Ali during one of my previous assignments, I found him self-driven, interacts effectively with stakeholders and a relentless commitment to delivering on promises. He often worked tirelessly to ensure that even the most challenging tasks were completed to the highest standard. I am confident his Network Performance analytics and programming skillsets holds more career achievements to come!",
},
{
author: "Parham Nooralishahi",
author_title:
"Staff Software Designer @ Trusted Positioning Inc. TDK | Drones and Robotics | Embedded System | Computer Vision | Deep Learning | Software Engineering",
relationship: "Worked with Ali but on different teams",
date: "2024-11-18",
linkedin_url: "https://www.linkedin.com/in/parham-nooralishahi",
recommendation_url:
"https://www.linkedin.com/in/aliarghyani/details/recommendations/?detailScreenTabIndex=0",
text:
"I know Ali from my time in Huawei Tech. I am delighted to recommend him, an industrious person whom I have had the pleasure of teaching Python. In my opinion, he was one of the most promising students who participated in the program. Throughout our time together, Ali has demonstrated an impressive willingness and ability to apply programming skills in practical and innovative ways, particularly in automating tasks within his work. During the program, he quickly grasped the fundamental concepts of Python and has consistently shown a keen interest in leveraging these skills to enhance efficiency. He used the gained knowledge to automate some aspects of Telecom. KPI monitoring which in my opinion, shows his eagerness to go one step further in his job and his natural skills in programming. During the sessions in which Ali has participated, we discussed the python syntax, lambda functions, database, networking, Restful web services, HTML templating using python, and object-oriented programming. He is a professional that constantly tries to improve his skills and help the organization in which he is working in any way he can. I believe he can be an important asset for any company and I wish him constant success in the future.",
},
{
author: "Mona Moslemi",
author_title: "Senior Configuration and Change Management team leader",
relationship: "Worked with Ali but at different companies",
date: "2024-11-12",
linkedin_url: "https://www.linkedin.com/in/mona-moslemi-a7408773",
recommendation_url:
"https://www.linkedin.com/in/aliarghyani/details/recommendations/?detailScreenTabIndex=0",
text:
"Ali and I collaborated in the telecommunications industry for about five years. During this time, Ali handled multiple roles, adapting quickly and excelling in each due to his dedication, intelligence, and work ethic. Alis commitment to his work was one of his most defining qualities. He approached every task with thoroughness, following up consistently to ensure that projects were completed to the highest standard. Even though we werent on the same team and company, his collaborative spirit and professionalism made him a standout colleague. I truly enjoyed the opportunity to work with him and am confident he will be an asset in any organization he joins.",
},
{
author: "Mahmood Rohani",
author_title: "Head of DevOps Engineering @AloPeyk | AWS Solution Architect",
relationship: "Worked with Ali on the same team",
date: "2024-11-12",
linkedin_url: "https://www.linkedin.com/in/mahmood-rohani",
recommendation_url:
"https://www.linkedin.com/in/aliarghyani/details/recommendations/?detailScreenTabIndex=0",
text:
"I had the pleasure of working with Ali at Huawei, where we were in different teams but collaborated closely. During our time, Ali consistently demonstrated dedication, hard work, and a keen attention to detail. He was always respectful, maintaining a logical and calm demeanor even in challenging situations. Ali was highly motivated and always pursued excellence in his work. I know he has recently started a new career path in front-end development, and with the perseverance, talent, and work ethic I've seen in him, I have no doubt that he will achieve great success, just as he has in the past. I highly recommend him!",
},
{
author: "Ghasem Danesh",
author_title: "IT CS Manager at Zarrin Roya",
relationship: "Worked with Ali but on different teams",
date: "2024-11-10",
linkedin_url: "https://www.linkedin.com/in/ghasem-danesh",
recommendation_url:
"https://www.linkedin.com/in/aliarghyani/details/recommendations/?detailScreenTabIndex=0",
text:
"I had the pleasure of working with Ali during his tenure in Huawei's Performance team, where he frequently collaborated with our IT department to address network and technical issues. Ali stood out as a dedicated team player, always proactive in finding solutions—not only for his own needs but also for any IT or network challenges that affected his team. Ali demonstrated a strong commitment to maintaining seamless operations within his team, often reaching out to troubleshoot and resolve issues with our IT department promptly. His resourcefulness and ability to coordinate solutions ensured minimal disruption and contributed to a smoother workflow for everyone involved. Ali's dedication and collaborative mindset were invaluable, and I am confident he will bring the same drive and support to any organization.",
},
{
author: "Arya Aghaei",
author_title:
"Frontend Developer @ BIKE24 | Computer Software Engineering",
relationship: "Worked with Ali but on different teams",
date: "2024-11-09",
linkedin_url: "https://www.linkedin.com/in/arya-aghaei",
recommendation_url:
"https://www.linkedin.com/in/aliarghyani/details/recommendations/?detailScreenTabIndex=0",
text:
"It was my pleasure to work with Ali Arghyani at Huawei Technologies. He is one of those employees who comes with passion and commitment to everything he is doing. Ali was outstandingly enthusiastic about learning, mainly programming, from day one. I watched him dive into new skills, always challenging himself to improve, and his talent for creativity, effective solutions was impressive. Having the front-row seat to observe his development, from the career development paths he chose down to how he committed himself each day, is a real inspiration. I would be thrilled to recommend Ali to any team, since he is one of those people who adds value with his presence, and I am excited to see all that he will do in life.",
},
{
author: "Milad Natanzi",
author_title:
"Research Assistant @ WPI | O-RAN and AI integrator for Next-G RAN | DS | DE | AI | ML | LLM Developer",
relationship: "Worked with Ali but on different teams",
date: "2024-11-07",
linkedin_url: "https://www.linkedin.com/in/natanzi",
recommendation_url:
"https://www.linkedin.com/in/aliarghyani/details/recommendations/?detailScreenTabIndex=0",
text:
"I had the opportunity to work with Ali in the same department on a Huawei project. He is incredibly talented and smart, always eager to learn more and willing to collaborate with everyone. Ali has a genuine passion for his work and a knack for keeping the team motivated, making him a fantastic teammate and someone you can always rely on to get things done right.",
},
{
author: "Amir Asadi",
author_title:
"Results-Driven Operations Management Professional | Expert in Logistics Efficiency, Supply Chain Optimization and Last Mile Delivery.",
relationship: "Managed Ali directly",
date: "2024-11-05",
linkedin_url: "https://www.linkedin.com/in/amir-asadi-",
recommendation_url:
"https://www.linkedin.com/in/aliarghyani/details/recommendations/?detailScreenTabIndex=0",
text:
"I had the pleasure of working alongside Ali, and I can confidently say he is one of the most dedicated, detail-oriented, and solution-focused engineers Ive collaborated with. Throughout our time together, he consistently demonstrated reliability and a proactive approach to solving challenges. Alis approachable demeanor and his strong track record of teamwork make him an invaluable asset to any team. I highly recommend him for any position or project that demands skill, dedication, and professionalism.",
},
{
author: "Muhammad Faisal Iqbal",
author_title:
"Network Technical Support | Telecommunications Expert LTE, 5G | CCNA | RHCSA",
relationship: "Worked with Ali on the same team",
date: "2024-11-04",
linkedin_url:
"https://www.linkedin.com/in/muhammad-faisal-iqbal-739a0b168",
recommendation_url:
"https://www.linkedin.com/in/aliarghyani/details/recommendations/?detailScreenTabIndex=0",
text:
"Ive worked closely with Ali on several projects over the years, and hes someone Ive always been able to count on. Hes a quick learner, a dedicated problem-solver, and brings a positive attitude to every challenge. No matter how tough the situation, Ali is always ready to help and never hesitates to ask questions to get the best result. His friendly, team-first approach has made a real difference in our work together. As he moves into web development, I have no doubt hell be successful. His drive to improve, strong analytical skills, and willingness to learn make him well-suited to any field he sets his mind to. I fully recommend Ali and believe hell thrive in this new career. Best, Faisal",
},
{
author: "Sami Ali",
author_title: "Deputy Project Director-SVMS Operation",
relationship: "Managed Ali directly",
date: "2024-11-04",
linkedin_url: "https://www.linkedin.com/in/sami-ali-2543852b",
recommendation_url:
"https://www.linkedin.com/in/aliarghyani/details/recommendations/?detailScreenTabIndex=0",
text:
"Ali Arghyani led Performance team with a strong focus on delivering results and enhancing operational efficiencies. His expertise in network performance and data analysis consistently drove improvements, while his exceptional communication skills allowed him to build positive relationships with both customers and colleagues. creating a productive and cohesive work environment. Since transitioning into frontend development, Ali has continued to demonstrate adaptability and a commitment to excellence. He is a results-oriented professional who combines technical skill with strong interpersonal abilities, making him a valuable asset in any field. I highly recommend Ali as a dedicated team player and impactful contributor.",
},
]

301
app/data/sections.ts Normal file
View File

@@ -0,0 +1,301 @@
export const sections = [
{
name: "Elements" as const,
icon: "i-heroicons-square-3-stack-3d",
components: [
{
name: "Accordion" as const,
aliases: ["Collapse", "Disclosure", "Expansion Panel"],
help: "Vertically collapsing accordion panels",
},
{
name: "Alert" as const,
help: "Contextual information to be emphasized to the user",
},
{ name: "Avatar" as const, help: "Rounded image of a resource" },
{
name: "Badge" as const,
aliases: ["Chip", "Tag"],
help: "Short text indicator to represent a status",
},
{
name: "Button" as const,
help: "Interactive element that users can click or tap to perform an action",
},
{
name: "Select Button" as const,
help: "A button (or button group) used for selection.",
},
{
name: "Toggle Button" as const,
help: "A button used to select a boolean value.",
},
{
name: "Breadcrumb" as const,
help: "Indicate the current page's location within a navigational hierarchy",
},
{
name: "Carousel" as const,
aliases: ["Slideshow", "Galleria"],
help: "A slideshow component for cycling through elements, typically images or slides of text",
},
{
name: "Chat Bubble" as const,
help: "Show one line of conversation and its data, like author image, name, time.",
},
{
name: "Divider" as const,
aliases: ["Separator"],
help: "Visual element which separates content vertically or horizontally.",
},
{
name: "Dropdown" as const,
aliases: ["Select Menu"],
help: "Toggle contextual overlays for displaying lists of actions or items to select.",
},
{
name: "Listbox" as const,
help: "Select one or more values from a displayed list of items.",
},
{
name: "Indicator" as const,
help: "Used to place an element on the corner of another element",
},
{
name: "Mockup" as const,
help: "A fake box (browser, code, phone, window) around the content",
},
{ name: "Panel Splitter" as const, help: "Used to separate and resize panels" },
{
name: "QR Code" as const,
help: "A component that generates a QR code based on the provided data.",
},
{
name: "Keyboard Key" as const,
help: "A keyboard key in a text block, typically to display a shortcut to a user",
},
{
name: "Scroll Bar" as const,
aliases: ["Scroll Area"],
help: "A customizable and cross browser alternative to native browser scrollbar",
},
{
name: "Speed Dial" as const,
aliases: ["Floating Action Button"],
help: "When pressed, this floating action button, displays multiple other action buttons. It is used to quickly access features.",
},
{
name: "Stack" as const,
help: "Stack visually puts elements on top of each other",
},
{
name: "Timeline" as const,
help: "A visual representation used to display a sequence of events or processes in chronological order.",
},
],
},
{
name: "Inputs" as const,
icon: "i-heroicons-pencil-square",
components: [
{
name: "Auto Complete" as const,
help: "An input component that provides real-time suggestions when being typed",
},
{
name: "Color Picker" as const,
help: "An input component to select a color",
},
{
name: "Input Chips" as const,
help: "Used to enter multiple values on a single input fields",
},
{
name: "OTP Input" as const,
help: "Used for MFA procedure of authenticating users by a one-time password",
},
{
name: "Date Picker" as const,
aliases: ["Calendar"],
help: "A popover to select a date or a time from a calendar-like interface",
},
{
name: "Rich Text Editor" as const,
help: "A component that allows users to format and style text, offering various options like bold, italics, lists, and more, without needing to write HTML or other markup code",
},
{ name: "Input" as const, help: "An input field" },
{
name: "Input Mask" as const,
help: "Automatically formats inputs like date, currency, email and phone",
},
{
name: "Knob" as const,
help: "An input element to adjust a value by turning or dragging the control, similar to a physical knob on an electronic device. It's often used to control volume, brightness, or other continuous values",
},
{
name: "Password Indicator" as const,
help: "Helper to provide information on password field like strength, while entering it",
},
{ name: "Textarea" as const, help: "A textarea field" },
{ name: "Select" as const, help: "A select field" },
{
name: "Swap" as const,
help: "Allows to toggle the visibility of two elements",
},
{
name: "Signature Pad" as const,
help: "A component that allows users to draw a signature using a signature pad.",
},
{ name: "Checkbox" as const, help: "A checkbox field" },
{ name: "Radio" as const, help: "A radio field" },
{
name: "Toggle" as const,
aliases: ["Switch", "Input Switch"],
help: "A toggle field",
},
{ name: "Range" as const, aliases: ["Slider"], help: "A range field" },
{
name: "Rating" as const,
help: "A set of radio buttons that allow the user to rate something",
},
{
name: "File Input" as const,
aliases: ["File Upload"],
help: "An input field for uploading files",
},
],
},
{
name: "Navigation" as const,
icon: "i-heroicons-link-solid",
components: [
{
name: "Bottom Tabs" as const,
help: "Bottom navigation that allows navigation between primary screens, usually used on mobile",
},
{
name: "Command Palette" as const,
help: "A powerful user interface element that allows users to access various commands, functions, or actions from a single input panel",
},
{ name: "Dock" as const, help: "A macOS-like Dock to navigate, using icons" },
{
name: "Pagination" as const,
aliases: ["Paginator"],
help: "Navigation system for large amount of data that need to be displayed one at a time",
},
{
name: "Progress" as const,
aliases: ["Progress Bar", "Progress Indicator"],
help: "A graphical element used to visualize the progression of a particular process, such as a file download, a setup process, or any task that might take some time to complete",
},
{
name: "Scroll Spy" as const,
aliases: ["Anchor"],
help: "A navigation mechanism that automatically highlights the navigation links based on the scroll position to indicate which section of the page is currently in the viewport. It's a common feature in single-page websites or long pages with distinct sections",
},
{
name: "Scroll Top" as const,
aliases: ["Back Top"],
help: "Display an item after a certain scroll position. It's usually used to navigates to the top of the page",
},
{
name: "Steps" as const,
help: "Used to show a list of steps in a process",
},
{
name: "Tabs" as const,
help: "A set of tab panels that are displayed one at a time",
},
{
name: "Vertical Navigation" as const,
help: "Navigation menu with items vertically stacked",
},
],
},
{
name: "Overlays" as const,
icon: "i-heroicons-square-2-stack",
components: [
{
name: "Modal" as const,
aliases: ["Dialog"],
help: "Dialog box or popup window that is displayed on top of the current page.",
},
{
name: "Slideover" as const,
aliases: ["Slide-out panel", "Sliding Overlay", "Offcanvas", "Drawer", "Sheet"],
help: "Extra content or navigation options to slide over the main content of a page, from one side of the screen",
},
{
name: "Popover" as const,
help: "A transient view that appears above other content onscreen when a user clicks or taps on a control or within a defined area",
},
{
name: "Tooltip" as const,
help: "A brief, informative message that appears when a user hovers over an element",
},
{
name: "Context Menu" as const,
help: "A dedicated menu that appears on right click",
},
{
name: "Toast" as const,
aliases: ["Notification", "Snack Bar"],
help: "A small, non-modal notification that provides feedback to a user, typically appearing for a short duration and then disappearing automatically",
},
{
name: "Tour" as const,
help: "Used to display a contextual help on features to new users",
},
{
name: "Watermark" as const,
help: "Add specific text or patterns to the page. It can be used to identify copyright or prevent information theft.",
},
],
},
{
name: "Layout" as const,
icon: "i-ph-layout",
components: [
{ name: "Card" as const, help: "Content with a header, body and footer" },
{ name: "Container" as const, help: "Let constrain the user's content" },
{
name: "Skeleton" as const,
aliases: ["Placeholder"],
help: "A placeholder typically used while content is loading",
},
],
},
{
name: "Data" as const,
icon: "i-heroicons-table-cells",
components: [
{
name: "Table" as const,
help: "Data displayed in a tabular format",
},
{
name: "Calendar" as const,
help: "Data displayed in a calendar format (weekly or monthly view)",
},
{
name: "Order List" as const,
help: "A component to display a list of items with actions to sort those items",
},
{
name: "Tree" as const,
aliases: ["Organization Chart"],
help: "A component to visualize and interact with hierarchical data",
},
{
name: "Pick List" as const,
aliases: ["Transfer"],
help: "A component for moving items between different lists and ordering them",
},
{
name: "Virtual Scroller" as const,
help: "A performant approach to render large amounts of data efficiently",
},
],
},
]

63
app/data/skills.ts Normal file
View File

@@ -0,0 +1,63 @@
import type { Tag } from '@/types/portfolio.types'
export const expert: Tag[] = [
{ label: 'Vue.js', icon: 'i-logos-vue', to: 'https://vuejs.org', type: 'Framework' },
{ label: 'TypeScript', icon: 'i-logos-typescript-icon', to: 'https://www.typescriptlang.org', type: 'Language' },
{ label: 'JavaScript', icon: 'i-logos-javascript', to: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript', type: 'Language' },
{ label: 'HTML5', icon: 'i-logos-html-5', to: 'https://developer.mozilla.org/en-US/docs/Web/HTML', type: 'Language' },
{ label: 'CSS', icon: 'i-logos-css-3', to: 'https://developer.mozilla.org/en-US/docs/Web/CSS', type: 'Language' },
{ label: 'Tailwind CSS', icon: 'i-logos-tailwindcss-icon', to: 'https://tailwindcss.com', type: 'Framework' },
{ label: 'Vuetify', icon: 'i-logos-vuetifyjs', to: 'https://vuetifyjs.com', type: 'Library' },
{ label: 'Pinia', icon: 'i-logos-pinia', to: 'https://pinia.vuejs.org', type: 'Library' },
{ label: 'Vite', icon: 'i-logos-vitejs', to: 'https://vitejs.dev', type: 'Tool' },
{ label: 'Axios', icon: 'simple-icons:axios', to: 'https://axios-http.com', type: 'Library' },
{ label: 'Git', icon: 'i-logos-git-icon', to: 'https://git-scm.com', type: 'DevOps' },
{ label: 'GitHub', icon: 'i-logos-github-octocat', to: 'https://github.com', type: 'Service' },
{ label: 'ESLint', icon: 'i-logos-eslint', to: 'https://eslint.org', type: 'Tool' },
{ label: 'Prettier', icon: 'i-logos-prettier', to: 'https://prettier.io', type: 'Tool' },
{ label: 'Ant Design', icon: 'i-logos-ant-design', to: 'https://ant.design', type: 'Library' },
{ label: 'vue-i18n', icon: 'i-twemoji-globe-with-meridians', to: 'https://vue-i18n.intlify.dev', type: 'Library' },
{ label: 'Jira', icon: 'i-logos-jira', to: 'https://www.atlassian.com/software/jira', type: 'Service' },
{ label: 'Slack', icon: 'i-logos-slack-icon', to: 'https://slack.com', type: 'Service' },
{ label: 'GitHub Actions', icon: 'simple-icons:githubactions', to: 'https://docs.github.com/actions', type: 'DevOps' },
]
export const proficient: Tag[] = [
{ label: 'Nuxt.js', icon: 'i-logos-nuxt-icon', to: 'https://nuxt.com', type: 'Framework' },
{ label: 'Postman', icon: 'logos:postman-icon', to: 'https://www.postman.com', type: 'Tool' },
{ label: 'Swagger', icon: 'logos:swagger', to: 'https://swagger.io', type: 'Tool' },
{ label: 'Sass', icon: 'i-logos-sass', to: 'https://sass-lang.com', type: 'Language' },
{ label: 'Nuxt UI', icon: 'i-logos-nuxt-icon', to: 'https://ui.nuxt.com', type: 'Library' },
{ label: 'Bootstrap', icon: 'i-logos-bootstrap', to: 'https://getbootstrap.com', type: 'Framework' },
{ label: 'Cypress', icon: 'logos:cypress-icon', to: 'https://www.cypress.io', type: 'Testing' },
{ label: 'Pusher', icon: 'simple-icons:pusher', to: 'https://pusher.com', type: 'Service' },
{ label: 'WebSocket', icon: 'i-twemoji-electric-plug', to: 'https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API', type: 'Library' },
{ label: 'ECharts', icon: 'simple-icons:apacheecharts', to: 'https://echarts.apache.org', type: 'Library' },
{ label: 'Chatwoot', icon: 'simple-icons:chatwoot', to: 'https://www.chatwoot.com', type: 'Service' },
]
export const usedBefore: Tag[] = [
{ label: 'GraphQL', icon: 'i-logos-graphql', to: 'https://graphql.org', type: 'Data' },
{ label: 'LESS', icon: 'i-logos-less', to: 'https://lesscss.org', type: 'Language' },
{ label: 'Python', icon: 'i-logos-python', to: 'https://www.python.org', type: 'Language' },
{ label: 'NumPy', icon: 'i-logos-numpy', to: 'https://numpy.org', type: 'Data' },
{ label: 'Pandas', icon: 'logos:pandas-icon', to: 'https://pandas.pydata.org', type: 'Data' },
{ label: 'Linux', icon: 'i-logos-linux-tux', to: 'https://www.kernel.org', type: 'DevOps' },
{ label: 'Iconify', icon: 'simple-icons:iconify', to: 'https://iconify.design', type: 'Library' },
{ label: 'Notion', icon: 'i-logos-notion-icon', to: 'https://www.notion.so', type: 'Service' },
{ label: 'GitLab', icon: 'logos:gitlab-icon', to: 'https://gitlab.com', type: 'DevOps' },
{ label: 'Playwright', icon: 'i-logos-playwright', to: 'https://playwright.dev', type: 'Testing' },
]
export const aiStack: Tag[] = [
{ label: 'Cursor', icon: 'i-mdi-cursor-default-click' },
{ label: 'Qoder', icon: 'i-mdi-robot-outline' },
{ label: 'Kiro', icon: 'i-mdi-brain' },
{ label: 'AgentRouter', icon: 'i-mdi-router-network' },
{ label: 'OpenSpec', icon: 'i-mdi-file-document-outline' },
{ label: 'RooCode', icon: 'i-mdi-code-braces' },
{ label: 'NotebookLM', icon: 'i-mdi-notebook-outline' },
{ label: 'context7 MCP', icon: 'i-mdi-server-network' },
{ label: 'BrowserMCP Tools', icon: 'i-mdi-web' },
{ label: 'Chrome DevTools', icon: 'i-mdi-google-chrome' },
]

View File

@@ -0,0 +1,7 @@
<template>
<UContainer>
<UAlert color="yellow" variant="soft" title="Blog disabled">
Nuxt Content is temporarily disabled. Blog post rendering will be restored later.
</UAlert>
</UContainer>
</template>

22
app/pages/blog/index.vue Normal file
View File

@@ -0,0 +1,22 @@
<template>
<section class="py-10">
<UContainer>
<div class="mb-8 flex flex-wrap items-center justify-between gap-4">
<div>
<p class="text-sm uppercase tracking-wide text-primary-500 dark:text-primary-300">{{ t('blog.explore') }}</p>
<h1 class="text-3xl font-semibold">{{ t('sections.blog') }}</h1>
</div>
</div>
<div class="rounded-xl border border-dashed border-gray-300 p-8 text-center text-gray-500 dark:border-gray-700 dark:text-gray-300">
Nuxt Content is temporarily disabled. Blog will be back soon.
</div>
</UContainer>
</section>
</template>
<script setup lang="ts">
const { locale, t } = useI18n()
// Blog listing requires @nuxt/content which is disabled for now
</script>

56
app/pages/index.vue Normal file
View File

@@ -0,0 +1,56 @@
<template>
<div class="max-w-6xl mx-auto pt-24">
<!-- Above-the-fold content only (improves LCP by reducing initial render work) -->
<Hero />
<!-- Below-the-fold sections rendered on server for SEO (SSR) -->
<!-- <TagGroups /> -->
<Skills />
<AIStack />
<SoftSkills />
<LanguageSkills />
<WorkExperience />
<EducationList />
<RecommendationsCarousel />
<ProjectsList />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, defineAsyncComponent } from 'vue'
import Hero from '@/components/portfolio/Hero.vue'
const TagGroups = defineAsyncComponent(() => import('@/components/portfolio/TagGroups.vue'))
const Skills = defineAsyncComponent(() => import('@/components/portfolio/Skills.vue'))
const AIStack = defineAsyncComponent(() => import('@/components/portfolio/AIStack.vue'))
const SoftSkills = defineAsyncComponent(() => import('@/components/portfolio/SoftSkills.vue'))
const LanguageSkills = defineAsyncComponent(() => import('@/components/portfolio/LanguageSkills.vue'))
const WorkExperience = defineAsyncComponent(() => import('@/components/portfolio/WorkExperience.vue'))
const EducationList = defineAsyncComponent(() => import('@/components/portfolio/EducationList.vue'))
const RecommendationsCarousel = defineAsyncComponent(() => import('@/components/portfolio/RecommendationsCarousel.vue'))
const ProjectsList = defineAsyncComponent(() => import('@/components/portfolio/ProjectsList.vue'))
import { usePortfolio } from '@/composables/usePortfolio'
const appConfig = useAppConfig()
const portfolio = usePortfolio()
const { t, locale } = useI18n()
const siteTitle = computed(() => `${portfolio.value.profile.name}${t('meta.portfolioTitleSuffix')}`)
const description = computed(() => `${portfolio.value.profile.title}. ${portfolio.value.profile.summary}`)
useHead(() => ({
title: siteTitle.value,
link: [
{ rel: 'icon', type: 'image/svg+xml', href: '/favicon/newlogo.png' },
],
}))
useSeoMeta({
title: () => siteTitle.value,
description: () => description.value,
ogTitle: () => siteTitle.value,
ogDescription: () => description.value,
ogType: 'website',
ogLocale: () => (locale.value === 'fa' ? 'fa_IR' : 'en_US'),
})
</script>

View File

@@ -0,0 +1,6 @@
export default defineNuxtPlugin(() => {
const script = document.createElement("script")
script.src = "https://media.bitterbrains.com/main.js?from=UILIB&type=top"
script.async = true
document.head.appendChild(script)
})

View File

@@ -0,0 +1,30 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<!-- Top left square -->
<path
fill="none"
stroke="#41B883"
stroke-width="2"
d="M4 5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1z"
/>
<!-- Top right square -->
<path
fill="none"
stroke="#41B883"
stroke-width="2"
d="M14 5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1z"
/>
<!-- Bottom left square -->
<path
fill="none"
stroke="#41B883"
stroke-width="2"
d="M4 16a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1z"
/>
<!-- Bottom right square -->
<path
fill="none"
stroke="#41B883"
stroke-width="2"
d="M14 13a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1z"
/>
</svg>

After

Width:  |  Height:  |  Size: 801 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="#10b981" d="M12 2C6.49 2 2 6.49 2 12s4.49 10 10 10a2.5 2.5 0 0 0 2.5-2.5c0-.61-.23-1.2-.64-1.67a.528.528 0 0 1-.13-.33c0-.28.22-.5.5-.5H16c3.31 0 6-2.69 6-6c0-4.96-4.49-9-10-9zm5.5 11c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5s1.5.67 1.5 1.5s-.67 1.5-1.5 1.5zm-3-4c-.83 0-1.5-.67-1.5-1.5S13.67 6 14.5 6s1.5.67 1.5 1.5S15.33 9 14.5 9zM5 11.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5S7.33 13 6.5 13S5 12.33 5 11.5zm6-4c0 .83-.67 1.5-1.5 1.5S8 8.33 8 7.5S8.67 6 9.5 6s1.5.67 1.5 1.5z"/></svg>

After

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

23
app/public/img/antvue.svg Normal file
View File

@@ -0,0 +1,23 @@
<svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient x1="69.644%" y1="0%" x2="69.644%" y2="100%" id="a">
<stop stop-color="#29CDFF" offset="0%"/>
<stop stop-color="#148EFF" offset="37.86%"/>
<stop stop-color="#0A60FF" offset="100%"/>
</linearGradient>
<linearGradient x1="-19.819%" y1="-36.793%" x2="138.579%" y2="157.638%" id="b">
<stop stop-color="#29CDFF" offset="0%"/>
<stop stop-color="#0F78FF" offset="100%"/>
</linearGradient>
<linearGradient x1="68.128%" y1="-35.691%" x2="30.44%" y2="114.943%" id="c">
<stop stop-color="#FA8E7D" offset="0%"/>
<stop stop-color="#F74A5C" offset="51.264%"/>
<stop stop-color="#F51D2C" offset="100%"/>
</linearGradient>
</defs>
<g fill="none" fill-rule="evenodd">
<path d="M89.96 90.48c-11.38 3-21.63-7.12-22.34-8L46.66 62.23a6 6 0 0 1-1.83-4.267l-.326-40.442a4 4 0 0 0-1.224-2.848L29.646 1.532a4 4 0 0 0-6.776 2.88v59.974a10 10 0 0 0 3.015 7.157l37.715 36.81a6 6 0 0 0 8.421-.04" fill="url(#a)" fill-rule="nonzero" transform="matrix(-1 0 0 1 131.83 9)"/>
<path d="M68 90.116c-11.38 3-22.54-6.756-23.25-7.636L23.79 62.23a6 6 0 0 1-1.83-4.267l-.326-40.442a4 4 0 0 0-1.224-2.848L6.776 1.532A4 4 0 0 0 0 4.412v59.974a10 10 0 0 0 3.015 7.157l37.766 36.858a6 6 0 0 0 8.37.012" fill="url(#b)" fill-rule="nonzero" transform="translate(19 9)"/>
<path d="M43.298 19.1 27.557 3.881a2 2 0 0 1 1.39-3.438h31.446a2 2 0 0 1 1.391 3.437L46.08 19.098a2 2 0 0 1-2.782.001z" fill="url(#c)" transform="translate(19 9)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

11
app/public/img/arkui.svg Normal file
View File

@@ -0,0 +1,11 @@
<svg viewBox="0 0 63 24" xmlns="http://www.w3.org/2000/svg" class="h_7 fill_none">
<title>Ark Logo</title>
<g clip-path="url(#clip0_901_179)">
<path d="M9.07455 11.5775C9.21266 11.1733 9.35082 10.7689 9.48904 10.3643C9.64944 9.8968 9.80558 9.44041 9.96157 8.98444L9.97953 8.93194C10.0327 8.77661 10.0858 8.62127 10.1388 8.46593C10.3495 7.84944 10.5601 7.23296 10.7744 6.6177C10.7763 6.61225 10.7782 6.60648 10.7802 6.60048C10.8003 6.54059 10.8278 6.45874 10.8969 6.45874C10.9642 6.45874 10.9919 6.53319 11.0132 6.59071C11.016 6.59812 11.0186 6.60525 11.0212 6.6119C11.084 6.77175 11.1405 6.93408 11.197 7.09641C11.2174 7.15501 11.2378 7.2136 11.2585 7.27208C11.418 7.72322 11.5776 8.17435 11.7372 8.62548C12.2869 10.1792 12.8365 11.7329 13.3858 13.2867C13.511 13.641 13.4621 13.707 13.0802 13.707C12.8417 13.707 12.6032 13.707 12.3647 13.707C11.3032 13.707 10.2418 13.707 9.18038 13.7077C9.13956 13.7078 9.09874 13.7121 9.05372 13.717C9.03136 13.7194 9.00796 13.7219 8.98301 13.7241L12.6148 24L12.6531 23.9938V19.5735H21.8947C21.887 19.5488 21.8805 19.5271 21.8745 19.5073C21.8632 19.4702 21.8541 19.4401 21.8437 19.4104C21.5622 18.6035 21.2807 17.7967 20.9991 16.9898C19.5087 12.7185 18.0183 8.44713 16.5307 4.17479C16.4829 4.0375 16.4169 3.99982 16.274 4C13.2227 4.00385 10.1713 4.00375 7.11986 4.00364C6.5828 4.00362 6.04573 4.0036 5.50866 4.00361L5.30127 4.00361L0 19.716L0.215452 19.716C0.631264 19.716 1.04708 19.7159 1.46289 19.7158C2.99697 19.7153 4.53106 19.7149 6.06512 19.7204C6.24167 19.7211 6.31584 19.6699 6.3727 19.5023C7.26885 16.8622 8.17034 14.2238 9.07455 11.5775ZM33.914 4C35.7783 4 37.5489 4.26803 38.8634 5.01851C40.2032 5.78342 41.0475 7.03904 41.0475 8.926C41.0475 10.1194 40.5583 11.1908 39.6868 11.9351C39.3349 12.2356 39.3611 12.9687 39.7035 13.28C39.7574 13.329 39.8092 13.3801 39.8587 13.4334C40.3694 13.9826 40.6094 14.7223 40.6094 15.6382V17.4035C40.6094 18.0283 40.614 18.5945 40.8783 19.1377C40.9775 19.3416 40.829 19.579 40.6023 19.579H35.2961C35.1816 19.579 35.0765 19.5156 35.023 19.4144C34.8005 18.9935 34.7109 18.3312 34.7109 17.4446V16.2746C34.7109 15.7898 34.6036 15.4616 34.3689 15.2452C34.1275 15.0226 33.6866 14.8578 32.8765 14.8578H29.7671C29.491 14.8578 29.2671 15.0817 29.2671 15.3578V19.079C29.2671 19.3551 29.0432 19.579 28.7671 19.579H23.8685C23.5924 19.579 23.3685 19.3551 23.3685 19.079V4.5C23.3685 4.22386 23.5924 4 23.8685 4H33.914ZM32.8304 10.9175C33.8079 10.9175 34.3605 10.7402 34.6671 10.4935C34.957 10.2602 35.0797 9.92042 35.0797 9.43917C35.0797 9.16287 35.0237 8.95713 34.9342 8.80069C34.8454 8.64532 34.7132 8.52021 34.5309 8.42032C34.1532 8.2134 33.5802 8.12503 32.8304 8.12503H29.7671C29.491 8.12503 29.2671 8.34889 29.2671 8.62503V10.4175C29.2671 10.6937 29.491 10.9175 29.7671 10.9175H32.8304ZM55.0708 4.13937C55.1639 4.04994 55.288 4 55.4171 4H61.6326C62.0938 4 62.3091 4.57119 61.9627 4.8756L55.9164 10.1885C55.7103 10.3697 55.6886 10.6831 55.868 10.8909L62.6548 18.7522C62.9345 19.0762 62.7043 19.579 62.2763 19.579H56.1211C55.9729 19.579 55.8323 19.5132 55.7373 19.3995L51.6631 14.5209C51.4793 14.3007 51.1483 14.2801 50.9385 14.4756L49.3549 15.9515C49.2534 16.0461 49.1958 16.1785 49.1958 16.3173V19.079C49.1958 19.3551 48.9719 19.579 48.6958 19.579H43.7972C43.5211 19.579 43.2972 19.3551 43.2972 19.079V4.5C43.2972 4.22386 43.5211 4 43.7972 4H48.6958C48.9719 4 49.1958 4.22386 49.1958 4.5V8.60801C49.1958 9.0486 49.7243 9.27382 50.0421 8.96863L55.0708 4.13937Z" class="fill-rule_evenodd clip-rule_evenodd fill_accent.default" fill="#eb5e41"></path>
</g>
<defs>
<clipPath id="clip0_901_179">
<rect width="62.7773" height="24" fill="white"></rect>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,6 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="256" y="670.72" width="512" height="256" rx="128" fill="#1AD1A5"/>
<circle cx="512" cy="353.28" r="256" fill="white"/>
<circle cx="512" cy="353.28" r="261" stroke="black" stroke-opacity="0.2" stroke-width="10"/>
<circle cx="512" cy="353.28" r="114.688" fill="#FF9903"/>
</svg>

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -0,0 +1 @@
<svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44"><defs><style>.cls-1{fill:#409eff;fill-rule:evenodd;}</style></defs><title>element plus-logo-small 副本</title><path id="element_plus-logo-small" data-name="element plus-logo-small" class="cls-1" d="M37.41,32.37c0,1.57-.83,1.93-.83,1.93L21.51,43A1.69,1.69,0,0,1,20,43S5.2,34.4,4.66,34a1.29,1.29,0,0,1-.55-1V15.24c0-.78,1-1.33,1-1.33L19.86,5.36a2,2,0,0,1,1.79,0l14.46,8.41a2.06,2.06,0,0,1,1.25,2.06V32.37Zm-5.9-17L21.35,9.5a1.59,1.59,0,0,0-1.41,0L8.33,16.15s-.77.46-.76,1.08,0,13.92,0,13.92A1,1,0,0,0,8,31.9c.43.3,12,7,12,7a1.31,1.31,0,0,0,1.19,0C21.91,38.5,33,32.11,33,32.11s.65-.28.65-1.51V27.13l-13,7.9V32a3.05,3.05,0,0,1,1-2.07L33.2,23a2.44,2.44,0,0,0,.55-1.46V18.43L20.64,26.35v-3.2a2.22,2.22,0,0,1,.83-1.79ZM41.07,4.22a.39.39,0,0,0-.37-.42H38V1.06c0-.16-.26-.22-.53-.22L36,1.08c-.18,0-.31.12-.31.23V3.8H33a.4.4,0,0,0-.36.37v2h3V9c0,.16.26.27.54.23l1.51-.25c.18,0,.29-.13.29-.23V6.14h3Z"/></svg>

After

Width:  |  Height:  |  Size: 996 B

View File

@@ -0,0 +1,49 @@
<svg width="240" height="240" viewBox="0 0 140 140" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M107.273 56.6245C106.949 58.8577 105.476 61.8916 103.787 64.5294C101.655 67.8577 98.2729 70.1889 94.4018 70.9984L77.3494 74.5642C74.6087 75.1373 72.1235 76.5717 70.2575 78.6575L59.0764 91.1554C56.9751 93.5043 55.5064 92.9441 55.5064 89.7938C55.4809 89.9107 49.9572 104.156 64.5958 112.599C70.2204 115.844 78.3169 114.678 83.9414 111.434L113.743 94.2437C124.889 87.8151 132.758 76.9397 135.373 64.3507C135.477 63.8527 135.557 63.353 135.643 62.8535L107.273 56.6245Z" fill="url(#paint0_linear_3014_5022)"/>
<path d="M99.5443 39.4536C105.169 42.6979 107.467 47.5279 107.467 54.0164C107.467 54.8972 107.398 55.7675 107.273 56.6245L119.281 61.7827L135.643 62.8535C137.738 50.7282 133.633 38.2474 126.272 28.3267C120.733 20.861 113.605 14.3822 105.036 9.43957C98.0753 5.42454 90.8417 2.77918 83.5741 1.32764L75.4085 11.902L72.8291 24.0442L99.5443 39.4536Z" fill="url(#paint1_linear_3014_5022)"/>
<path d="M3.42839 48.3516C3.42533 48.3612 3.43379 48.364 3.43698 48.3544C4.06708 46.4645 4.85719 44.3561 5.84357 42.1036C10.996 30.3376 20.9252 22.4902 33.1404 18.4891C45.3556 14.4881 58.693 15.8905 69.8258 22.312L72.8287 24.044L83.5737 1.32743C49.3981 -5.49834 14.4695 14.5798 3.47184 48.22C3.46658 48.2361 3.44747 48.2922 3.42839 48.3516Z" fill="url(#paint2_linear_3014_5022)"/>
<path d="M82.7739 111.433C77.1493 114.678 70.2195 114.678 64.595 111.433C63.8315 110.993 63.1117 110.498 62.431 109.962L52.5391 116.763L44.0132 130.54C53.4765 138.415 65.7649 140.947 78.0449 139.539C87.2861 138.479 96.4661 135.552 105.035 130.609C111.996 126.594 117.906 121.658 122.798 116.096L117.714 103.744L109.489 96.0239L82.7739 111.433Z" fill="url(#paint3_linear_3014_5022)"/>
<path d="M62.4312 109.961C58.0977 106.548 55.5058 101.312 55.5058 95.7048V95.1455V48.4415C55.5058 45.8003 56.284 45.3514 58.5735 46.672C55.0472 44.638 46.9007 37.6553 36.8421 43.4572C31.2176 46.7015 26.5859 53.8628 26.5859 60.3513V94.731C26.5859 107.588 33.2454 121 42.8503 129.558C43.2303 129.896 43.6236 130.215 44.0133 130.54L62.4312 109.961Z" fill="url(#paint4_linear_3014_5022)"/>
<path d="M122.108 23.2417C122.101 23.2343 122.095 23.2402 122.101 23.2477C123.424 24.7379 124.857 26.4757 126.316 28.4553C133.94 38.7963 136.788 51.8936 134.149 64.4628C131.509 77.032 123.625 87.8703 112.492 94.2918L109.489 96.0239L122.798 116.096C145.803 89.9399 145.862 49.6804 122.2 23.3451C122.189 23.3325 122.15 23.2879 122.108 23.2417Z" fill="url(#paint5_linear_3014_5022)"/>
<path d="M27.753 60.3516C27.753 53.863 31.2179 47.8674 36.8424 44.6231C37.6059 44.1827 38.395 43.8076 39.2001 43.4866L38.2508 31.5276L31.5814 17.8452C20.0231 22.0952 10.6737 30.8785 5.75434 42.2073C2.0523 50.7327 4.284e-05 60.139 0 70.0242C0 78.0543 1.32365 85.6355 3.6992 92.6493L16.9484 94.427L27.753 91.1704V60.3516V60.3516Z" fill="url(#paint6_linear_3014_5022)"/>
<path d="M39.2002 43.4865C44.3259 41.4439 50.1603 41.8191 55.0213 44.623L55.5062 44.9026L94.4234 67.3503C97.1535 68.925 96.9054 70.4745 93.8198 71.1196L96.0648 70.6502C99.019 70.0325 101.718 68.5228 103.782 66.3228C107.33 62.5402 108.633 57.9765 108.633 54.016C108.633 47.5275 105.168 41.5319 99.5439 38.2877L69.7419 21.0978C58.5966 14.6691 45.2347 13.2983 33.0142 17.3298C32.5308 17.4892 32.0576 17.67 31.5815 17.8451L39.2002 43.4865Z" fill="url(#paint7_linear_3014_5022)"/>
<path d="M84.5346 138.479C84.5444 138.477 84.5426 138.468 84.5327 138.47C82.5794 138.87 80.3567 139.241 77.9109 139.514C65.1353 140.939 52.3579 136.855 42.7819 128.286C33.206 119.718 27.753 107.477 27.753 94.6346L27.753 91.1704L3.69922 92.6492C14.8701 125.631 49.7391 145.812 84.3987 138.507C84.4153 138.504 84.4735 138.492 84.5346 138.479Z" fill="url(#paint8_linear_3014_5022)"/>
<defs>
<linearGradient id="paint0_linear_3014_5022" x1="87.7629" y1="103.994" x2="101.533" y2="56.03" gradientUnits="userSpaceOnUse">
<stop stop-color="#049253"/>
<stop offset="1" stop-color="#40B983"/>
</linearGradient>
<linearGradient id="paint1_linear_3014_5022" x1="119.479" y1="45.5016" x2="83.2555" y2="9.40562" gradientUnits="userSpaceOnUse">
<stop stop-color="#40B983"/>
<stop offset="1" stop-color="#40B983"/>
</linearGradient>
<linearGradient id="paint2_linear_3014_5022" x1="70.7197" y1="21.9675" x2="16.0753" y2="25.4205" gradientUnits="userSpaceOnUse">
<stop stop-color="#40B983"/>
<stop offset="1" stop-color="#40B983"/>
</linearGradient>
<linearGradient id="paint3_linear_3014_5022" x1="67.0235" y1="125.399" x2="116.097" y2="112.157" gradientUnits="userSpaceOnUse">
<stop stop-color="#40B983"/>
<stop offset="1" stop-color="#40B983"/>
</linearGradient>
<linearGradient id="paint4_linear_3014_5022" x1="31.8005" y1="68.6116" x2="66.6415" y2="104.714" gradientUnits="userSpaceOnUse">
<stop stop-color="#049253"/>
<stop offset="1" stop-color="#40B983"/>
</linearGradient>
<linearGradient id="paint5_linear_3014_5022" x1="111.345" y1="94.656" x2="135.655" y2="45.6511" gradientUnits="userSpaceOnUse">
<stop stop-color="#40B983"/>
<stop offset="1" stop-color="#40B983"/>
</linearGradient>
<linearGradient id="paint6_linear_3014_5022" x1="23.4669" y1="39.948" x2="10.4586" y2="88.8411" gradientUnits="userSpaceOnUse">
<stop stop-color="#19A466"/>
<stop offset="1" stop-color="#50BD8C"/>
</linearGradient>
<linearGradient id="paint7_linear_3014_5022" x1="89.876" y1="37.6223" x2="42.3399" y2="49.4578" gradientUnits="userSpaceOnUse">
<stop stop-color="#049252"/>
<stop offset="1" stop-color="#19A466"/>
</linearGradient>
<linearGradient id="paint8_linear_3014_5022" x1="28.0295" y1="93.4354" x2="58.3298" y2="139.014" gradientUnits="userSpaceOnUse">
<stop stop-color="#4FBD8C"/>
<stop offset="1" stop-color="#40B983" stop-opacity="0.6"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -0,0 +1,50 @@
<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.5194 13.4429C24.4453 13.9533 24.1087 14.6468 23.7226 15.2497C23.2354 16.0105 22.4622 16.5433 21.5774 16.7283L17.6797 17.5434C17.0533 17.6744 16.4852 18.0022 16.0587 18.479L13.503 21.3357C13.0227 21.8725 12.687 21.7445 12.687 21.0244C12.6812 21.0511 11.4186 24.3072 14.7646 26.2371C16.0502 26.9787 17.9009 26.7122 19.1865 25.9707L25.9983 22.0416C28.5458 20.5722 30.3445 18.0863 30.9424 15.2089C30.966 15.095 30.9843 14.9808 31.004 14.8667L24.5194 13.4429Z" fill="url(#paint0_linear_2484_3243)"/>
<path d="M22.7528 9.51774C24.0384 10.2593 24.5637 11.3633 24.5637 12.8464C24.5637 13.0477 24.5479 13.2466 24.5194 13.4425L27.2641 14.6215L31.004 14.8663C31.4829 12.0948 30.5444 9.24202 28.862 6.97445C27.5959 5.268 25.9667 3.78714 24.0081 2.65738C22.417 1.73966 20.7636 1.13501 19.1025 0.803223L17.2361 3.22023L16.6465 5.99559L22.7528 9.51774Z" fill="url(#paint1_linear_2484_3243)"/>
<path d="M0.783597 11.5518C0.782899 11.554 0.784832 11.5546 0.78556 11.5524C0.929583 11.1205 1.11018 10.6385 1.33564 10.1237C2.51334 7.4343 4.78286 5.64062 7.57492 4.72608C10.367 3.81156 13.4155 4.13212 15.9601 5.59988L16.6465 5.99578L19.1025 0.803412C11.291 -0.756765 3.30728 3.83253 0.793528 11.5217C0.792327 11.5254 0.787957 11.5382 0.783597 11.5518Z" fill="url(#paint2_linear_2484_3243)"/>
<path d="M18.9199 25.9704C17.6343 26.712 16.0503 26.712 14.7647 25.9704C14.5902 25.8697 14.4257 25.7566 14.2701 25.634L12.0091 27.1885L10.0603 30.3376C12.2233 32.1377 15.0321 32.7164 17.839 32.3945C19.9513 32.1523 22.0495 31.4832 24.0082 30.3534C25.5992 29.4357 26.9501 28.3075 28.0682 27.0361L26.9063 24.2128L25.0262 22.4482L18.9199 25.9704Z" fill="url(#paint3_linear_2484_3243)"/>
<path d="M14.2701 25.6341C13.2796 24.8539 12.6872 23.6572 12.6872 22.3754V22.2476V11.5724C12.6872 10.9687 12.865 10.8661 13.3884 11.168C12.5823 10.703 10.7203 9.10701 8.42118 10.4331C7.13557 11.1747 6.0769 12.8116 6.0769 14.2946V22.1529C6.0769 25.0917 7.59906 28.1573 9.79448 30.1133C9.88132 30.1906 9.97122 30.2636 10.0603 30.3377L14.2701 25.6341Z" fill="url(#paint4_linear_2484_3243)"/>
<path d="M27.9105 5.8123C27.909 5.8106 27.9075 5.81197 27.909 5.81368C28.2114 6.15428 28.5389 6.5515 28.8725 7.00399C30.6149 9.36765 31.2659 12.3613 30.6627 15.2343C30.0594 18.1072 28.2573 20.5846 25.7126 22.0523L25.0262 22.4482L28.0683 27.0361C33.3265 21.0576 33.3401 11.8554 27.9316 5.83594C27.9291 5.83306 27.9201 5.82287 27.9105 5.8123Z" fill="url(#paint5_linear_2484_3243)"/>
<path d="M6.34355 14.2944C6.34354 12.8113 7.13552 11.4408 8.42113 10.6993C8.59565 10.5986 8.77601 10.5129 8.96002 10.4395L8.74304 7.70603L7.21862 4.57861C4.57671 5.55005 2.4397 7.55766 1.31528 10.1471C0.469097 12.0957 9.792e-06 14.2458 0 16.5052C0 18.3407 0.302549 20.0735 0.845533 21.6767L3.87391 22.083L6.34355 21.3387V14.2944V14.2944Z" fill="url(#paint6_linear_2484_3243)"/>
<path d="M8.96003 10.4395C10.1316 9.97264 11.4652 10.0584 12.5763 10.6993L12.6871 10.7632L21.5825 15.8941C22.2065 16.254 22.1498 16.6082 21.4445 16.7557L21.9577 16.6484C22.6329 16.5072 23.2498 16.1621 23.7216 15.6592C24.5327 14.7946 24.8305 13.7515 24.8305 12.8463C24.8304 11.3632 24.0385 9.99274 22.7529 9.2512L15.941 5.32209C13.3935 3.85267 10.3394 3.53934 7.5461 4.46083C7.4356 4.49727 7.32744 4.5386 7.21863 4.57861L8.96003 10.4395Z" fill="url(#paint7_linear_2484_3243)"/>
<path d="M19.3222 32.1523C19.3245 32.1518 19.3241 32.1498 19.3218 32.1503C18.8753 32.2417 18.3673 32.3264 17.8083 32.3888C14.8881 32.7145 11.9676 31.781 9.77876 29.8225C7.58999 27.8641 6.3436 25.0662 6.3436 22.1307L6.34359 21.3389L0.845581 21.6769C3.39893 29.2156 11.369 33.8285 19.2912 32.1588C19.295 32.158 19.3083 32.1553 19.3222 32.1523Z" fill="url(#paint8_linear_2484_3243)"/>
<defs>
<linearGradient id="paint0_linear_2484_3243" x1="20.0599" y1="24.2701" x2="23.2075" y2="13.307" gradientUnits="userSpaceOnUse">
<stop stop-color="#1724C9"/>
<stop offset="1" stop-color="#1C64F2"/>
</linearGradient>
<linearGradient id="paint1_linear_2484_3243" x1="27.3093" y1="10.9001" x2="19.0297" y2="2.64962" gradientUnits="userSpaceOnUse">
<stop stop-color="#1C64F2"/>
<stop offset="1" stop-color="#0092FF"/>
</linearGradient>
<linearGradient id="paint2_linear_2484_3243" x1="16.1645" y1="5.52115" x2="3.67432" y2="6.3104" gradientUnits="userSpaceOnUse">
<stop stop-color="#0092FF"/>
<stop offset="1" stop-color="#45B2FF"/>
</linearGradient>
<linearGradient id="paint3_linear_2484_3243" x1="15.3198" y1="29.1626" x2="26.5366" y2="26.1359" gradientUnits="userSpaceOnUse">
<stop stop-color="#1C64F2"/>
<stop offset="1" stop-color="#0092FF"/>
</linearGradient>
<linearGradient id="paint4_linear_2484_3243" x1="7.26881" y1="16.1827" x2="15.2325" y2="24.4347" gradientUnits="userSpaceOnUse">
<stop stop-color="#1724C9"/>
<stop offset="1" stop-color="#1C64F2"/>
</linearGradient>
<linearGradient id="paint5_linear_2484_3243" x1="25.4505" y1="22.1356" x2="31.007" y2="10.9345" gradientUnits="userSpaceOnUse">
<stop stop-color="#0092FF"/>
<stop offset="1" stop-color="#45B2FF"/>
</linearGradient>
<linearGradient id="paint6_linear_2484_3243" x1="5.36387" y1="9.63067" x2="2.39054" y2="20.8063" gradientUnits="userSpaceOnUse">
<stop stop-color="#1C64F2"/>
<stop offset="1" stop-color="#0092FF"/>
</linearGradient>
<linearGradient id="paint7_linear_2484_3243" x1="20.5431" y1="9.09912" x2="9.67768" y2="11.8044" gradientUnits="userSpaceOnUse">
<stop stop-color="#1724C9"/>
<stop offset="1" stop-color="#1C64F2"/>
</linearGradient>
<linearGradient id="paint8_linear_2484_3243" x1="6.40679" y1="21.8566" x2="13.3326" y2="32.2745" gradientUnits="userSpaceOnUse">
<stop stop-color="#0092FF"/>
<stop offset="1" stop-color="#45B2FF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="256px" height="256px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<title>Headless UI</title>
<defs>
<linearGradient x1="35.8706002%" y1="7.22395026%" x2="60.3143692%" y2="115.987925%" id="linearGradient-1">
<stop stop-color="#66E3FF" offset="0%"></stop>
<stop stop-color="#7064F9" offset="100%"></stop>
</linearGradient>
<linearGradient x1="39.1716101%" y1="-0.000258114803%" x2="55.0500969%" y2="99.1811839%" id="linearGradient-2">
<stop stop-color="#66E3FF" offset="0%"></stop>
<stop stop-color="#7064F9" offset="100%"></stop>
</linearGradient>
</defs>
<g>
<path d="M50.6986759,159.08871 L206.923729,106.936393 C203.80243,87.4132795 201.577503,74.3711999 199.00043,64.4176124 C196.223274,53.7119051 194.022357,50.8474482 193.157997,49.8552901 C190.14581,46.4084468 186.395385,43.6845493 182.185428,41.8860191 C180.968922,41.3659363 177.559502,40.1577436 166.53091,40.8218494 C154.86205,41.5259618 139.543672,43.9103419 115.28557,47.7509546 C91.0354724,51.591567 75.7250972,54.0639613 64.4163882,56.9924283 C53.7159324,59.7688711 50.8427359,61.9772231 49.8503228,62.8413611 C46.4088895,65.857842 43.6877564,69.6024394 41.8870066,73.8111103 C41.3667899,75.0273045 40.1582869,78.4358481 40.8225634,89.4696076 C41.5268568,101.135468 43.9118497,116.457912 47.7534494,140.709779 C48.8258961,147.510864 49.7942992,153.591833 50.6986759,159.08871 L50.6986759,159.08871 Z" id="Path" fill="url(#linearGradient-1)"></path>
<path d="M7.62473955,147.062792 C0.197646837,100.14331 -3.52390284,76.6835685 4.52744981,57.8485649 C8.72454124,48.0252484 15.0819292,39.2737796 23.1271949,32.2444815 C38.549617,18.778334 62.0073849,15.065742 108.922921,7.63255653 C155.854464,0.191369851 179.312232,-3.52122222 198.16008,4.52806145 C207.980567,8.72542915 216.728782,15.0811604 223.754738,23.1230269 C237.23235,38.5494871 240.945896,62.0092284 248.372989,108.936712 C255.808085,155.856195 259.521632,179.323937 251.470279,198.15094 C247.275985,207.973747 240.921308,216.72525 232.878537,223.755023 C217.456115,237.229172 193.990344,240.941764 147.066804,248.374949 C100.151269,255.808135 76.6854971,259.520727 57.8536554,251.471443 C48.0333048,247.273835 39.2851393,240.91814 32.2589976,232.876477 C18.7813853,217.450017 15.0678389,193.990276 7.63274287,147.062792 L7.62473955,147.062792 Z M90.3872026,230.388081 C102.952435,229.62796 119.135174,227.091555 143.089148,223.29895 C167.051126,219.506345 183.209855,216.913932 195.406934,213.753428 C207.211849,210.688939 212.654116,207.704463 216.167578,204.631973 C221.336653,200.112766 225.420781,194.487252 228.116554,188.173349 C229.957321,183.884665 231.117804,177.787692 230.381497,165.60975 C229.621181,153.047747 227.084124,136.861165 223.290544,112.905345 C219.496965,88.9495246 216.911889,72.7789457 213.750572,60.5930026 C210.685296,48.7831189 207.692049,43.3422514 204.626773,39.8296913 C200.109733,34.6598101 194.485483,30.5739786 188.171921,27.8757849 C183.882135,26.0354914 177.783595,24.8753064 165.602523,25.6114238 C153.037291,26.371545 136.862555,28.9079495 112.900577,32.7005543 C88.9466028,36.5011605 72.7798707,39.0855725 60.590795,42.2460764 C48.7778761,45.3105653 43.3436133,48.3030424 39.8301503,51.3675312 C34.6583762,55.8860779 30.5715011,61.5116077 27.8731712,67.8261559 C26.040408,72.1148399 24.8799248,78.2118123 25.6162313,90.3897542 C26.376548,102.959759 28.9056012,119.138339 32.707184,143.094159 C36.5007639,167.04998 39.0858402,183.220558 42.2471565,195.406502 C45.3124331,207.216385 48.2976761,212.657253 51.3709559,216.169813 C55.8881822,221.33949 61.5123817,225.425286 67.8258079,228.12372 C72.1155942,229.964013 78.2061302,231.124198 90.3952059,230.388081 L90.3872026,230.388081 Z" id="Shape" fill="url(#linearGradient-2)"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 124 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 207.74 252.58"><defs><style>.cls-1{fill:#93ceaa;}.cls-2{fill:#4c9717;}.cls-3{fill:#5fbc21;}.cls-4{fill:#e8ceaa;opacity:0.6;}</style></defs><title>Naive UI - LOGO</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M60.37,205.2c0,2.5,0,5,.05,7.5,0,.54,0,1.07-.05,1.58Z"/><path class="cls-1" d="M147.29,37.68v85.2c-.06-27.24-.19-54.49-.22-81.73A29,29,0,0,1,147.29,37.68Z"/><path class="cls-2" d="M147.06,125.43a16.9,16.9,0,0,0,.23-2.55v79.66l-41.74-38.17-21-19.15L71.91,133.6l-5.62-5.13-2.07-1.89-.15-.13c-.51-.41-1-.84-1.47-1.23s-.88-.73-1.44-.56a1.25,1.25,0,0,0-.79.9v-75l.43.39,8.57,7.75c4.51,4,9.06,8,13.56,12Q92.53,79.3,102.07,88q7.55,6.86,15.08,13.74c6,5.47,11.92,11,17.92,16.43,2.7,2.44,5.42,4.83,8.15,7.23.58.51,1.15,1,1.74,1.52a1.47,1.47,0,0,0,1.4.18,1.16,1.16,0,0,0,.56-.72C147,126,147,125.74,147.06,125.43Z"/><path class="cls-3" d="M60.28,126a15.67,15.67,0,0,0-.1,2.74c0,25.5,0,51,.19,76.49v9.08a14.85,14.85,0,0,1-5.87,11.09c-6.81,5.69-13.16,11.91-19.73,17.89-2.75,2.5-5.48,5-8.31,7.44s-10.11,2.74-14.14-.63a78.48,78.48,0,0,1-8.54-8.66,12.76,12.76,0,0,1-3.73-8.69c0-.72-.06-1.51,0-2.38,0-1.5,0-3,.05-4.51.05-4.3,0-8.61,0-12.91V72.71a12.64,12.64,0,0,1,.38-3.18,18.42,18.42,0,0,1,1-2.64A22.16,22.16,0,0,1,3.5,63.31l16-16L29.69,37.13a7.84,7.84,0,0,1,1.84-1.38,8.05,8.05,0,0,1,3.21-1h0a13.85,13.85,0,0,1,9.71,1.79h0a13.59,13.59,0,0,1,2.09,1.57L60.37,50.53v75A3.38,3.38,0,0,0,60.28,126Z"/><path class="cls-4" d="M60.28,126a3.38,3.38,0,0,1,.09-.41V205.2c-.15-25.5-.14-51-.19-76.49A15.67,15.67,0,0,1,60.28,126Z"/><path class="cls-3" d="M205.59,187.21l-29.39,27a3.75,3.75,0,0,1-.36.24l-.11.08-.2.12A14,14,0,0,1,158.77,213L157,211.45l-9.75-8.91V37.68c.16-1.39.4-2.78.67-4.14.7-3.55,3.82-5.31,6.2-7.53,5.17-4.8,10.5-9.42,15.77-14.12,3.63-3.24,7.25-6.49,10.91-9.71,3.19-2.81,9.37-2.67,12.37-.93a10.11,10.11,0,0,1,2.08,1.61l3.38,3.26,2.73,2.61,2.49,2.41a11.45,11.45,0,0,1,1.78,2.09,11.73,11.73,0,0,1,1.7,6.46c-.05,14.66.06,29.32.09,44q.09,37.35.16,74.7,0,19.14.07,38.27c0,.59,0,1.17,0,1.75C207.8,182.21,207.72,184.72,205.59,187.21Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

3
app/public/img/nuxt.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="900" height="900" viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M504.908 750H839.476C850.103 750.001 860.542 747.229 869.745 741.963C878.948 736.696 886.589 729.121 891.9 719.999C897.211 710.876 900.005 700.529 900 689.997C899.995 679.465 897.193 669.12 891.873 660.002L667.187 274.289C661.876 265.169 654.237 257.595 645.036 252.329C635.835 247.064 625.398 244.291 614.773 244.291C604.149 244.291 593.711 247.064 584.511 252.329C575.31 257.595 567.67 265.169 562.36 274.289L504.908 372.979L392.581 179.993C387.266 170.874 379.623 163.301 370.42 158.036C361.216 152.772 350.777 150 340.151 150C329.525 150 319.086 152.772 309.883 158.036C300.679 163.301 293.036 170.874 287.721 179.993L8.12649 660.002C2.80743 669.12 0.00462935 679.465 5.72978e-06 689.997C-0.00461789 700.529 2.78909 710.876 8.10015 719.999C13.4112 729.121 21.0523 736.696 30.255 741.963C39.4576 747.229 49.8973 750.001 60.524 750H270.538C353.748 750 415.112 713.775 457.336 643.101L559.849 467.145L614.757 372.979L779.547 655.834H559.849L504.908 750ZM267.114 655.737L120.551 655.704L340.249 278.586L449.87 467.145L376.474 593.175C348.433 639.03 316.577 655.737 267.114 655.737Z" fill="#00DC82"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#00DC82" d="M18 3.143c-9.941 0-18 6.908-18 15.428 0 1.066.126 2.107.367 3.112C2.146 24.744 3.377 22.812 9 20c5.727-2.864 0 4-2 8-.615 1.23-.282 2.271.56 3.124C10.506 32.928 14.104 34 18 34c9.941 0 18-6.907 18-15.429 0-8.52-8.059-15.428-18-15.428zm2.849 24.447c-.395 1.346-2.46 1.924-4.613 1.291-2.153-.632-3.578-2.234-3.183-3.581.395-1.346 2.46-1.924 4.613-1.29 2.153.631 3.578 2.233 3.183 3.58z"/><circle fill="#5C913B" cx="10" cy="11" r="3"/><circle fill="#269" cx="20" cy="9" r="3"/><circle fill="#DD2E44" cx="29" cy="15" r="3"/><circle fill="#FFCC4D" cx="28" cy="24" r="3"/></svg>

After

Width:  |  Height:  |  Size: 657 B

View File

@@ -0,0 +1,11 @@
<svg width="35" height="40" viewBox="0 0 35 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2517_369)">
<path d="M25.87 18.05L23.16 17.45L25.27 20.46V29.78L32.49 23.76V13.53L29.18 14.73L25.87 18.04V18.05ZM25.27 35.49L29.18 31.58V27.67L25.27 30.98V35.49ZM20.16 17.14H20.03H20.17H20.16ZM30.1 5.19L34.89 4.81L33.08 12.33L24.1 15.67L30.08 5.2L30.1 5.19ZM5.72 14.74L2.41 13.54V23.77L9.63 29.79V20.47L11.74 17.46L9.03 18.06L5.72 14.75V14.74ZM9.63 30.98L5.72 27.67V31.58L9.63 35.49V30.98ZM4.8 5.2L10.78 15.67L1.81 12.33L0 4.81L4.79 5.19L4.8 5.2ZM24.37 21.05V34.59L22.56 37.29L20.46 39.4H14.44L12.34 37.29L10.53 34.59V21.05L12.42 18.23L17.45 26.8L22.48 18.23L24.37 21.05ZM22.85 0L22.57 0.69L17.45 13.08L12.33 0.69L12.05 0H22.85Z" fill="#41B883"/>
<path d="M30.69 4.21L24.37 4.81L22.57 0.69L22.86 0H26.48L30.69 4.21ZM23.75 5.67L22.66 3.08L18.05 14.24V17.14H19.7H20.03H20.16H20.2L24.1 15.7L30.11 5.19L23.75 5.67ZM4.21002 4.21L10.53 4.81L12.33 0.69L12.05 0H8.43002L4.22002 4.21H4.21002ZM21.9 17.4L20.6 18.2H14.3L13 17.4L12.4 18.2L12.42 18.23L17.45 26.8L22.48 18.23L22.5 18.2L21.9 17.4ZM4.79002 5.19L10.8 15.7L14.7 17.14H14.74H15.2H16.85V14.24L12.24 3.09L11.15 5.68L4.79002 5.2V5.19Z" fill="#455C71"/>
</g>
<defs>
<clipPath id="clip0_2517_369">
<rect width="34.9" height="39.4" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

36
app/public/img/quasar.svg Normal file
View File

@@ -0,0 +1,36 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 1024 1024"
xml:space="preserve">
<style>
.st0 {
fill: #050a14
}
.st1 {
fill: #00b4ff
}
</style>
<path id="path8860-5-4-8-2-7-1-8_20_" d="M586.2 512a74.2 74.2 0 11-148.4 0 74.2 74.2 0 11148.4 0z" class="st0" />
<path id="path8862-5-5-9-1-9-2-7_20_"
d="M841.3 321.8a375.7 375.7 0 00-58.3-76.2l-85.8 49.5a286.7 286.7 0 00-89.5-51.9 352 352 0 00-69.3 98.9c95.5-6.5 194.1 28.1 285.6 99.6l54-31.2a377.5 377.5 0 00-36.7-88.7z"
class="st0" />
<path id="path8866-7-5-5-0-3-5-6_20_"
d="M512 892.2c32.1-.1 64.1-4.3 95.1-12.4v-99a285 285 0 0089.7-51.6 352.3 352.3 0 00-51-109.5c-42.1 85.9-121.3 154-229 197.5v62.3a369.4 369.4 0 0095.2 12.7z"
class="st0" />
<path id="path8870-5-3-9-3-0-0-9_20_"
d="M182.7 321.9a377.8 377.8 0 00-36.8 88.6l85.8 49.5a285.8 285.8 0 00-.2 103.5 352.7 352.7 0 00120.3 10.6C298.4 494.7 279.1 392 295.2 277l-54-31.2a376.9 376.9 0 00-58.5 76.1z"
class="st0" />
<g>
<path id="path8864-4-8-1-2-7-3-1_20_"
d="M841.3 702.1c16-27.8 28.3-57.6 36.8-88.6L792.4 564c6.4-34.6 6.4-69.6.2-103.5a352.7 352.7 0 00-120.3-10.6C725.7 529.3 745 632 728.9 747l54 31.2a382 382 0 0058.4-76.1z"
class="st1" />
<path id="path8868-6-7-4-7-5-7-1_20_"
d="M182.7 702.1a375.7 375.7 0 0058.3 76.2l85.8-49.5a286.7 286.7 0 0089.5 51.9 352 352 0 0069.3-98.9c-95.5 6.5-194.1-28.1-285.6-99.6l-54 31.2c8.5 31 20.8 60.8 36.7 88.7z"
class="st1" />
<path id="path8872-6-3-2-1-4-2-1_20_"
d="M512 131.7c-32.1.1-64.1 4.3-95.1 12.4v99a285 285 0 00-89.7 51.6 352.3 352.3 0 0051 109.5c42.1-85.9 121.3-154 229-197.5v-62.3a378.3 378.3 0 00-95.2-12.7z"
class="st1" />
</g>
<path
d="M512 66.4c245.7 0 445.6 199.9 445.6 445.6S757.7 957.6 512 957.6 66.4 757.7 66.4 512 266.3 66.4 512 66.4M512 1a511 511 0 100 1022A511 511 0 00512 1z"
class="st1" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,36 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 1024 1024"
xml:space="preserve">
<style>
.st0 {
fill: #fff
}
.st1 {
fill: #00b4ff
}
</style>
<path id="path8860-5-4-8-2-7-1-8_21_" d="M586.2 512a74.2 74.2 0 01-148.4 0 74.2 74.2 0 11148.4 0z" class="st0" />
<path id="path8862-5-5-9-1-9-2-7_21_"
d="M841.3 321.8a378.5 378.5 0 00-58.3-76.2l-85.7 49.5a286 286 0 00-89.6-51.9 348.6 348.6 0 00-69.3 98.9c95.5-6.5 194.1 28 285.6 99.6l53.9-31.1c-8.3-31-20.7-60.9-36.6-88.8z"
class="st0" />
<path id="path8866-7-5-5-0-3-5-6_21_"
d="M512 892.3c32.1-.1 64-4.2 95.2-12.4v-99a284 284 0 0089.7-51.6 352.3 352.3 0 00-51-109.5c-42.1 86-121.3 154-229 197.6v62.3a382 382 0 0095.1 12.6z"
class="st0" />
<path id="path8870-5-3-9-3-0-0-9_21_"
d="M182.7 321.9a373.6 373.6 0 00-36.8 88.6l85.7 49.5a285.2 285.2 0 00-.2 103.4A350 350 0 00351.7 574c-53.4-79.4-72.8-182.1-56.6-297.1l-53.9-31.1a376.9 376.9 0 00-58.5 76.1z"
class="st0" />
<g>
<path id="path8864-4-8-1-2-7-3-1_21_"
d="M841.3 702.1c16-27.8 28.4-57.6 36.8-88.6L792.5 564c6.4-34.5 6.4-69.6.2-103.4-40.3-10.6-80.6-14.1-120.3-10.6 53.4 79.4 72.8 182.1 56.6 297.1l53.9 31.1a375.6 375.6 0 0058.4-76.1z"
class="st1" />
<path id="path8868-6-7-4-7-5-7-1_21_"
d="M182.7 702.1a378.5 378.5 0 0058.3 76.2l85.7-49.5a286 286 0 0089.6 51.9 348.6 348.6 0 0069.3-98.9c-95.5 6.5-194.1-28-285.6-99.6l-54 31.1c8.4 31.1 20.8 61 36.7 88.8z"
class="st1" />
<path id="path8872-6-3-2-1-4-2-1_21_"
d="M512 131.7c-32.1.1-64 4.2-95.2 12.4v99a284 284 0 00-89.7 51.6c11 40.2 28.2 76.9 51 109.5 42.1-86 121.3-154 229-197.6v-62.3c-30.9-8.2-63-12.4-95.1-12.6z"
class="st1" />
</g>
<path
d="M512 66.4c245.7 0 445.6 199.9 445.6 445.6S757.7 957.6 512 957.6 66.4 757.7 66.4 512 266.3 66.4 512 66.4M512 1a511 511 0 100 1022A511 511 0 00512 1z"
class="st1" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,34 @@
<svg width="290" height="290" viewBox="0 0 290 290" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M226.269 52.4044L226.274 52.4067C237.406 58.2619 245.614 66.3008 250.94 76.5218C256.285 86.7776 258.969 98.4614 258.969 111.596C258.969 124.732 256.285 136.34 250.943 146.446C245.618 156.521 237.447 164.451 226.389 170.234C221.59 172.712 219.355 178.881 222.459 183.542L257.922 236.789C261.773 242.571 257.628 250.311 250.681 250.311H196.906C193.806 250.311 190.939 248.661 189.382 245.98L79.3991 56.5686C76.0313 50.7687 80.2159 43.5 86.9227 43.5H183.394C200.888 43.5 215.161 46.4896 226.269 52.4044Z" fill="url(#paint0_linear_30_25)" stroke="url(#paint1_linear_30_25)" stroke-width="1.93333"/>
<path d="M116.722 247.228C113.004 253.687 103.684 253.687 99.9661 247.228L26.2042 119.085C22.4947 112.64 27.1462 104.596 34.5821 104.596L182.106 104.596C189.542 104.596 194.193 112.64 190.484 119.085L116.722 247.228Z" fill="url(#paint2_radial_30_25)"/>
<path d="M116.722 247.228C113.004 253.687 103.684 253.687 99.9661 247.228L26.2042 119.085C22.4947 112.64 27.1462 104.596 34.5821 104.596L182.106 104.596C189.542 104.596 194.193 112.64 190.484 119.085L116.722 247.228Z" fill="url(#paint3_radial_30_25)" fill-opacity="0.5"/>
<path d="M100.804 246.745L27.042 118.602C23.7034 112.802 27.8898 105.562 34.5821 105.562L182.106 105.562C188.798 105.562 192.985 112.802 189.646 118.602L115.884 246.745C112.538 252.558 104.15 252.558 100.804 246.745Z" fill="url(#paint4_radial_30_25)" fill-opacity="0.5" stroke="url(#paint5_linear_30_25)" stroke-width="1.93333" stroke-linejoin="round"/>
<defs>
<linearGradient id="paint0_linear_30_25" x1="241.666" y1="264.867" x2="88.9331" y2="7.73334" gradientUnits="userSpaceOnUse">
<stop stop-color="#00996C"/>
<stop offset="1" stop-color="#7AFFD8"/>
</linearGradient>
<linearGradient id="paint1_linear_30_25" x1="192.366" y1="289.033" x2="233.933" y2="42.5333" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.56"/>
<stop offset="0.494792" stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="white"/>
</linearGradient>
<radialGradient id="paint2_radial_30_25" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(185.6 110.2) rotate(147.858) scale(129.006 128.619)">
<stop offset="0.48614" stop-color="#00C58A"/>
<stop offset="1" stop-color="white" stop-opacity="0.21"/>
</radialGradient>
<radialGradient id="paint3_radial_30_25" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(108.266 160.467) rotate(90) scale(191.4 190.827)">
<stop stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="white"/>
</radialGradient>
<radialGradient id="paint4_radial_30_25" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(108.266 181.733) rotate(-90) scale(129.533 129.145)">
<stop stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="white"/>
</radialGradient>
<linearGradient id="paint5_linear_30_25" x1="105.366" y1="105.367" x2="105.366" y2="261" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.6"/>
<stop offset="0.494792" stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="white" stop-opacity="0.38"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
app/public/img/shadcn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 54 33"><g clip-path="url(#prefix__clip0)"><path fill="#38bdf8" fill-rule="evenodd" d="M27 0c-7.2 0-11.7 3.6-13.5 10.8 2.7-3.6 5.85-4.95 9.45-4.05 2.054.513 3.522 2.004 5.147 3.653C30.744 13.09 33.808 16.2 40.5 16.2c7.2 0 11.7-3.6 13.5-10.8-2.7 3.6-5.85 4.95-9.45 4.05-2.054-.513-3.522-2.004-5.147-3.653C36.756 3.11 33.692 0 27 0zM13.5 16.2C6.3 16.2 1.8 19.8 0 27c2.7-3.6 5.85-4.95 9.45-4.05 2.054.514 3.522 2.004 5.147 3.653C17.244 29.29 20.308 32.4 27 32.4c7.2 0 11.7-3.6 13.5-10.8-2.7 3.6-5.85 4.95-9.45 4.05-2.054-.513-3.522-2.004-5.147-3.653C23.256 19.31 20.192 16.2 13.5 16.2z" clip-rule="evenodd"/></g><defs><clipPath id="prefix__clip0"><path fill="#fff" d="M0 0h54v32.4H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 772 B

3
app/public/img/volt.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 40" fill="#7c3aed">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.043 15.0334L8.03101 16.6142H10.7377L11.713 0.106103C11.7195 -0.00249147 11.5724 -0.0417968 11.5238 0.0555237L4.043 15.0334ZM2.55847 23.2515H0.100129C0.0258101 23.2515 -0.0225409 23.1733 0.0106666 23.1068L2.30586 18.5114L4.69286 17.9483L2.55847 21.1927V23.2515ZM17.4689 29.2273L10.9293 39.2178C10.8699 39.3085 10.7288 39.2531 10.747 39.1462L13.4481 23.2515H4.44864V21.8287H4.46887L6.90673 17.9375L8.42408 18.9076H13.5137L15.031 17.9375L17.4689 21.8287V29.2273ZM20.0244 17.8162H24.7534C24.8329 17.8162 24.8806 17.9044 24.8371 17.9709L19.3793 26.3088V21.1927L17.2449 17.9483L19.3793 18.595L20.0244 17.8162Z"/>
</svg>

After

Width:  |  Height:  |  Size: 743 B

2
app/public/img/vuejs.svg Normal file
View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 261.76 226.69" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(1.3333 0 0 -1.3333 -76.311 313.34)"><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-75.491l98.16-170.02 98.16 170.02z" fill="#41b883"/></g><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-36.227l58.896-102.01 58.896 102.01z" fill="#34495e"/></g></g></svg>

After

Width:  |  Height:  |  Size: 467 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -0,0 +1,15 @@
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#41D1FF"/>
<stop offset="1" stop-color="#BD34FE"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEA83"/>
<stop offset="0.0833333" stop-color="#FFDD35"/>
<stop offset="1" stop-color="#FFA800"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,5 @@
<svg width="260" height="260" viewBox="0 0 165 165" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M120.831 57.2543L84.693 109.505C84.3099 110.059 83.7558 110.474 83.1148 110.687C82.4738 110.9 81.7809 110.898 81.1412 110.684C80.5015 110.469 79.95 110.052 79.5702 109.497C79.1905 108.941 79.0032 108.277 79.037 107.606L80.4833 78.7582L57.1343 73.8064C56.6353 73.7007 56.1704 73.474 55.7807 73.1465C55.391 72.8191 55.0885 72.4009 54.9001 71.929C54.7117 71.4571 54.6432 70.9461 54.7006 70.4412C54.758 69.9364 54.9395 69.4532 55.2291 69.0345L91.3675 16.7837C91.7507 16.2294 92.3048 15.8145 92.9458 15.6018C93.5869 15.3891 94.2798 15.3902 94.9196 15.6051C95.5593 15.8199 96.1109 16.2367 96.4906 16.7923C96.8703 17.3478 97.0575 18.0117 97.0236 18.6833L95.5773 47.5314L118.926 52.4828C119.425 52.5885 119.89 52.8152 120.28 53.1426C120.67 53.4701 120.972 53.8883 121.16 54.3602C121.349 54.8321 121.417 55.3431 121.36 55.8479C121.303 56.3528 121.121 56.836 120.831 57.2547L120.831 57.2543Z" fill="#FCC72B"/>
<path d="M82.9866 153.343C82.0254 153.344 81.0735 153.156 80.1855 152.788C79.2975 152.42 78.4909 151.88 77.8122 151.2L43.6658 117.056C42.2998 115.683 41.5341 113.824 41.5366 111.887C41.5392 109.95 42.3098 108.092 43.6796 106.723C45.0493 105.353 46.9064 104.582 48.8435 104.579C50.7807 104.577 52.6399 105.342 54.0134 106.708L82.9866 135.678L146.105 72.5626C147.481 71.2088 149.336 70.4536 151.266 70.4615C153.197 70.4693 155.046 71.2396 156.41 72.6045C157.775 73.9695 158.546 75.8184 158.554 77.7487C158.561 79.679 157.806 81.5342 156.452 82.9101L88.1597 151.2C87.4811 151.881 86.6747 152.42 85.7869 152.788C84.8992 153.156 83.9475 153.344 82.9866 153.343Z" fill="#729B1B"/>
<path d="M82.9572 153.343C83.9184 153.344 84.8703 153.156 85.7583 152.788C86.6463 152.42 87.4528 151.88 88.1316 151.2L122.278 117.056C123.644 115.683 124.41 113.824 124.407 111.887C124.405 109.95 123.634 108.092 122.264 106.723C120.894 105.353 119.037 104.582 117.1 104.579C115.163 104.577 113.304 105.342 111.93 106.708L82.9572 135.678L19.8389 72.5626C18.4629 71.2088 16.6077 70.4536 14.6775 70.4615C12.7472 70.4693 10.8982 71.2396 9.53331 72.6045C8.16839 73.9695 7.39811 75.8184 7.39025 77.7487C7.38239 79.679 8.13759 81.5342 9.49135 82.9101L77.784 151.2C78.4627 151.881 79.2691 152.42 80.1568 152.788C81.0446 153.156 81.9963 153.344 82.9572 153.343Z" fill="#729B1B" fill-opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,139 @@
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.2008 20.3651C21.4407 20.0835 18.5038 20.1549 16.0126 21.5176C13.4134 22.9385 11.6334 25.7102 10.7017 28.4542C10.2196 29.8737 9.95734 31.376 9.93416 32.8755C9.90767 34.5649 10.2533 36.2113 10.6229 37.8517C10.9818 39.4478 11.3354 41.0664 11.2612 42.7127C11.2281 43.446 11.1076 44.2084 10.7758 44.8715C10.4593 45.5036 9.9037 45.8554 9.31963 46.2164C8.59583 46.664 7.97071 47.2221 7.2979 47.7398C6.60457 48.2734 5.88541 48.7752 5.14109 49.2347C3.6233 50.171 2.00089 50.9082 0.283116 51.3935C-0.213541 51.5337 -0.0022966 52.3079 0.496347 52.1671C3.54251 51.3062 6.24366 49.6652 8.67066 47.6644C9.2097 47.2201 9.81165 46.8934 10.3765 46.4875C10.9612 46.0676 11.3665 45.5691 11.6367 44.9C12.1923 43.5227 12.1353 41.9497 11.946 40.5044C11.7195 38.7773 11.2195 37.1032 10.9328 35.3867C10.6758 33.8508 10.6666 32.2785 10.9414 30.7439C11.4579 27.8551 12.9134 24.8784 15.2272 23.0026C17.7376 20.9675 21.1288 20.8551 24.2015 21.1678C24.7153 21.2201 24.7114 20.418 24.2015 20.3658L24.2008 20.3651Z" fill="#007CFF"/>
<path d="M11.0156 47.1818C11.9963 47.2837 12.8737 46.5722 12.975 45.593C13.077 44.6138 12.3645 43.7377 11.3837 43.6365C10.403 43.5347 9.52625 44.2461 9.42427 45.2254C9.32229 46.2046 10.0348 47.08 11.0156 47.1818Z" fill="#007CFF"/>
<path d="M11.15 46.385C11.252 50.5498 9.98052 54.6704 7.56213 58.0642C7.26149 58.4854 7.95813 58.8868 8.25547 58.4689C10.75 54.9692 12.0579 50.6781 11.9532 46.385C11.9407 45.8699 11.1374 45.8679 11.15 46.385Z" fill="#007CFF"/>
<path d="M10.8949 45.794C10.5353 50.0905 8.18848 54.1621 4.64963 56.6323C4.22913 56.9259 4.62976 57.6214 5.0549 57.3246C7.98981 55.2755 10.1619 52.2261 11.1539 48.7899C11.436 47.812 11.6134 46.8076 11.6982 45.7934C11.7412 45.279 10.938 45.2816 10.8949 45.7934V45.794Z" fill="#007CFF"/>
<path d="M10.3367 45.7709C8.19507 49.3459 5.51908 53.6747 0.951159 54.2156C0.443907 54.2758 0.437947 55.0785 0.951159 55.0176C5.85879 54.4371 8.70961 50.0494 11.03 46.1755C11.2962 45.7312 10.6015 45.3279 10.3367 45.7709Z" fill="#007CFF"/>
<path d="M10.5103 46.0844C9.71101 49.7064 7.42771 52.9383 4.31135 54.951C3.42068 55.5262 2.46975 56.0003 1.47047 56.3554C0.987058 56.5273 1.19499 57.3028 1.6837 57.1289C5.39141 55.8105 8.50115 52.9846 10.1964 49.4393C10.6765 48.4356 11.0447 47.3837 11.2844 46.298C11.3957 45.7948 10.6209 45.5799 10.5096 46.0851L10.5103 46.0844Z" fill="#007CFF"/>
<path d="M10.5102 46.9059C11.1254 49.103 10.5831 51.4456 9.46724 53.3915C8.32493 55.383 6.64557 56.9903 4.91124 58.4674C4.51722 58.8027 5.08805 59.368 5.47941 59.0347C7.32036 57.4677 9.10104 55.738 10.2718 53.5984C11.4148 51.5104 11.936 49.021 11.2843 46.6929C11.1453 46.1964 10.3698 46.4073 10.5096 46.9059H10.5102Z" fill="#007CFF"/>
<path d="M10.3136 45.767C8.0045 48.7906 4.87423 51.1894 1.35128 52.6348C0.879783 52.8285 1.08639 53.6048 1.56451 53.4084C5.30136 51.8757 8.55744 49.3798 11.007 46.1723C11.3202 45.7617 10.6229 45.3617 10.3136 45.7677V45.767Z" fill="#007CFF"/>
<path opacity="0.3" d="M11.0156 47.1818C11.9963 47.2837 12.8737 46.5722 12.975 45.593C13.077 44.6138 12.3645 43.7377 11.3837 43.6365C10.403 43.5347 9.52625 44.2461 9.42427 45.2254C9.32229 46.2046 10.0348 47.08 11.0156 47.1818Z" fill="black"/>
<path d="M1.40839 33.5687L0.505798 31.5362L17.8564 0L53.5686 2.05696L54.2269 3.82565L1.76929 33.5687" fill="url(#paint0_linear_384_666)"/>
<path d="M0.956665 33.5686L18.8317 2.05688L53.7758 3.82557L36.7061 35.3392L0.956665 33.5686Z" fill="url(#paint1_linear_384_666)"/>
<path d="M47.4951 21.5798C47.6599 21.8242 47.8187 22.2809 47.6737 23.1427C47.5257 24.0222 47.0796 25.1614 46.2463 26.492C44.5833 29.1476 41.6318 32.1598 37.7344 34.7796C33.8371 37.3994 29.9315 38.9966 26.8415 39.536C25.2933 39.8063 24.0683 39.7903 23.1965 39.5963C22.3422 39.4063 21.9782 39.0876 21.8134 38.8432C21.6486 38.5987 21.4898 38.1421 21.6348 37.2803C21.7828 36.4008 22.2289 35.2616 23.0622 33.9309C24.7252 31.2754 27.6767 28.2632 31.5741 25.6434C35.4714 23.0235 39.377 21.4264 42.4669 20.887C44.0152 20.6167 45.2402 20.6326 46.112 20.8266C46.9663 21.0167 47.3303 21.3354 47.4951 21.5798Z" fill="#101F23" stroke="url(#paint2_linear_384_666)" stroke-width="5"/>
<path d="M32.8885 18.4866V24.3956L49.5577 53.0054V47.097L32.8885 18.4866Z" fill="url(#paint3_linear_384_666)"/>
<path opacity="0.4" d="M32.8885 23.4052L49.5577 52.0156V50.5094L32.8885 21.899V23.4052Z" fill="#041423"/>
<path d="M66.2268 18.4866V24.3956L49.5577 53.0054V47.097L66.2268 18.4866Z" fill="url(#paint4_linear_384_666)"/>
<path opacity="0.4" d="M66.2268 23.4052L49.5577 52.0156V50.5094L66.2268 21.899V23.4052Z" fill="#041423"/>
<path d="M49.5577 51.9061L31.498 21.0569H19.7067L49.5584 72.4718V51.9061H49.5577Z" fill="url(#paint5_linear_384_666)"/>
<path d="M19.7059 28.5852L49.5576 80.0001V72.4717L19.7059 21.0569V28.5852Z" fill="url(#paint6_linear_384_666)"/>
<path d="M49.5577 72.4718L79.4094 21.0569H67.6181L49.5577 51.9054V72.4711V72.4718Z" fill="url(#paint7_linear_384_666)"/>
<path d="M79.4094 28.5852L49.5577 80.0001V72.4717L79.4094 21.0569V28.5852Z" fill="#00546F"/>
<path d="M32.8885 18.4866L49.5577 47.097V29.4908L43.0826 18.4866H32.8885Z" fill="url(#paint8_linear_384_666)"/>
<path d="M56.0328 18.4866L49.5577 29.4908V47.097L66.2268 18.4866H56.0328Z" fill="url(#paint9_linear_384_666)"/>
<path d="M20.8661 21.724H31.1144L48.8709 52.0548H49.5576V51.9061L31.4978 21.0569H19.7059L20.0933 21.724H20.8661Z" fill="url(#paint10_linear_384_666)"/>
<path d="M20.8662 21.7233H20.0934L49.5577 72.471L48.8896 69.9895L20.8662 21.7233Z" fill="url(#paint11_linear_384_666)"/>
<path d="M48.8709 52.0546L48.8895 52.0863V69.99L49.5576 72.4715V52.0546H48.8709Z" fill="url(#paint12_linear_384_666)"/>
<path d="M78.6366 21.0569H79.4094L49.5577 72.4718L50.2258 69.9903L78.6366 21.0569Z" fill="url(#paint13_linear_384_666)"/>
<path d="M50.2444 52.0546L50.2258 52.0863V69.99L49.5577 72.4715V52.0546H50.2444Z" fill="url(#paint14_linear_384_666)"/>
<path d="M79.4082 21.0631L79.4119 21.0568H79.4028C79.4028 21.0568 79.4064 21.0613 79.4082 21.0631Z" fill="url(#paint15_linear_384_666)"/>
<path d="M67.6181 21.0571L49.5577 51.9063V52.0551H50.2444L68.3479 21.0571H67.6181Z" fill="url(#paint16_linear_384_666)"/>
<path opacity="0.2" d="M37.5128 51.726V59.2543L45.3394 72.7346V65.2063L37.5128 51.726Z" fill="#35495E"/>
<path opacity="0.4" d="M45.9229 66.2112V73.7395L47.095 75.7575V68.2291L45.9229 66.2112Z" fill="#35495E"/>
<path opacity="0.4" d="M49.5577 72.4717L47.9141 69.6405V77.1688L49.5577 80V72.4717Z" fill="#35495E"/>
<path opacity="0.6" d="M22.8799 26.5228V34.0511L26.6969 40.6254V33.097L22.8799 26.5228Z" fill="#EFFDCD"/>
<path opacity="0.6" d="M27.1672 33.9069V41.4352L28.6055 43.9127V36.3843L27.1672 33.9069Z" fill="#EFFDCD"/>
<path opacity="0.3" d="M20.8662 21.7233H31.1146L41.055 38.7026L30.4378 22.6377L20.8662 21.7233Z" fill="#EFFDCD"/>
<path opacity="0.6" d="M50.2258 69.99L50.2443 52.0546L50.6661 67.6203L60.6893 51.9686L50.2258 69.99Z" fill="#EFFDCD"/>
<path opacity="0.4" d="M56.0328 18.4866H66.2268L49.5577 47.097L65.5514 18.7649H55.8692L56.0328 18.4866Z" fill="url(#paint17_linear_384_666)"/>
<path opacity="0.1" d="M32.8885 18.4866H43.0833L49.5577 29.4908V47.097L49.0743 29.5351L42.8727 18.8463L32.8885 18.4866Z" fill="url(#paint18_linear_384_666)"/>
<path d="M44.7626 21.4964C37.8838 20.8895 29.2516 27.8188 25.7954 31.3594L24.7838 37.429L20.4846 29.8421L27.0599 22.761L34.1409 19.9792L42.4865 17.7031L44.7626 21.4964Z" fill="black" fill-opacity="0.2"/>
<path d="M29.0206 21.7053C36.9989 16.3431 45.101 14.7238 47.8499 17.9584L43.6402 11.923C43.5515 11.7207 43.4462 11.527 43.3223 11.3432C40.8497 7.67553 32.2284 9.14932 24.0666 14.6352C16.5472 19.6894 12.0409 26.3859 13.313 30.277C13.4216 30.6089 13.5719 30.921 13.7666 31.2093C13.9236 31.4414 14.105 31.653 14.309 31.8447L19.547 40.0871C19.5437 40.0818 18.7391 38.7945 18.5014 38.4421C16.0287 34.7744 20.7827 27.2428 29.0206 21.706V21.7053Z" fill="url(#paint19_linear_384_666)"/>
<path d="M49.3583 20.0705C49.3583 20.0705 48.3392 18.643 48.1008 18.2899C48.0226 18.1742 47.9392 18.0637 47.8491 17.9586L49.2874 20.0202C49.3113 20.0367 49.3351 20.0533 49.3583 20.0705Z" fill="#007CFF"/>
<path d="M30.4191 23.7807C37.8113 18.8118 45.9816 17.6646 49.2881 20.0205L47.8497 17.9589C45.1002 14.7243 36.9981 16.3429 29.0205 21.7058C20.7826 27.2433 16.0286 34.7743 18.5013 38.4419C18.739 38.7943 19.5436 40.0817 19.5469 40.0869C18.5251 36.1601 22.9745 28.7845 30.4197 23.7807H30.4191Z" fill="url(#paint20_linear_384_666)"/>
<path opacity="0.1" d="M40.7933 10.7485C40.4867 10.4093 33.8904 9.49558 26.6035 14.5087C26.6035 14.5087 26.22 22.002 35.0407 19.6515C39.2748 18.5228 41.8482 17.8173 43.3786 17.3882C44.5758 17.0523 45.0559 15.6275 44.3196 14.6264C42.923 12.7281 40.7933 10.7479 40.7933 10.7479V10.7485Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_384_666" x1="26.0483" y1="-1.26448" x2="3.79346" y2="33.3823" gradientUnits="userSpaceOnUse">
<stop stop-color="#3ED592"/>
<stop offset="1" stop-color="#0B9055"/>
</linearGradient>
<linearGradient id="paint1_linear_384_666" x1="27.3662" y1="2.05689" x2="11.886" y2="33.3824" gradientUnits="userSpaceOnUse">
<stop stop-color="#101F23"/>
<stop offset="1" stop-color="#000304"/>
</linearGradient>
<linearGradient id="paint2_linear_384_666" x1="30.1764" y1="23.5705" x2="39.1131" y2="36.8652" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0"/>
<stop offset="1" stop-color="#5C8692"/>
</linearGradient>
<linearGradient id="paint3_linear_384_666" x1="32.0052" y1="20.4192" x2="48.4582" y2="47.8599" gradientUnits="userSpaceOnUse">
<stop stop-color="#0A356B"/>
<stop offset="0.52" stop-color="#00546F"/>
<stop offset="1" stop-color="#07393F"/>
</linearGradient>
<linearGradient id="paint4_linear_384_666" x1="65.7507" y1="21.7886" x2="49.8684" y2="50.0846" gradientUnits="userSpaceOnUse">
<stop offset="0.5" stop-color="#428690"/>
<stop offset="1" stop-color="#0A5980"/>
</linearGradient>
<linearGradient id="paint5_linear_384_666" x1="21.4403" y1="15.001" x2="54.8294" y2="70.7352" gradientUnits="userSpaceOnUse">
<stop stop-color="#038F7F"/>
<stop offset="0.5" stop-color="#1ADCC4"/>
<stop offset="1" stop-color="#2ED385"/>
</linearGradient>
<linearGradient id="paint6_linear_384_666" x1="19.098" y1="24.3185" x2="49.9177" y2="76.4792" gradientUnits="userSpaceOnUse">
<stop stop-color="#EEFFC9"/>
<stop offset="0.53" stop-color="#22C986"/>
<stop offset="1" stop-color="#01587A"/>
</linearGradient>
<linearGradient id="paint7_linear_384_666" x1="72.8787" y1="20.9418" x2="50.6485" y2="62.2182" gradientUnits="userSpaceOnUse">
<stop stop-color="#D5FFBA"/>
<stop offset="0.5" stop-color="#B8FFCC"/>
<stop offset="1" stop-color="#85F793"/>
</linearGradient>
<linearGradient id="paint8_linear_384_666" x1="51.4211" y1="42.4475" x2="38.6733" y2="19.3018" gradientUnits="userSpaceOnUse">
<stop stop-color="#1580AA"/>
<stop offset="1" stop-color="#0A5980"/>
</linearGradient>
<linearGradient id="paint9_linear_384_666" x1="63.2701" y1="17.1807" x2="47.9837" y2="40.0327" gradientUnits="userSpaceOnUse">
<stop stop-color="#1580AA"/>
<stop offset="1" stop-color="#0A5980"/>
</linearGradient>
<linearGradient id="paint10_linear_384_666" x1="26.6915" y1="22.7581" x2="45.9441" y2="52.238" gradientUnits="userSpaceOnUse">
<stop stop-color="#068882"/>
<stop offset="0.5" stop-color="#2AE3C1"/>
<stop offset="1" stop-color="#A1FEC6"/>
</linearGradient>
<linearGradient id="paint11_linear_384_666" x1="20.496" y1="22.6767" x2="46.8829" y2="66.5998" gradientUnits="userSpaceOnUse">
<stop stop-color="#00787D"/>
<stop offset="0.5" stop-color="#0CBFA3"/>
<stop offset="1" stop-color="#37DA87"/>
</linearGradient>
<linearGradient id="paint12_linear_384_666" x1="45.0195" y1="54.7317" x2="53.0492" y2="68.098" gradientUnits="userSpaceOnUse">
<stop stop-color="#ADFEC8"/>
<stop offset="1" stop-color="#31D885"/>
</linearGradient>
<linearGradient id="paint13_linear_384_666" x1="79.0055" y1="22.0249" x2="52.2717" y2="66.5257" gradientUnits="userSpaceOnUse">
<stop stop-color="#F5FFD9"/>
<stop offset="1" stop-color="#37DA87"/>
</linearGradient>
<linearGradient id="paint14_linear_384_666" x1="54.0958" y1="54.7317" x2="46.0661" y2="68.098" gradientUnits="userSpaceOnUse">
<stop stop-color="#ADFEC8"/>
<stop offset="1" stop-color="#31D885"/>
</linearGradient>
<linearGradient id="paint15_linear_384_666" x1="79.4064" y1="21.0559" x2="79.4101" y2="21.0613" gradientUnits="userSpaceOnUse">
<stop stop-color="#068882"/>
<stop offset="0.5" stop-color="#2AE3C1"/>
<stop offset="1" stop-color="#A1FEC6"/>
</linearGradient>
<linearGradient id="paint16_linear_384_666" x1="55.5566" y1="31.3717" x2="63.0499" y2="42.8453" gradientUnits="userSpaceOnUse">
<stop stop-color="#068882"/>
<stop offset="0.5" stop-color="#2AE3C1"/>
<stop offset="1" stop-color="#A1FEC6"/>
</linearGradient>
<linearGradient id="paint17_linear_384_666" x1="60.9272" y1="19.1418" x2="54.5911" y2="46.3026" gradientUnits="userSpaceOnUse">
<stop stop-color="#F5FFD9"/>
<stop offset="1" stop-color="#37DA87"/>
</linearGradient>
<linearGradient id="paint18_linear_384_666" x1="44.1435" y1="20.3115" x2="38.8606" y2="42.9602" gradientUnits="userSpaceOnUse">
<stop stop-color="#F5FFD9"/>
<stop offset="1" stop-color="#37DA87"/>
</linearGradient>
<linearGradient id="paint19_linear_384_666" x1="41.9809" y1="10.8744" x2="15.1739" y2="34.1409" gradientUnits="userSpaceOnUse">
<stop stop-color="#2C383C"/>
<stop offset="1" stop-color="#112024"/>
</linearGradient>
<linearGradient id="paint20_linear_384_666" x1="33.5673" y1="16.2985" x2="18.7142" y2="37.6818" gradientUnits="userSpaceOnUse">
<stop stop-color="#3DD491"/>
<stop offset="1" stop-color="#0A8F54"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 261.76 226.69" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(1.3333 0 0 -1.3333 -76.311 313.34)"><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-75.491l98.16-170.02 98.16 170.02z" fill="#41b883"/></g><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-36.227l58.896-102.01 58.896 102.01z" fill="#34495e"/></g></g></svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@@ -0,0 +1,39 @@
<svg width="329" height="70" viewBox="0 0 329 70" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M101.159 17.5519C101.458 16.6369 102.056 16.2302 102.853 16.2302H106.738C107.137 16.2302 107.436 16.3319 107.734 16.6369C108.033 16.9419 108.133 17.2469 108.133 17.6536C108.133 17.7553 108.133 17.9586 108.033 18.162L96.4768 51.1039C96.2776 51.7139 95.9787 52.2223 95.7795 52.4256C95.4806 52.7306 95.0821 52.8323 94.584 52.8323H90.4994C90.0012 52.8323 89.6027 52.7306 89.3039 52.4256C89.005 52.1206 88.8057 51.7139 88.6065 51.1039L77.0501 18.162C76.9504 17.9586 76.9504 17.7553 76.9504 17.6536C76.9504 17.2469 77.0501 16.9419 77.3489 16.6369C77.6478 16.3319 77.9467 16.2302 78.3452 16.2302H82.2305C83.0275 16.2302 83.6253 16.6369 83.9241 17.5519L92.5915 42.5634L101.159 17.5519Z" fill="white"/>
<path d="M109.727 26.0924C110.026 25.7874 110.325 25.6857 110.723 25.6857H114.609C115.007 25.6857 115.306 25.7874 115.605 26.0924C115.904 26.3974 116.003 26.7025 116.003 27.1091V40.4283C116.003 42.665 116.502 44.2918 117.398 45.5119C118.295 46.732 119.69 47.342 121.582 47.342C123.376 47.342 124.77 46.732 125.667 45.5119C126.663 44.2918 127.161 42.5634 127.161 40.4283V27.1091C127.161 26.7025 127.261 26.3974 127.56 26.0924C127.859 25.7874 128.158 25.6857 128.556 25.6857H132.442C132.84 25.6857 133.139 25.7874 133.438 26.0924C133.737 26.3974 133.836 26.7025 133.836 27.1091V51.5106C133.836 51.9172 133.737 52.2223 133.438 52.5273C133.139 52.8323 132.84 52.934 132.442 52.934H128.556C128.158 52.934 127.859 52.8323 127.56 52.5273C127.261 52.2223 127.161 51.9172 127.161 51.5106V50.2905C125.169 52.4256 122.778 53.4423 119.988 53.4423C116.103 53.4423 113.413 52.3239 111.72 50.0871C110.126 47.8503 109.229 44.9018 109.229 41.2416V27.1091C109.329 26.7025 109.528 26.2958 109.727 26.0924Z" fill="white"/>
<path d="M157.247 26.9058C159.14 28.1259 160.635 29.7526 161.731 31.7861C162.727 33.8195 163.325 36.0563 163.325 38.3948V40.7333C163.325 41.14 163.225 41.445 162.926 41.75C162.627 42.055 162.328 42.1567 161.93 42.1567H144.794C144.794 43.6818 145.392 44.9018 146.588 45.7152C147.783 46.6303 149.178 47.037 150.772 47.037C152.665 47.037 154.06 46.6303 155.155 45.8169C155.554 45.5119 155.853 45.3085 156.052 45.2069C156.251 45.1052 156.65 45.0035 157.048 45.0035H161.133C161.531 45.0035 161.83 45.1052 162.129 45.4102C162.428 45.7152 162.528 46.0202 162.528 46.3253C162.528 47.037 162.029 47.952 161.133 49.0704C160.236 50.1888 158.841 51.2055 157.048 52.0189C155.255 52.8323 153.163 53.239 150.772 53.239C148.281 53.239 145.99 52.6289 144.097 51.4089C142.204 50.1888 140.71 48.562 139.714 46.4269C138.717 44.2918 138.219 41.8517 138.219 39.0048C138.219 36.3613 138.717 34.0229 139.714 31.8877C140.71 29.7526 142.204 28.0242 144.097 26.8041C145.99 25.5841 148.182 24.8723 150.672 24.8723C153.063 25.0757 155.255 25.6857 157.247 26.9058ZM156.251 36.2597C155.853 33.0061 153.96 31.4811 150.573 31.4811C148.779 31.4811 147.385 31.9894 146.388 32.9045C145.392 33.9212 144.794 35.0396 144.695 36.2597H156.251Z" fill="white"/>
<path d="M200.983 26.3975C200.684 26.7025 200.385 26.8041 199.987 26.8041H196.4C195.902 26.8041 195.404 26.7025 195.105 26.4991C194.806 26.2958 194.507 25.9908 194.308 25.5841C193.71 24.5673 192.913 23.8556 191.817 23.2456C190.721 22.6356 189.227 22.3306 187.334 22.3306C185.441 22.3306 183.947 22.7372 182.652 23.449C181.357 24.2623 180.759 25.2791 180.759 26.4991C180.759 27.5159 181.058 28.3292 181.656 28.9393C182.253 29.5493 183.15 29.956 184.445 30.3627C185.74 30.7694 187.733 31.2777 190.622 31.8878C194.806 32.8028 197.795 34.0229 199.588 35.7513C201.481 37.3781 202.378 39.6149 202.378 42.4617C202.378 44.5968 201.78 46.4269 200.584 48.0537C199.389 49.6805 197.695 51.0022 195.404 51.9173C193.212 52.8323 190.522 53.3407 187.533 53.3407C184.943 53.3407 182.552 52.934 180.361 52.1206C178.169 51.3072 176.376 50.0871 174.981 48.5621C173.586 47.037 172.889 45.2069 172.789 43.2751C172.789 42.8684 172.889 42.5634 173.188 42.2584C173.486 41.9533 173.785 41.8517 174.184 41.8517H177.77C178.268 41.8517 178.767 41.9533 179.065 42.1567C179.364 42.36 179.663 42.6651 179.862 43.0717C180.46 44.1901 181.257 45.1052 182.353 45.8169C183.449 46.5286 185.143 46.8336 187.533 46.8336C189.825 46.8336 191.718 46.4269 193.212 45.7152C194.706 45.0035 195.503 43.8851 195.503 42.4617C195.503 41.6483 195.205 40.9366 194.607 40.4283C194.009 39.9199 193.212 39.5132 192.116 39.2082C191.02 38.9032 189.426 38.4965 187.334 37.8864C186.438 37.6831 185.342 37.4798 184.047 37.0731C180.46 36.158 177.87 34.9379 176.276 33.3112C174.682 31.7861 173.885 29.4476 173.885 26.3975C173.885 24.364 174.483 22.5339 175.678 20.8055C176.874 19.1787 178.468 17.857 180.56 16.9419C182.652 16.0269 184.943 15.5185 187.434 15.5185C190.323 15.5185 192.814 16.0269 194.906 17.0436C196.998 18.0603 198.592 19.2804 199.688 20.8055C200.784 22.3306 201.381 23.754 201.481 25.1774C201.381 25.7874 201.282 26.0924 200.983 26.3975Z" fill="white"/>
<path d="M213.635 45.6136C214.731 46.6303 216.126 47.1386 217.719 47.1386C219.214 47.1386 220.409 46.9353 221.206 46.4269C222.003 46.0202 222.701 45.2069 223.298 44.2918C223.597 43.8851 223.797 43.4784 224.095 43.2751C224.394 43.0717 224.693 42.8684 224.992 42.8684H228.877C229.276 42.8684 229.575 42.9701 229.874 43.2751C230.173 43.5801 230.272 43.8851 230.272 44.2918C230.173 45.4102 229.674 46.732 228.778 48.1554C227.881 49.5788 226.486 50.7989 224.594 51.8156C222.701 52.8323 220.409 53.3407 217.719 53.3407C215.329 53.3407 213.137 52.8323 211.244 51.7139C209.351 50.5955 207.857 49.1721 206.761 47.342C205.665 45.5119 205.067 43.4784 204.868 41.3433C204.868 40.9366 204.768 40.2249 204.768 39.2082C204.768 38.1915 204.768 37.4798 204.868 37.0731C205.067 34.9379 205.665 32.9045 206.761 31.0744C207.857 29.2443 209.351 27.8209 211.244 26.7025C213.137 25.5841 215.329 25.0757 217.719 25.0757C220.409 25.0757 222.701 25.5841 224.594 26.6008C226.486 27.6175 227.881 28.8376 228.778 30.261C229.674 31.6844 230.173 33.0062 230.272 34.1246C230.272 34.5313 230.173 34.8363 229.874 35.1413C229.575 35.4463 229.276 35.548 228.877 35.548H224.992C224.693 35.548 224.394 35.4463 224.095 35.1413C223.797 34.9379 223.597 34.5313 223.298 34.1246C222.601 33.1078 221.904 32.3961 221.206 31.9894C220.409 31.5828 219.313 31.2777 217.719 31.2777C216.126 31.2777 214.83 31.7861 213.635 32.8028C212.439 33.8196 211.842 35.3446 211.642 37.3781C211.642 37.6831 211.543 38.2931 211.543 39.2082C211.543 40.1232 211.543 40.7333 211.642 41.0383C211.742 43.0717 212.439 44.5968 213.635 45.6136Z" fill="white"/>
<path d="M258.865 52.4256C258.566 52.7306 258.267 52.8323 257.868 52.8323H253.983C253.584 52.8323 253.286 52.7306 252.987 52.4256C252.688 52.1205 252.588 51.8155 252.588 51.4088V38.1914C252.588 36.0563 252.09 34.3279 251.094 33.1078C250.098 31.8877 248.603 31.2777 246.71 31.2777C244.917 31.2777 243.522 31.8877 242.427 33.2095C241.331 34.4295 240.833 36.158 240.833 38.1914V51.5105C240.833 51.9172 240.733 52.2222 240.434 52.5272C240.135 52.8323 239.836 52.9339 239.438 52.9339H235.552C235.154 52.9339 234.855 52.8323 234.556 52.5272C234.257 52.2222 234.158 51.9172 234.158 51.5105V17.1452C234.158 16.7385 234.257 16.4335 234.556 16.1285C234.855 15.8235 235.154 15.7218 235.552 15.7218H239.438C239.836 15.7218 240.135 15.8235 240.434 16.1285C240.733 16.4335 240.833 16.7385 240.833 17.1452V28.3292C242.925 26.1941 245.316 25.1773 248.205 25.1773C252.09 25.1773 254.88 26.2957 256.573 28.5325C258.267 30.7693 259.163 33.7178 259.163 37.378V51.5105C259.263 51.8155 259.064 52.2222 258.865 52.4256Z" fill="white"/>
<path d="M289.15 39.3099C289.15 40.2249 289.15 41.14 289.051 41.9534C288.852 44.0885 288.254 46.0203 287.158 47.7487C286.062 49.4771 284.568 50.9005 282.675 51.9173C280.782 52.934 278.59 53.4423 276.199 53.4423C273.808 53.4423 271.616 52.934 269.724 51.9173C267.831 50.9005 266.336 49.4771 265.241 47.7487C264.145 46.0203 263.547 44.0885 263.348 41.9534C263.348 41.5467 263.248 40.6316 263.248 39.3099C263.248 37.9881 263.248 37.0731 263.348 36.6664C263.547 34.5313 264.145 32.4978 265.241 30.7694C266.336 29.0409 267.831 27.6175 269.724 26.6008C271.616 25.5841 273.808 25.0757 276.199 25.0757C278.59 25.0757 280.782 25.5841 282.675 26.6008C284.568 27.6175 286.062 29.0409 287.158 30.7694C288.254 32.4978 288.852 34.4296 289.051 36.6664C289.15 37.4798 289.15 38.3948 289.15 39.3099ZM272.015 32.7012C270.819 33.5145 270.222 34.9379 270.022 36.9714C270.022 37.2764 269.923 38.0898 269.923 39.3099C269.923 40.5299 269.923 41.3433 270.022 41.6483C270.222 43.6818 270.919 45.0035 272.015 45.9186C273.21 46.732 274.506 47.1387 276.199 47.1387C277.793 47.1387 279.188 46.732 280.383 45.9186C281.579 45.1052 282.177 43.6818 282.376 41.6483C282.476 40.9366 282.476 40.1232 282.476 39.3099C282.476 38.3948 282.476 37.6831 282.376 36.9714C282.177 34.9379 281.479 33.6162 280.383 32.7012C279.188 31.8878 277.893 31.4811 276.199 31.4811C274.605 31.3794 273.21 31.7861 272.015 32.7012Z" fill="white"/>
<path d="M318.141 39.3099C318.141 40.2249 318.141 41.14 318.041 41.9534C317.842 44.0885 317.244 46.0203 316.148 47.7487C315.053 49.4771 313.558 50.9005 311.665 51.9173C309.772 52.934 307.581 53.4423 305.19 53.4423C302.799 53.4423 300.607 52.934 298.714 51.9173C296.821 50.9005 295.327 49.4771 294.231 47.7487C293.135 46.0203 292.537 44.0885 292.338 41.9534C292.338 41.5467 292.239 40.6316 292.239 39.3099C292.239 37.9881 292.239 37.0731 292.338 36.6664C292.537 34.5313 293.135 32.4978 294.231 30.7694C295.327 29.0409 296.821 27.6175 298.714 26.6008C300.607 25.5841 302.799 25.0757 305.19 25.0757C307.581 25.0757 309.772 25.5841 311.665 26.6008C313.558 27.6175 315.053 29.0409 316.148 30.7694C317.244 32.4978 317.842 34.4296 318.041 36.6664C318.141 37.4798 318.141 38.3948 318.141 39.3099ZM301.005 32.7012C299.81 33.5145 299.212 34.9379 299.013 36.9714C299.013 37.2764 298.913 38.0898 298.913 39.3099C298.913 40.5299 298.913 41.3433 299.013 41.6483C299.212 43.6818 299.91 45.0035 301.005 45.9186C302.101 46.732 303.496 47.1387 305.19 47.1387C306.784 47.1387 308.178 46.732 309.374 45.9186C310.47 45.1052 311.167 43.6818 311.366 41.6483C311.466 40.9366 311.466 40.1232 311.466 39.3099C311.466 38.3948 311.466 37.6831 311.366 36.9714C311.167 34.9379 310.47 33.6162 309.374 32.7012C308.178 31.8878 306.883 31.4811 305.19 31.4811C303.496 31.3794 302.101 31.7861 301.005 32.7012Z" fill="white"/>
<path d="M328.602 52.4256C328.303 52.7306 328.004 52.8323 327.605 52.8323H323.72C323.321 52.8323 323.023 52.7306 322.724 52.4256C322.425 52.1206 322.325 51.8155 322.325 51.4088V17.1452C322.325 16.7385 322.425 16.4335 322.724 16.1285C323.023 15.8235 323.321 15.7218 323.72 15.7218H327.605C328.004 15.7218 328.303 15.8235 328.602 16.1285C328.9 16.4335 329 16.7385 329 17.1452V51.5105C329 51.8155 328.9 52.2222 328.602 52.4256Z" fill="white"/>
<path d="M18.0193 43.4278C13.3391 20.1921 17.7735 4.94458 17.7735 4.94458C17.7735 4.94458 16.5872 14.2263 25.9124 29.1867C25.7936 47.7331 29.4296 57.4076 29.4296 57.4076L30.1768 56.8556L29.4296 57.5092C29.4296 57.5092 23.9004 52.9774 18.0193 43.4278Z" fill="url(#paint0_linear)"/>
<path d="M40.0617 40.1381C42.2987 42.4014 44.3858 43.9674 45.8943 44.7121C45.8854 44.7076 45.8765 44.7031 45.8676 44.6985C45.8676 44.6985 47.5612 45.3085 47.8601 44.9019C49.9248 42.707 50.4294 38.6169 49.0534 30.9295C41.1826 26.4169 40.1891 19.3821 40.1891 19.3821C40.1891 19.3821 38.7714 29.1486 40.0617 40.1381Z" fill="url(#paint1_linear)"/>
<path d="M17.7733 4.94458C17.7733 4.94458 11.2977 27.2109 22.2564 58.9327C22.5552 59.8477 22.2564 60.8645 21.4594 61.4745L9.90295 69.6083C8.80708 70.4217 7.21309 69.9133 6.71497 68.5916C3.62661 60.1527 -4.84147 32.1928 3.72623 1.58938C4.12473 0.369314 5.41985 -0.342394 6.61534 0.165969L17.7733 4.94458Z" fill="url(#paint2_linear)"/>
<path d="M39.8901 49.6805L29.4295 57.4076C29.4295 57.4076 23.8505 42.5634 26.7396 14.4001C26.9389 12.7734 28.6325 11.96 30.0272 12.7734L40.1889 19.3821C40.1889 19.3821 38.1964 33.1079 41.0855 46.3253C41.3844 47.647 40.8863 48.9688 39.8901 49.6805Z" fill="url(#paint3_linear)"/>
<path d="M59.2171 32.4978L49.5535 26.0925C48.9558 25.6858 48.0592 26.2958 48.2584 27.0075C50.5498 37.2764 50.2509 42.3601 47.8599 44.9019C47.561 45.3086 45.8674 44.6985 45.8674 44.6985C47.4614 45.5119 47.8599 45.1052 47.8599 45.1052C49.4539 43.8851 55.4314 39.5132 59.2171 36.8697C60.7115 35.7513 60.7115 33.5145 59.2171 32.4978Z" fill="url(#paint4_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="28.8913" y1="18.8094" x2="35.0967" y2="39.5842" gradientUnits="userSpaceOnUse">
<stop stop-color="#131057"/>
<stop offset="1" stop-color="#4F48AD"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="28.8913" y1="18.8094" x2="35.0967" y2="39.5842" gradientUnits="userSpaceOnUse">
<stop stop-color="#131057"/>
<stop offset="1" stop-color="#4F48AD"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="18.283" y1="48.2944" x2="6.18562" y2="20.1682" gradientUnits="userSpaceOnUse">
<stop stop-color="#EE6D93"/>
<stop offset="1" stop-color="#F7C6B8"/>
</linearGradient>
<linearGradient id="paint3_linear" x1="32.6998" y1="56.6983" x2="30.824" y2="12.4476" gradientUnits="userSpaceOnUse">
<stop stop-color="#3CAC96"/>
<stop offset="1" stop-color="#5FCEB8"/>
</linearGradient>
<linearGradient id="paint4_linear" x1="53.4174" y1="22.3712" x2="46.1697" y2="38.4852" gradientUnits="userSpaceOnUse">
<stop stop-color="#80C3FF"/>
<stop offset="0.455382" stop-color="#349BF7"/>
<stop offset="1" stop-color="#714DD6"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View File

@@ -0,0 +1,6 @@
<svg width="432" height="384" viewBox="0 0 432 384" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M219.897 103.248L146.523 230.363L216 350.772L308.021 191.386L400 32H261.005L219.897 103.248Z" fill="#1697F6"/>
<path d="M124.021 191.386L128.84 199.768L197.478 80.8258L225.68 32H216H32L124.021 191.386Z" fill="#AEDDFF"/>
<path d="M261.005 32C295.785 146.458 216 350.772 216 350.772L146.523 230.363L261.005 32Z" fill="#1867C0"/>
<path d="M225.68 32C79.1426 32 128.84 199.768 128.84 199.768L225.68 32Z" fill="#7BC6FF"/>
</svg>

After

Width:  |  Height:  |  Size: 533 B

View File

@@ -0,0 +1,19 @@
import { sections } from "@/data/sections"
/* Types are defined from the data */
export type SectionName = (typeof sections)[number]["name"]
export type Section = {
name: SectionName
icon: string
components: Component[]
}
export type ComponentName = (typeof sections)[number]["components"][number]["name"]
export type Component = {
name: ComponentName
aliases?: string[]
help: string
}

4
app/types/global.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare type Optional<T> = T | undefined // sugar
// https://stackoverflow.com/a/75926487/2255491
declare function useNuxtApp(): NuxtApp

View File

@@ -0,0 +1,128 @@
export type SocialLinks = Partial<{
website: string
github: string
linkedin: string
twitter: string
x: string
email: string
telegram: string
instagram: string
whatsapp: string
spotify: string
bento: string
}>
export type Profile = {
name: string
title: string
location?: string
summary: string
avatar?: string
socials?: SocialLinks
}
export type SkillType =
| 'Language'
| 'Framework'
| 'Library'
| 'Tool'
| 'Service'
| 'DevOps'
| 'Testing'
| 'Data'
export const SKILL_TYPES: readonly SkillType[] = [
'Language',
'Framework',
'Library',
'Tool',
'Service',
'DevOps',
'Testing',
'Data'
] as const
export type Tag = {
label: string
icon?: string
description?: string
to?: string
type?: SkillType
}
export type TagGroup = {
title: string
items: Tag[]
}
// Backward-compatible single-position experience
export type Experience = {
company: string
role: string
start: string
end?: string
ongoing?: boolean
description: string[]
icons?: string[]
location?: string
type?: string // e.g., Full-time, Self-employed
link?: string
linkLabel?: string
logo?: string
}
// Position inside a company timeline (LinkedIn-style)
export type Position = {
title: string
start: string
end?: string
ongoing?: boolean
description: string[]
icons?: string[]
location?: string
type?: string
link?: string
linkLabel?: string
logo?: string
}
// Company grouping multiple positions
export type CompanyExperience = {
company: string
link?: string
logo?: string
location?: string
type?: string
positions: Position[]
}
export type Education = {
school: string
degree: string
start: string
end: string
icons?: string[]
logo?: string
}
export type Project = {
name: string
description: string
status?: string
links?: { label: string; to: string; icon?: string }[]
icons?: string[]
thumbnail?: string
opensource?: boolean
category?: 'current' | 'freelance' | 'public'
}
export type PortfolioData = {
profile: Profile
mainTools: TagGroup
roles: TagGroup
values: TagGroup
// Supports both single experiences and grouped company timelines
experiences: Array<Experience | CompanyExperience>
education: Education[]
projects: Project[]
}

View File

@@ -0,0 +1,10 @@
export type Recommendation = {
author: string
author_title: string
relationship: string
date: string
linkedin_url: string
recommendation_url: string
text: string
}

9
app/utils/any_all.ts Normal file
View File

@@ -0,0 +1,9 @@
// like `any` in Python
export const any = (iterable: any[]): boolean => {
return iterable.some(Boolean)
}
// like `all` in Python
export const all = (iterable: any[]): boolean => {
return iterable.every(Boolean)
}

13
app/utils/chipTones.ts Normal file
View File

@@ -0,0 +1,13 @@
export const chipTones = [
'bg-indigo-50 text-indigo-700 ring-indigo-200 dark:bg-indigo-400/10 dark:text-indigo-300 dark:ring-indigo-500/30',
'bg-emerald-50 text-emerald-700 ring-emerald-200 dark:bg-emerald-400/10 dark:text-emerald-300 dark:ring-emerald-500/30',
'bg-sky-50 text-sky-700 ring-sky-200 dark:bg-sky-400/10 dark:text-sky-300 dark:ring-sky-500/30',
'bg-violet-50 text-violet-700 ring-violet-200 dark:bg-violet-400/10 dark:text-violet-300 dark:ring-violet-500/30',
'bg-rose-50 text-rose-700 ring-rose-200 dark:bg-rose-400/10 dark:text-rose-300 dark:ring-rose-500/30',
'bg-amber-50 text-amber-800 ring-amber-200 dark:bg-amber-400/10 dark:text-amber-300 dark:ring-amber-500/30',
'bg-cyan-50 text-cyan-700 ring-cyan-200 dark:bg-cyan-400/10 dark:text-cyan-300 dark:ring-cyan-500/30',
'bg-fuchsia-50 text-fuchsia-700 ring-fuchsia-200 dark:bg-fuchsia-400/10 dark:text-fuchsia-300 dark:ring-fuchsia-500/30'
]
export const neutralChipTone =
'bg-white/70 text-slate-700 dark:bg-white/5 dark:text-slate-300 ring-slate-200/70 dark:ring-slate-700/50'

3
app/utils/findBy.ts Normal file
View File

@@ -0,0 +1,3 @@
// Find an item in `items` array, given a specific key/value.
export const findBy = <T>(key: keyof T, value: T[keyof T], items: T[]): T | undefined =>
items.find((obj) => obj[key] === value)

View File

@@ -0,0 +1,3 @@
// round to the first decimal if the number is higher than 1000
export const getDisplayableNumber = (number: number): string =>
number >= 1000 ? `${Math.round(number / 100) / 10}k` : `${number}`

5
app/utils/isSubset.ts Normal file
View File

@@ -0,0 +1,5 @@
// return true if all elements of arr1 are in arr2
export const isSubset = (arr1: any[], arr2: any[]): boolean => {
let set2 = new Set(arr2)
return arr1.every((i) => set2.has(i))
}

6
i18n.config.ts Normal file
View File

@@ -0,0 +1,6 @@
export default defineI18nConfig(() => ({
legacy: false,
warnHtmlMessage: false,
fallbackLocale: 'en',
// Messages are loaded lazily from `langDir` configured in nuxt.config.ts
}))

169
i18n/locales/en.json Normal file
View File

@@ -0,0 +1,169 @@
{
"sections": {
"work": "Work Experience",
"education": "Education",
"recommendations": "Recommendations",
"projects": " Projects",
"blog": "Blog",
"contact": "Contact",
"mainTools": "Main tools",
"roles": "Roles",
"values": "Values",
"skills": "Skills",
"softSkills": "Values & Soft Skills",
"language": "Language Proficiency"
},
"skills": {
"expert": "Expert",
"proficient": "Proficient",
"usedBefore": "Used before",
"aiStack": "AI Stack"
},
"softSkills": {
"problemSolving": {
"label": "Problem solving",
"desc": "Approaches complex issues with pragmatic, thoughtful solutions."
},
"attentionToDetail": {
"label": "Attention to detail",
"desc": "Catches small issues early; delivers polished work."
},
"teamwork": {
"label": "Teamwork",
"desc": "Collaborative, reliable, and generous with knowledge sharing."
},
"teamLeadership": {
"label": "Team leadership",
"desc": "Aligns goals, unblocks teammates, fosters momentum."
},
"communication": {
"label": "Communication",
"desc": "Clear, concise updates and respectful listening."
},
"teamPerformance": {
"label": "Team performance",
"desc": "Improves processes and outcomes with actionable feedback."
},
"customerFocus": {
"label": "Customer focus",
"desc": "Owns outcomes; follows through to satisfaction."
},
"projectManagement": {
"label": "Project management",
"desc": "Plans, prioritizes, and ships on schedule."
},
"adaptability": {
"label": "Adaptability",
"desc": "Learns fast and adjusts smoothly to change."
}
},
"languageSection": {
"tagline": "Advanced English communication for global collaboration.",
"duolingo": {
"label": "Duolingo English Test",
"value": "85"
},
"items": {
"ielts": {
"title": "IELTS readiness",
"desc": "Ready to schedule the IELTS exam immediately once an overseas opportunity is confirmed; current study plan keeps academic band goals on track."
},
"huawei": {
"title": "English at Huawei",
"desc": "Ran performance analytics, stakeholder updates, and incident coordination entirely in English for 7+ years with multinational teams."
}
}
},
"hero": {
"currently": "Currently at"
},
"toasts": {
"emailCopied": {
"title": "Copied",
"desc": "Email address copied to clipboard: {email}"
},
"copyFailed": {
"title": "Copy failed",
"desc": "Could not access clipboard. Please copy: {email}"
}
},
"buttons": {
"blog": "Blog Posts",
"contact": "Contact",
"website": "Website",
"switchToEnglish": "English",
"switchToPersian": "فارسی"
},
"nav": {
"home": "Home",
"skills": "Skills",
"blog": "Blog",
"contact": "Contact",
"theme": "Theme"
},
"blog": {
"explore": "Stories & notes",
"empty": "No posts yet. Check back soon!",
"readMore": "Read article"
},
"common": {
"present": "Present"
},
"meta": {
"portfolioTitleSuffix": "Portfolio"
},
"projectLabels": {
"openSource": "Open Source"
},
"theme": {
"customizer": "Theme Customizer",
"mode": {
"system": "System",
"light": "Light",
"dark": "Dark"
},
"primary": {
"label": "Primary Color",
"indigo": "Indigo",
"emerald": "Emerald",
"violet": "Violet",
"rose": "Rose",
"amber": "Amber"
}
},
"ai_stack": {
"title": "AI Stack",
"subtitle": "IDEs, protocols, concepts, and approaches that power my AI workflow",
"filter": {
"all": "All",
"methods": "Methods",
"ides": "IDEs",
"assistants": "Assistants",
"rules": "Rules",
"mcps": "MCPs",
"extensions": "Extensions",
"infra": "Infra",
"evaluation": "Evaluation"
},
"group": {
"ide_dev": "IDEs / Development",
"protocols": "Protocols & Standards",
"concepts": "AI Concepts & Methodologies",
"approaches": "Emerging Approaches"
},
"badge": {
"setup": "Setup",
"rules": "Rules",
"example": "Example"
},
"legend": {
"featured": "Featured",
"heat": "Usage Intensity"
}
},
"projectCategories": {
"current": "Current",
"freelance": "Freelance Projects",
"public": "Public Repositories"
}
}

Some files were not shown because too many files have changed in this diff Show More