Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v1
- name: Setup Node
uses: actions/setup-node@v4
with:
bun-version: latest
node-version: '20'

- name: Install dependencies
run: bun install
run: npm install

- name: Build
run: bun run build
- name: Generate static site
run: npm run generate

- name: Deploy to GitHub Pages
if: github.ref == 'refs/heads/vue-recreation'
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist
publish_dir: ./.output/public
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# FixeQ Portfolio - Git Ignore Rules

# Nuxt
.nuxt
.output
dist
node_modules
.env

# Development files
.vscode/
.idea/
Expand Down
15 changes: 15 additions & 0 deletions app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<template>
<NuxtPage />
</template>

<script setup>
useHead({
htmlAttrs: {
lang: 'en',
class: 'scroll-smooth'
},
bodyAttrs: {
class: 'bg-dark text-white'
}
})
</script>
60 changes: 60 additions & 0 deletions components/AboutSection.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<template>
<section id="about" class="py-20 px-6 relative">
<div class="container mx-auto max-w-5xl">
<h2 class="text-5xl font-display font-bold text-center mb-4">
About <span class="text-transparent bg-clip-text bg-gradient-to-r from-primary to-secondary">Me</span>
</h2>
<p class="text-center text-gray-400 mb-12">Get to know me better</p>

<div class="bg-gradient-to-br from-white/10 to-white/5 rounded-3xl p-8 md:p-12 border border-white/20 shadow-2xl backdrop-blur-sm relative overflow-hidden group">
<!-- Animated border gradient -->
<div class="absolute inset-0 bg-gradient-to-r from-primary via-secondary to-primary opacity-0 group-hover:opacity-20 blur-xl transition-opacity duration-500"></div>

<div class="relative z-10">
<p class="text-lg md:text-xl text-gray-300 leading-relaxed mb-4 text-center max-w-3xl mx-auto">
{{ data?.developer?.description }}
</p>
<p class="text-base text-gray-400 text-center mb-10 italic">
Also loves to <span class="text-primary font-semibold">vibecode</span> – coding with AI assistance and good vibes ✨🤖
</p>

<div class="grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-8">
<div v-for="(stat, idx) in stats" :key="stat.label"
class="text-center p-6 rounded-2xl bg-gradient-to-br from-white/5 to-transparent border border-white/10 hover:border-primary/50 transition-all duration-300 hover:scale-105 hover:shadow-xl hover:shadow-primary/20 group/stat"
:style="{ animationDelay: `${idx * 100}ms` }">
<div class="text-4xl md:text-5xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-primary to-secondary mb-3 group-hover/stat:scale-110 transition-transform">
{{ stat.value }}
</div>
<div class="text-sm md:text-base text-gray-400 group-hover/stat:text-gray-300 transition-colors">{{ stat.label }}</div>
</div>
</div>

