-
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
Changes from all commits
bf71131
1a67cd8
e1a8584
3494c65
467b6f6
216cb7c
fc532fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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> |
| 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> |
| 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"> | ||
| © 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> |
| 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"> | ||
| <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> | ||
| 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
|
||||||||||||||||||||||||||||||||||||
| <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
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| <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
|
||||||||||||||||||||||||||||||||||||
| </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) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
| } | |
| } | |
| 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
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 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' }}
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: