This commit is contained in:
Oscar
2026-06-22 09:31:44 +03:00
commit 3902d0dec4
48 changed files with 19218 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
<template>
<section id="about" class="py-24 px-4">
<div class="max-w-4xl mx-auto">
<div class="section-hidden" ref="sectionRef">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
О себе
</h2>
<div class="w-16 h-1 bg-indigo-500 mb-10 rounded-full" />
<div class="grid md:grid-cols-2 gap-8">
<div>
<p class="text-gray-400 text-lg leading-relaxed mb-6">
Привет! Я fullstack-разработчик с более чем 5 годами опыта создания
масштабируемых веб-приложений. Специализируюсь на экосистеме Node.js
и современных JavaScript-фреймворках.
</p>
<p class="text-gray-400 text-lg leading-relaxed">
Люблю чистую архитектуру, хорошо задокументированный код и всё, что
связано с DevOps-практиками. Открыт к интересным проектам и
долгосрочному сотрудничеству.
</p>
</div>
<div class="space-y-4">
<UCard class="bg-gray-900 border-gray-800">
<div class="flex items-center gap-3">
<UIcon name="i-heroicons-map-pin" class="text-indigo-400 w-5 h-5" />
<span class="text-gray-300">Москва, Россия</span>
</div>
</UCard>
<UCard class="bg-gray-900 border-gray-800">
<div class="flex items-center gap-3">
<UIcon name="i-heroicons-briefcase" class="text-indigo-400 w-5 h-5" />
<span class="text-gray-300">5+ лет опыта</span>
</div>
</UCard>
<UCard class="bg-gray-900 border-gray-800">
<div class="flex items-center gap-3">
<UIcon name="i-heroicons-language" class="text-indigo-400 w-5 h-5" />
<span class="text-gray-300">Русский, English (B2)</span>
</div>
</UCard>
<UCard class="bg-gray-900 border-gray-800">
<div class="flex items-center gap-3">
<UIcon name="i-heroicons-check-circle" class="text-indigo-400 w-5 h-5" />
<span class="text-gray-300">Доступен для фриланса</span>
</div>
</UCard>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
const sectionRef = ref<HTMLElement | null>(null);
onMounted(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.remove('section-hidden');
entry.target.classList.add('section-visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.15 },
);
if (sectionRef.value) observer.observe(sectionRef.value);
});
</script>

View File

