-
Notifications
You must be signed in to change notification settings - Fork 0
Rebuild portfolio site with Nuxt 3, Tailwind CSS, Three.js, and FontAwesome #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Co-authored-by: FixeQyt <134298056+FixeQyt@users.noreply.github.com>
Co-authored-by: FixeQyt <134298056+FixeQyt@users.noreply.github.com>
…ects Co-authored-by: FixeQyt <134298056+FixeQyt@users.noreply.github.com>
Co-authored-by: FixeQyt <134298056+FixeQyt@users.noreply.github.com>
|
Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot Zastąp te |
…, add vibecode text, replace Threads with Discord modal Co-authored-by: FixeQyt <134298056+FixeQyt@users.noreply.github.com>
All requested changes implemented in commit 216cb7c! ✅ Changes made:
Screenshots: |
Co-authored-by: FixeQyt <134298056+FixeQyt@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR represents a complete rewrite of a portfolio site from vanilla HTML/CSS/JS to a modern Vue 3 stack using Nuxt 3, Tailwind CSS, and Three.js. The rebuild aims to simplify the codebase while adding enhanced visual effects including an interactive 3D particle background, glitch text effects, and typewriter animations.
Key Changes:
- Migrated from vanilla JavaScript to Nuxt 3 with Vue 3 composition API
- Replaced custom CSS with Tailwind CSS utility classes
- Added Three.js for 1500-particle interactive 3D background
- Integrated FontAwesome for consistent iconography
- Implemented Discord modal with copy-to-clipboard functionality
Reviewed changes
Copilot reviewed 17 out of 21 changed files in this pull request and generated 23 comments.
Show a summary per file
| File | Description |
|---|---|
package.json |
Defines project dependencies including Nuxt 3.15.1, Vue 3.5.13, Three.js, FontAwesome, and Tailwind CSS |
nuxt.config.ts |
Configures Nuxt with SSR disabled, FontAwesome CSS, and meta tags for SEO |
tailwind.config.js |
Sets up Tailwind with custom color palette (primary cyan, secondary pink) and Orbitron display font |
app.vue |
Root component that sets up HTML lang attribute and dark theme styling |
pages/index.vue |
Main page component orchestrating all sections with back-to-top button and scroll handling |
composables/usePortfolioData.ts |
Composable for fetching and caching portfolio data from JSON files |
components/ThreeBackground.vue |
Three.js particle system with 1500 particles and mouse parallax interaction |
components/HeroSection.vue |
Hero section with avatar, glitch text effect, typewriter animation, and floating code symbols |
components/AboutSection.vue |
About section displaying developer stats and achievements with hover effects |
components/SkillsSection.vue |
Skills display with intersection observer-triggered progress bar animations |
components/ProjectsSection.vue |
Projects grid with under construction message placeholder |
components/AppHeader.vue |
Glassmorphic navigation header with mobile menu toggle |
components/AppFooter.vue |
Footer with social links and Discord modal integration |
components/DiscordModal.vue |
Modal component for displaying Discord username with copy functionality |
plugins/fontawesome.ts |
FontAwesome plugin configuration registering brand and solid icons |
public/data.json |
Portfolio data including developer info, skills, socials, and achievements |
public/projects.json |
Projects data structure with under construction flag |
.github/workflows/deploy.yml |
GitHub Actions workflow updated to use Node.js and npm instead of Bun |
.gitignore |
Git ignore rules for Nuxt build artifacts and dependencies |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| </div> | ||
|
|
||
| <!-- Mobile menu button --> | ||
| <button @click="mobileMenuOpen = !mobileMenuOpen" class="md:hidden text-gray-300 hover:text-primary"> |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
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">| <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" | |
| > |
| <div class="absolute inset-0 overflow-hidden pointer-events-none opacity-30"> | ||
| <div v-for="(symbol, i) in codeSymbols" :key="i" | ||
| class="absolute text-primary/40 font-mono text-2xl md:text-4xl animate-float" | ||
| :style="{ | ||
| left: `${Math.random() * 100}%`, | ||
| top: `${Math.random() * 100}%`, | ||
| animationDelay: `${Math.random() * 5}s`, | ||
| animationDuration: `${Math.random() * 15 + 10}s` | ||
| }"> | ||
| {{ symbol }} | ||
| </div> | ||
| </div> |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The animation relies on randomly generated positions and durations for code symbols. On each render, these values will be recalculated, causing the symbols to jump positions.
The random values should be computed once and stored:
const codeSymbols = ref([
'{ }', '< >', '[ ]', '( )', '/>', '=>', '...', '&&', '||', '??', '</>'
].map(symbol => ({
text: symbol,
left: Math.random() * 100,
top: Math.random() * 100,
delay: Math.random() * 5,
duration: Math.random() * 15 + 10
})))Then in the template:
<div v-for="(symbol, i) in codeSymbols" :key="i"
class="..."
:style="{
left: `${symbol.left}%`,
top: `${symbol.top}%`,
animationDelay: `${symbol.delay}s`,
animationDuration: `${symbol.duration}s`
}">
{{ symbol.text }}
</div>| <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"> | ||
| <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> | ||
|
|
||
| <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> |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
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.
| } catch (err) { | ||
| console.error('Failed to copy:', err) | ||
| } | ||
| } |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
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>| } | |
| } | |
| 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) | |
| }) | |
| }) |
| <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"> | ||
| <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> |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
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">| "@fortawesome/free-solid-svg-icons": "^7.1.0", | ||
| "@fortawesome/vue-fontawesome": "^3.1.2", | ||
| "@types/three": "^0.181.0", | ||
| "@vueuse/core": "^14.1.0", |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The @vueuse/core dependency is declared but never used anywhere in the codebase. This adds unnecessary weight to the bundle and increases installation time.
Remove this unused dependency from package.json.
| "@vueuse/core": "^14.1.0", |
| <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"> |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
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()
}
}| onUnmounted(() => { | ||
| if (animationId) { | ||
| cancelAnimationFrame(animationId) | ||
| } | ||
| if (renderer) { | ||
| renderer.dispose() | ||
| } | ||
| }) |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate onUnmounted hook registration. Vue components should only have one onUnmounted hook per component. The second one (lines 112-119) will override or conflict with the first one (lines 106-109).
Consolidate all cleanup logic into a single onUnmounted hook outside of onMounted.
| onMounted(() => { | ||
| const observer = new IntersectionObserver((entries) => { | ||
| entries.forEach(entry => { | ||
| if (entry.isIntersecting && !inView.value) { | ||
| inView.value = true | ||
| animateLevels() | ||
| } | ||
| }) | ||
| }, { threshold: 0.2 }) | ||
|
|
||
| const section = document.getElementById('skills') | ||
| if (section) observer.observe(section) | ||
| }) |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Memory leak: The IntersectionObserver is never disconnected when the component unmounts. This will keep references to DOM elements and prevent garbage collection.
Add cleanup in onUnmounted:
let observer = null
onMounted(() => {
observer = new IntersectionObserver((entries) => {
// ... existing logic
}, { threshold: 0.2 })
const section = document.getElementById('skills')
if (section) observer.observe(section)
})
onUnmounted(() => {
if (observer) {
observer.disconnect()
}
// ... existing cleanup
})| const displayedLevels = ref({}) | ||
| const timers = ref([]) | ||
|
|
||
| const getCategoryIcon = (category) => { | ||
| const icons = { | ||
| 'Frontend Development': '🎨', | ||
| 'Backend Development': '⚙️', | ||
| 'Mobile Development': '📱', | ||
| 'Tools & DevOps': '🛠️' | ||
| } | ||
| return icons[category] || '💻' | ||
| } | ||
|
|
||
| const adjustColor = (color, amount) => { | ||
| const num = parseInt(color.replace('#', ''), 16) | ||
| const r = Math.min(255, (num >> 16) + amount) | ||
| const g = Math.min(255, ((num >> 8) & 0x00FF) + amount) | ||
| const b = Math.min(255, (num & 0x0000FF) + amount) | ||
| return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}` | ||
| } | ||
|
|
||
| onMounted(() => { | ||
| const observer = new IntersectionObserver((entries) => { | ||
| entries.forEach(entry => { | ||
| if (entry.isIntersecting && !inView.value) { | ||
| inView.value = true | ||
| animateLevels() | ||
| } | ||
| }) | ||
| }, { threshold: 0.2 }) | ||
|
|
||
| const section = document.getElementById('skills') | ||
| if (section) observer.observe(section) | ||
| }) | ||
|
|
||
| onUnmounted(() => { | ||
| // Clean up all timers | ||
| timers.value.forEach(timer => clearInterval(timer)) | ||
| timers.value = [] | ||
| }) | ||
|
|
||
| const animateLevels = () => { | ||
| if (!props.data?.skills) return | ||
|
|
||
| props.data.skills.forEach(category => { | ||
| category.technologies.forEach(tech => { | ||
| const key = `${category.category}-${tech.name}` | ||
| let current = 0 | ||
| const target = tech.level | ||
| const increment = target / 50 | ||
|
|
||
| const timer = setInterval(() => { | ||
| current += increment | ||
| if (current >= target) { | ||
| displayedLevels.value[key] = target | ||
| clearInterval(timer) | ||
| timers.value = timers.value.filter(t => t !== timer) | ||
| } else { | ||
| displayedLevels.value[key] = Math.floor(current) | ||
| } | ||
| }, 20) | ||
|
|
||
| timers.value.push(timer) | ||
| }) | ||
| }) | ||
| } |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The displayedLevels reactive state is defined but never used in the template. The animation logic references it but the template only uses tech.level directly in the style binding. This appears to be leftover code from a previous implementation where percentages were displayed.
Either remove the unused displayedLevels state and animateLevels function, or update the template to use the animated values.
* chore: initial commit for PR #2 * Sortout pages * chore: add GitHub Actions workflow for deployment * Rebuild portfolio site with Nuxt 3, Tailwind CSS, Three.js, and FontAwesome (#3) * Initial plan * Update plan: Use Nuxt and Tailwind CSS Co-authored-by: FixeQyt <134298056+FixeQyt@users.noreply.github.com> * Convert site to Nuxt with Tailwind CSS - simpler design Co-authored-by: FixeQyt <134298056+FixeQyt@users.noreply.github.com> * Add Three.js particles, animations, FontAwesome icons, and visual effects Co-authored-by: FixeQyt <134298056+FixeQyt@users.noreply.github.com> * Fix memory leaks in Three.js and animation components Co-authored-by: FixeQyt <134298056+FixeQyt@users.noreply.github.com> * Replace floating elements with code symbols, remove skill percentages, add vibecode text, replace Threads with Discord modal Co-authored-by: FixeQyt <134298056+FixeQyt@users.noreply.github.com> * Update vibecode description to clarify AI-assisted coding Co-authored-by: FixeQyt <134298056+FixeQyt@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: FixeQyt <134298056+FixeQyt@users.noreply.github.com> * Final changes before deploy * Forgot to change branch 💀 --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>



Complete rewrite from vanilla HTML/CSS/JS to modern Vue stack per user requirements for simpler code with enhanced visual effects.
Architecture
Components
Core Layout
ThreeBackground.vue: Mouse-reactive particle system with proper cleanupAppHeader.vue: Glassmorphic nav with mobile menuHeroSection.vue: Glitch text effect + typewriter animation loop + floating code symbolsSkillsSection.vue: Intersection observer-triggered progress animations (percentages removed)AboutSection.vue: Stats grid with hover states + vibecode mentionProjectsSection.vue: Project cards with gradient overlaysAppFooter.vue: Social links with icon integrationDiscordModal.vue: Custom modal for Discord with username display and copy functionalityData Flow
Visual Effects
{ },< >,[ ],=>) instead of generic circlesContact Integration
fixeq.devAbout Section
Build Output
.output/publicCode Reduction
Before: 10+ CSS files, multiple JS modules, manual DOM manipulation
After: 8 SFC components, composable data layer, declarative templates
Security
CodeQL scan: 0 vulnerabilities
Screenshots
Hero Section with Code Symbols:

Discord Modal:

Full Site:

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.