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
+112
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>
+37
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>
+34
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>
+177
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>
+111
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>