@@ -0,0 +1,200 @@
<template>
<section id="contact" class="py-24 px-4 bg-gray-950">
<div class="max-w-2xl mx-auto">
<div class="section-hidden" ref="sectionRef">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
Связаться
</h2>
<div class="w-16 h-1 bg-indigo-500 mb-10 rounded-full" />
<UCard class="bg-gray-900 border-gray-800">
<div v-if="success" class="space-y-4">
<UAlert
color="success"
variant="soft"
icon="i-heroicons-check-circle"
title="Сообщение отправлено!"
description="Мы получили ваше обращение и свяжемся с вами в ближайшее время."
/>
<UAlert
v-if="aiReply"
color="primary"
variant="soft"
icon="i-heroicons-sparkles"
title="Автоответ от AI"
:description="aiReply"
/>
<UButton
color="neutral"
variant="ghost"
icon="i-heroicons-arrow-left"
@click="handleReset"
>
Отправить ещё
</UButton>
</div>
<form v-else @submit.prevent="handleSubmit" class="space-y-5">
<UAlert
v-if="error"
color="error"
variant="soft"
icon="i-heroicons-exclamation-triangle"
:title="error"
/>
<UFormField
label="Имя"
required
:error="validationErrors.name"
>
<UInput
v-model="form.name"
placeholder="Иван Иванов"
:disabled="loading"
size="lg"
class="w-full"
/>
</UFormField>
<UFormField
label="Телефон"
required
:error="validationErrors.phone"
>
<UInput
v-model="form.phone"
placeholder="+7 999 123-45-67"
type="tel"
:disabled="loading"
size="lg"
class="w-full"
/>
</UFormField>
<UFormField
label="Email"
required
:error="validationErrors.email"
>
<UInput
v-model="form.email"
placeholder="ivan@example.com"
type="email"
:disabled="loading"
size="lg"
class="w-full"
/>
</UFormField>
<UFormField label="Комментарий">
<UTextarea
v-model="form.comment"
placeholder="Расскажите о вашем проекте..."
:disabled="loading"
:rows="4"
class="w-full"
/>
</UFormField>
<UButton
type="submit"
color="primary"
size="lg"
:loading="loading"
:disabled="loading"
icon="i-heroicons-paper-airplane"
class="w-full justify-center"
>
{{ loading ? 'Отправляем...' : 'Отправить' }}
</UButton>
</form>
</UCard>
</div>
</div>
</section>
</template>
<script setup lang="ts">
const sectionRef = ref<HTMLElement | null>(null);
const { loading, error, success, aiReply, submit, reset } = useContact();
const form = reactive({
name: '',
phone: '',
email: '',
comment: '',
});
const validationErrors = reactive({
name: '',
phone: '',
email: '',
});
function validate(): boolean {
validationErrors.name = '';
validationErrors.phone = '';
validationErrors.email = '';
let valid = true;
if (!form.name.trim()) {
validationErrors.name = 'Введите имя';
valid = false;
}
if (!form.phone.trim()) {
validationErrors.phone = 'Введите телефон';
valid = false;
}
else if (!/^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/.test(form.phone.replace(/\s/g, ''))) {
validationErrors.phone = 'Некорректный формат телефона';
valid = false;
}
if (!form.email.trim()) {
validationErrors.email = 'Введите email';
valid = false;
}
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) {
validationErrors.email = 'Некорректный email';
valid = false;
}
return valid;
}
async function handleSubmit() {
if (!validate()) return;
await submit({
name: form.name.trim(),
phone: form.phone.trim(),
email: form.email.trim(),
comment: form.comment.trim() || undefined,
});
}
function handleReset() {
reset();
form.name = '';
form.phone = '';
form.email = '';
form.comment = '';
}
onMounted(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.remove('section-hidden');
entry.target.classList.add('section-visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1 },
);
if (sectionRef.value) observer.observe(sectionRef.value);
});
</script>

View File

@@ -0,0 +1,56 @@
<template>
<section id="hero" class="hero-gradient min-h-screen flex items-center justify-center px-4 py-24">
<div class="max-w-4xl mx-auto text-center section-hidden" ref="sectionRef">
<div class="mb-6">
<UBadge color="primary" variant="soft" size="lg" class="mb-4">
Доступен для новых проектов
</UBadge>
</div>
<h1 class="text-5xl md:text-7xl font-bold text-white mb-6 leading-tight">
Алексей<br>
<span class="text-indigo-400">Разработчик</span>
</h1>
<p class="text-xl md:text-2xl text-gray-400 mb-4 font-medium">
Fullstack Developer · NestJS · Nuxt 3 · TypeScript
</p>
<p class="text-lg text-gray-500 max-w-2xl mx-auto mb-10">
Создаю современные веб-приложения с чистым кодом и отличным UX.
От MVP до production-ready решений.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<UButton
size="xl"
color="primary"
icon="i-heroicons-paper-airplane"
@click="scrollTo('#contact')"
>
Связаться
</UButton>
<UButton
size="xl"
variant="ghost"
color="neutral"
icon="i-heroicons-code-bracket"
@click="scrollTo('#projects')"
>
Проекты
</UButton>
</div>
</div>
</section>
</template>
<script setup lang="ts">
const sectionRef = ref<HTMLElement | null>(null);
function scrollTo(selector: string) {
document.querySelector(selector)?.scrollIntoView({ behavior: 'smooth' });
}
onMounted(() => {
if (sectionRef.value) {
sectionRef.value.classList.remove('section-hidden');
sectionRef.value.classList.add('section-visible');
}
});
</script>

View File