<!-- Achievements -->
<div class="mt-12 grid md:grid-cols-2 gap-4">
<div v-for="achievement in data?.achievements" :key="achievement.title"
class="flex items-start gap-4 p-4 rounded-xl bg-white/5 hover:bg-white/10 border border-white/5 hover:border-primary/30 transition-all duration-300 group/achievement">
<div class="text-3xl">{{ achievement.icon }}</div>
<div>
<h4 class="font-bold text-lg mb-1 group-hover/achievement:text-primary transition-colors">{{ achievement.title }}</h4>
<p class="text-sm text-gray-400">{{ achievement.description }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</template>

<script setup>
const props = defineProps({
data: Object
})

const stats = computed(() => [
{ label: 'Experience', value: props.data?.developer?.experience },
{ label: 'Projects', value: props.data?.developer?.projectsCompleted },
{ label: 'Technologies', value: props.data?.developer?.technologiesMastered },
{ label: 'Location', value: '🇵🇱' }
])
</script>
72 changes: 72 additions & 0 deletions components/AppFooter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<template>
<footer id="contact" class="py-20 px-6 bg-gradient-to-t from-white/5 to-transparent border-t border-white/10 relative overflow-hidden">
<!-- Animated background -->
<div class="absolute inset-0 bg-[radial-gradient(circle_at_center,#00D9FF10_0%,transparent_70%)] animate-pulse-slow"></div>

<div class="container mx-auto max-w-4xl text-center relative z-10">
<h2 class="text-5xl font-display font-bold mb-4">
Get in <span class="text-transparent bg-clip-text bg-gradient-to-r from-primary to-secondary">Touch</span>
</h2>

<p class="text-xl text-gray-400 mb-12">
Interested in working together? Let's connect!
</p>

<div class="flex flex-wrap gap-4 justify-center mb-12">
<component
v-for="social in data?.socials"
:key="social.name"
:is="social.name === 'Discord' ? 'button' : 'a'"
:href="social.name !== 'Discord' ? social.url : undefined"
:target="social.name !== 'Discord' ? '_blank' : undefined"
:rel="social.name !== 'Discord' ? 'noopener' : undefined"
@click="social.name === 'Discord' ? openDiscordModal(social) : null"
class="group relative px-8 py-4 bg-gradient-to-r from-white/5 to-white/10 hover:from-white/10 hover:to-white/20 border border-white/10 hover:border-primary/50 rounded-xl transition-all duration-300 flex items-center gap-3 hover:scale-105 hover:shadow-xl hover:shadow-primary/20">
<div class="absolute inset-0 rounded-xl bg-gradient-to-r opacity-0 group-hover:opacity-10 blur-xl transition-opacity"
:style="{ background: social.color }"></div>
<font-awesome-icon :icon="['fab', social.icon]" class="w-6 h-6 relative z-10 group-hover:scale-110 transition-transform" />
<span class="relative z-10 font-medium group-hover:text-primary transition-colors">{{ social.name }}</span>
</component>
</div>

<!-- Discord Modal -->
<DiscordModal
:isOpen="showDiscordModal"
:username="discordUsername"
:profileUrl="discordProfileUrl"
@close="showDiscordModal = false"
/>

<div class="pt-8 border-t border-white/10">
<p class="text-gray-500 text-sm mb-2">
&copy; 2024 {{ data?.developer?.name }}
</p>
<p class="text-gray-600 text-xs">
Built with 💙 using <span class="text-primary">Nuxt 3</span>, <span class="text-secondary">Tailwind CSS</span> & <span class="text-primary">Three.js</span>
</p>
</div>
</div>
</footer>
</template>

<script setup>
const props = defineProps({
data: Object
})

const showDiscordModal = ref(false)
const discordUsername = ref('')
const discordProfileUrl = ref('')

const openDiscordModal = (social) => {
discordUsername.value = social.username || 'fixeq.dev'
discordProfileUrl.value = social.url
showDiscordModal.value = true
}
</script>

<style scoped>
.animate-pulse-slow {
animation: pulse 8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
</style>
45 changes: 45 additions & 0 deletions components/AppHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<template>
<header class="fixed top-0 w-full bg-dark/60 backdrop-blur-xl border-b border-white/10 z-50 shadow-lg">
<nav class="container mx-auto px-6 py-4">
<div class="flex items-center justify-between">
<a href="#home" class="text-2xl md:text-3xl font-display font-bold">
<span class="text-transparent bg-clip-text bg-gradient-to-r from-primary to-secondary hover:from-secondary hover:to-primary transition-all duration-300">
FixeQ
</span>
</a>
<div class="hidden md:flex gap-8">
<a v-for="link in links" :key="link.href" :href="link.href"
class="relative text-gray-300 hover:text-primary transition-colors duration-300 font-medium group">
{{ link.label }}
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-primary to-secondary group-hover:w-full transition-all duration-300"></span>
</a>
</div>

<!-- Mobile menu button -->
<button @click="mobileMenuOpen = !mobileMenuOpen" class="md:hidden text-gray-300 hover:text-primary">
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing keyboard accessibility for the mobile menu button. The button should be accessible via keyboard navigation and should toggle the menu on Enter/Space key press. While the click handler will work for keyboard users due to button semantics, screen readers would benefit from aria attributes.

Add accessibility attributes:

<button 
  @click="mobileMenuOpen = !mobileMenuOpen" 
  class="md:hidden text-gray-300 hover:text-primary"
  :aria-expanded="mobileMenuOpen"
  aria-label="Toggle mobile menu">
Suggested change
<button @click="mobileMenuOpen = !mobileMenuOpen" class="md:hidden text-gray-300 hover:text-primary">
<button
@click="mobileMenuOpen = !mobileMenuOpen"
class="md:hidden text-gray-300 hover:text-primary"
:aria-expanded="mobileMenuOpen"
aria-label="Toggle mobile menu"
>

Copilot uses AI. Check for mistakes.
<font-awesome-icon :icon="mobileMenuOpen ? 'times' : 'bars'" class="w-6 h-6" />
</button>
</div>

<!-- Mobile menu -->
<div v-show="mobileMenuOpen" class="md:hidden mt-4 pb-4 space-y-4">
<a v-for="link in links" :key="link.href" :href="link.href"
@click="mobileMenuOpen = false"
class="block text-gray-300 hover:text-primary transition-colors duration-300 font-medium">
{{ link.label }}
</a>
</div>
</nav>
</header>
</template>

<script setup>
const links = [
{ href: '#about', label: 'About' },
{ href: '#skills', label: 'Skills' },
{ href: '#projects', label: 'Projects' },
{ href: '#contact', label: 'Contact' }
]

const mobileMenuOpen = ref(false)
</script>
86 changes: 86 additions & 0 deletions components/DiscordModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<template>
<Teleport to="body">
<Transition name="modal">
<div v-if="isOpen" @click="closeModal" class="fixed inset-0 z-[100] flex items-center justify-center bg-black/70 backdrop-blur-sm">
<div @click.stop class="bg-gradient-to-br from-[#5865F2]/20 to-[#5865F2]/10 backdrop-blur-xl border border-[#5865F2]/30 rounded-2xl p-8 max-w-md w-full mx-4 shadow-2xl shadow-[#5865F2]/20">
Comment on lines +4 to +5
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Using @click.stop on the modal content prevents event propagation, but the modal backdrop click handler relies on clicks reaching the outer div. While this works as intended, it's better to explicitly check the event target to avoid potential issues and make the intent clearer:

<div v-if="isOpen" @click="handleBackdropClick" class="...">
  <div class="...">
const handleBackdropClick = (e) => {
  if (e.target === e.currentTarget) {
    closeModal()
  }
}

Copilot uses AI. Check for mistakes.
<div class="flex justify-between items-center mb-6">
<h3 class="text-2xl font-bold text-white flex items-center gap-2">
<font-awesome-icon :icon="['fab', 'discord']" class="text-[#5865F2]" />
Discord
</h3>
<button @click="closeModal" class="text-gray-400 hover:text-white transition">
<font-awesome-icon icon="times" class="w-6 h-6" />
</button>
</div>
Comment on lines +4 to +14
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The modal lacks ARIA attributes for proper screen reader support. Screen readers won't announce this as a dialog or communicate its purpose.

Add appropriate ARIA attributes:

<div 
  v-if="isOpen" 
  @click="closeModal" 
  class="fixed inset-0 z-[100] flex items-center justify-center bg-black/70 backdrop-blur-sm"
  role="dialog"
  aria-modal="true"
  aria-labelledby="discord-modal-title">
  <div @click.stop class="...">
    <h3 id="discord-modal-title" class="text-2xl font-bold text-white flex items-center gap-2">

Copilot uses AI. Check for mistakes.

<div class="space-y-4">
<div class="bg-black/30 rounded-xl p-4 border border-white/10">
<p class="text-sm text-gray-400 mb-2">Username</p>
<div class="flex items-center justify-between">
<p class="text-xl font-mono text-white">{{ username }}</p>
<button @click="copyUsername" class="px-3 py-1 bg-[#5865F2]/20 hover:bg-[#5865F2]/30 text-[#5865F2] rounded-lg transition text-sm">
{{ copied ? 'Copied!' : 'Copy' }}
</button>
</div>
</div>

<a :href="profileUrl" target="_blank" rel="noopener"
class="block w-full py-3 bg-[#5865F2] hover:bg-[#5865F2]/90 text-white rounded-lg font-semibold text-center transition shadow-lg shadow-[#5865F2]/30">
Open Discord Profile
</a>
</div>
</div>
</div>
</Transition>
</Teleport>
Comment on lines +1 to +35
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing focus trap in modal. When the modal is open, keyboard users can still tab to elements behind the modal, which violates accessibility best practices. The modal should trap focus within itself and return focus to the triggering element when closed.

Consider using Vue's focus management or a library like focus-trap to ensure keyboard navigation is contained within the modal when open.

Copilot uses AI. Check for mistakes.
</template>

<script setup>
const props = defineProps({
isOpen: Boolean,
username: String,
profileUrl: String
})

const emit = defineEmits(['close'])

const copied = ref(false)

const closeModal = () => {
emit('close')
}

const copyUsername = async () => {
try {
await navigator.clipboard.writeText(props.username)
copied.value = true
setTimeout(() => {
copied.value = false
}, 2000)
} catch (err) {
console.error('Failed to copy:', err)
}
}
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The modal backdrop should close on Escape key press for better user experience. Currently, users can only close the modal by clicking the X button or clicking outside.

Add keyboard event handler:

<script setup>
// ... existing code

onMounted(() => {
  const handleEscape = (e) => {
    if (e.key === 'Escape' && props.isOpen) {
      closeModal()
    }
  }
  window.addEventListener('keydown', handleEscape)
  
  onUnmounted(() => {
    window.removeEventListener('keydown', handleEscape)
  })
})
</script>
Suggested change
}
}
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
const handleEscape = (e) => {
if (e.key === 'Escape' && props.isOpen) {
closeModal()
}
}
window.addEventListener('keydown', handleEscape)
onUnmounted(() => {
window.removeEventListener('keydown', handleEscape)
})
})

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +63
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling for clipboard API. The navigator.clipboard.writeText call can fail (e.g., on insecure contexts, permissions denied), but the error is only logged to console. Users should receive visual feedback when the copy operation fails.

Update error handling:

const copyUsername = async () => {
  try {
    await navigator.clipboard.writeText(props.username)
    copied.value = true
    setTimeout(() => {
      copied.value = false
    }, 2000)
  } catch (err) {
    console.error('Failed to copy:', err)
    // Show error feedback to user
    copied.value = 'Error'
    setTimeout(() => {
      copied.value = false
    }, 2000)
  }
}

And update the button text: {{ copied === 'Error' ? 'Failed' : copied ? 'Copied!' : 'Copy' }}

Copilot uses AI. Check for mistakes.
</script>

<style scoped>
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s ease;
}

.modal-enter-from,
.modal-leave-to {
opacity: 0;
}

.modal-enter-active > div,
.modal-leave-active > div {
transition: transform 0.3s ease;
}

.modal-enter-from > div,
.modal-leave-to > div {
transform: scale(0.9);
}
</style>
Loading
Loading