@@ -0,0 +1,91 @@
<template>
<section id="projects" class="py-24 px-4">
<div class="max-w-4xl mx-auto">
<div class="section-hidden" ref="sectionRef">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
Проекты
</h2>
<div class="w-16 h-1 bg-indigo-500 mb-10 rounded-full" />
<div class="grid md:grid-cols-2 gap-6">
<UCard
v-for="project in projects"
:key="project.title"
class="bg-gray-900 border-gray-800 hover:border-indigo-500 transition-colors duration-300"
>
<div class="space-y-3">
<div class="flex items-start justify-between">
<h3 class="text-white font-semibold text-lg">{{ project.title }}</h3>
<UBadge :color="project.statusColor" variant="soft" size="sm">
{{ project.status }}
</UBadge>
</div>
<p class="text-gray-400 text-sm leading-relaxed">{{ project.description }}</p>
<div class="flex flex-wrap gap-2 pt-1">
<UBadge
v-for="tech in project.stack"
:key="tech"
color="neutral"
variant="outline"
size="xs"
>
{{ tech }}
</UBadge>
</div>
</div>
</UCard>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
const sectionRef = ref<HTMLElement | null>(null);
const projects = [
{
title: 'E-Commerce Platform',
description: 'Полноценная платформа электронной коммерции с микросервисной архитектурой, real-time уведомлениями и AI-рекомендациями.',
stack: ['NestJS', 'Nuxt 3', 'PostgreSQL', 'Redis', 'Docker'],
status: 'Production',
statusColor: 'success' as const,
},
{
title: 'DevOps Dashboard',
description: 'Мониторинговый дашборд для CI/CD пайплайнов с метриками, алертами и историей деплоев.',
stack: ['Vue 3', 'Node.js', 'InfluxDB', 'Grafana', 'K8s'],
status: 'Production',
statusColor: 'success' as const,
},
{
title: 'AI Content Generator',
description: 'Инструмент генерации контента на основе OpenAI API с кастомными промптами и экспортом.',
stack: ['Nuxt 3', 'NestJS', 'OpenAI', 'TypeScript'],
status: 'Beta',
statusColor: 'warning' as const,
},
{
title: 'Real-Time Chat',
description: 'Мессенджер с WebSocket, поддержкой медиафайлов, сквозным шифрованием и мобильным приложением.',
stack: ['NestJS', 'Socket.io', 'React Native', 'MongoDB'],
status: 'WIP',
statusColor: 'info' as const,
},
];
onMounted(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.remove('section-hidden');
entry.target.classList.add('section-visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1 },
);
if (sectionRef.value) observer.observe(sectionRef.value);
});
</script>

View File

@@ -0,0 +1,62 @@
<template>
<section id="skills" class="py-24 px-4 bg-gray-950">
<div class="max-w-4xl mx-auto">
<div class="section-hidden" ref="sectionRef">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
Технологии
</h2>
<div class="w-16 h-1 bg-indigo-500 mb-10 rounded-full" />
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
<UCard
v-for="skill in skills"
:key="skill.name"
class="bg-gray-900 border-gray-800 hover:border-indigo-500 transition-colors duration-300 cursor-default"
>
<div class="flex flex-col items-center gap-2 py-2">
<span class="text-2xl">{{ skill.icon }}</span>
<span class="text-white font-medium text-sm">{{ skill.name }}</span>
<UBadge :color="skill.color" variant="soft" size="xs">
{{ skill.level }}
</UBadge>
</div>
</UCard>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
const sectionRef = ref<HTMLElement | null>(null);
const skills = [
{ name: 'NestJS', icon: '🏗️', level: 'Expert', color: 'error' as const },
{ name: 'Nuxt 3', icon: '💚', level: 'Expert', color: 'success' as const },
{ name: 'TypeScript', icon: '🔷', level: 'Expert', color: 'info' as const },
{ name: 'Node.js', icon: '🟩', level: 'Expert', color: 'success' as const },
{ name: 'PostgreSQL', icon: '🐘', level: 'Advanced', color: 'info' as const },
{ name: 'Redis', icon: '🔴', level: 'Advanced', color: 'error' as const },
{ name: 'Docker', icon: '🐳', level: 'Advanced', color: 'sky' as const },
{ name: 'Vue 3', icon: '💡', level: 'Expert', color: 'success' as const },
{ name: 'React', icon: '⚛️', level: 'Advanced', color: 'info' as const },
{ name: 'GraphQL', icon: '🔗', level: 'Intermediate', color: 'warning' as const },
{ name: 'Kubernetes', icon: '☸️', level: 'Intermediate', color: 'secondary' as const },
{ name: 'AWS', icon: '☁️', level: 'Intermediate', color: 'warning' as const },
];
onMounted(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.remove('section-hidden');
entry.target.classList.add('section-visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1 },
);
if (sectionRef.value) observer.observe(sectionRef.value);
});
</script>