-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1ada65f
commit 73281b1
Showing
20 changed files
with
2,304 additions
and
83 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,164 @@ | ||
<script setup lang="ts"> | ||
// import { projects } from "~/data/projects"; | ||
import socialMedia from "~/data/social-media"; | ||
import { projects as projectsObject } from "~/data/projects"; | ||
import { boxIconString } from "~/data/constants"; | ||
const route = useRoute(); | ||
const isHomePage = computed(() => route.name === "home" || route.path === "/"); | ||
const quickLinks = useNuxtApp() | ||
.$router.getRoutes() | ||
.filter((route) => route.path !== "/:catchAll(.*)") | ||
.map((route) => ({ | ||
label: route.path === "/" ? "Home" : convertToTitleCase(route.path), | ||
href: route.path, | ||
})) | ||
.reverse(); | ||
const projects = projectsObject.map(({ website, title }) => ({ | ||
href: website, | ||
label: title, | ||
})); | ||
useSeoMeta({ | ||
title: "sabeer bikba portfolio", | ||
description: "//TODO: add description", | ||
}); | ||
useHead({ | ||
htmlAttrs: { | ||
lang: "en", | ||
}, | ||
meta: [ | ||
{ | ||
name: "viewport", | ||
content: "width=device-width, initial-scale=1.0", | ||
}, | ||
], | ||
link: [ | ||
{ | ||
rel: "icon", | ||
type: "image/svg+xml", | ||
href: boxIconString, | ||
}, | ||
], | ||
script: [ | ||
{ | ||
type: "application/ld+json", | ||
children: JSON.stringify({ | ||
"@context": "https://schema.org", | ||
"@type": "CreativeWork", | ||
name: "Featured Works", | ||
description: | ||
"Explore my featured projects and creations. See live previews, project details, and GitHub repo visuals.", | ||
creator: { | ||
"@type": "Person", | ||
name: "Sabeer Bikba", | ||
}, | ||
workExample: projectsObject.map(({ title, website, icon, about }) => ({ | ||
"@type": "WebPage", | ||
name: title, | ||
url: website, | ||
image: icon, | ||
about: about, | ||
})), | ||
}), | ||
}, | ||
], | ||
}); | ||
</script> | ||
|
||
<template> | ||
<div> | ||
<NuxtRouteAnnouncer /> | ||
<NuxtWelcome /> | ||
<main class="w-full h-full bg-white bg-dot-black/[0.4] relative"> | ||
<div | ||
class="absolute pointer-events-none inset-0 bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)]" | ||
></div> | ||
<div | ||
class="relative z-20 bg-clip-text text-transparent bg-gradient-to-b from-neutral-200 to-neutral-500" | ||
> | ||
<NuxtPage /> | ||
</div> | ||
</main> | ||
<footer | ||
v-if="isHomePage" | ||
class="border-t border-neutral-300 px-8 py-20 bg-white" | ||
> | ||
<div | ||
class="max-w-7xl mx-auto text-sm text-neutral-500 flex sm:flex-row flex-col justify-between items-start" | ||
> | ||
<div> | ||
<div class="mr-4 md:flex mb-1.5"> | ||
<NuxtLink | ||
to="/" | ||
aria-label="Home page" | ||
class="center max-md:justify-normal space-x-2 text-2xl font-bold text-center text-neutral-600 selection:bg-emerald-500 mr-10 py-0" | ||
> | ||
<IconLogo /> | ||
</NuxtLink> | ||
</div> | ||
<div class="text-base">Sabeer Bikba</div> | ||
<div class="font-medium text-base text-neutral-600"> | ||
Turning Ideas into Reality with Code | ||
</div> | ||
</div> | ||
|
||
<!-- Navigation --> | ||
<div class="grid grid-cols-3 gap-10 items-start mt-10 md:mt-0"> | ||
<div | ||
v-for="({ id, title, links }, index) in [ | ||
{ | ||
id: 'quick-links', | ||
title: 'Quick Links', | ||
links: quickLinks, | ||
}, | ||
{ | ||
id: 'social-media', | ||
title: 'Social Media', | ||
links: socialMedia, | ||
}, | ||
{ | ||
id: 'projects', | ||
title: 'Projects', | ||
links: projects, | ||
}, | ||
]" | ||
:key="id" | ||
> | ||
<nav :aria-labelledby="id"> | ||
<h2 :id="id" class="sr-only">{{ title }}</h2> | ||
<ul class="flex justify-center space-y-4 flex-col mt-4"> | ||
<li v-for="{ href, label } in links" :key="label"> | ||
<NuxtLink | ||
v-if="index === 0" | ||
:to="href" | ||
:aria-label="`Navigate to ${label} page`" | ||
class="transition-colors hover:text-foreground/80 text-foreground/60" | ||
> | ||
{{ label }} | ||
</NuxtLink> | ||
<a | ||
v-else | ||
:href="href" | ||
:aria-label="`${ | ||
id === 'social-media' | ||
? 'Link to' | ||
: 'Visit the website for project' | ||
} ${label}`" | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
class="transition-colors hover:text-foreground/80 text-foreground/60" | ||
> | ||
{{ label }} | ||
</a> | ||
</li> | ||
</ul> | ||
</nav> | ||
</div> | ||
</div> | ||
</div> | ||
</footer> | ||
</div> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
<script setup lang="ts"> | ||
interface Props { | ||
words: string[]; | ||
duration?: number; | ||
class?: string; | ||
} | ||
const props = withDefaults(defineProps<Props>(), { | ||
duration: 3000, | ||
class: "", | ||
}); | ||
defineEmits(["animationStart", "animationComplete"]); | ||
const currentWord = ref(props.words[0]); | ||
const isVisible = ref(true); | ||
const timeoutId = ref<number | null>(null); | ||
function startAnimation() { | ||
isVisible.value = false; | ||
setTimeout(() => { | ||
const currentIndex = props.words.indexOf(currentWord.value); | ||
const nextWord = props.words[currentIndex + 1] || props.words[0]; | ||
currentWord.value = nextWord; | ||
isVisible.value = true; | ||
}, 600); | ||
} | ||
const splitWords = computed(() => { | ||
return currentWord.value.split(" ").map((word) => ({ | ||
word, | ||
letters: word.split(""), | ||
})); | ||
}); | ||
function startTimeout() { | ||
timeoutId.value = window.setTimeout(() => { | ||
startAnimation(); | ||
}, props.duration); | ||
} | ||
onMounted(() => { | ||
startTimeout(); | ||
}); | ||
onBeforeUnmount(() => { | ||
if (timeoutId.value) { | ||
clearTimeout(timeoutId.value); | ||
} | ||
}); | ||
watch(isVisible, (newValue) => { | ||
if (newValue) { | ||
startTimeout(); | ||
} | ||
}); | ||
</script> | ||
|
||
|
||
<template> | ||
<div class="relative inline-block px-2"> | ||
<Transition @after-enter="$emit('animationStart')" @after-leave="$emit('animationComplete')"> | ||
<div v-show="isVisible" :class="[ | ||
'relative z-10 inline-block text-left text-neutral-600', | ||
props.class, | ||
]"> | ||
<template v-for="(wordObj, wordIndex) in splitWords" :key="wordObj.word + wordIndex"> | ||
<span class="inline-block whitespace-nowrap opacity-0" :style="{ | ||
animation: `fadeInWord 0.3s ease forwards`, | ||
animationDelay: `${wordIndex * 0.3}s`, | ||
}"> | ||
<span v-for="(letter, letterIndex) in wordObj.letters" :key="wordObj.word + letterIndex" | ||
class="inline-block opacity-0" :style="{ | ||
animation: `fadeInLetter 0.2s ease forwards`, | ||
animationDelay: `${wordIndex * 0.3 + letterIndex * 0.05}s`, | ||
}"> | ||
{{ letter }} | ||
</span> | ||
<span class="inline-block"> </span> | ||
</span> | ||
</template> | ||
</div> | ||
</Transition> | ||
</div> | ||
</template> | ||
|
||
<style> | ||
@keyframes fadeInWord { | ||
0% { | ||
opacity: 0; | ||
transform: translateY(10px); | ||
filter: blur(8px); | ||
} | ||
100% { | ||
opacity: 1; | ||
transform: translateY(0); | ||
filter: blur(0); | ||
} | ||
} | ||
@keyframes fadeInLetter { | ||
0% { | ||
opacity: 0; | ||
transform: translateY(10px); | ||
filter: blur(8px); | ||
} | ||
100% { | ||
opacity: 1; | ||
transform: translateY(0); | ||
filter: blur(0); | ||
} | ||
} | ||
.v-enter-active { | ||
animation: enterWord 0.6s ease-in-out forwards; | ||
} | ||
.v-leave-active { | ||
animation: leaveWord 0.6s ease-in-out forwards; | ||
} | ||
@keyframes enterWord { | ||
0% { | ||
opacity: 0; | ||
transform: translateY(10px); | ||
} | ||
100% { | ||
opacity: 1; | ||
transform: translateY(0); | ||
} | ||
} | ||
@keyframes leaveWord { | ||
0% { | ||
opacity: 1; | ||
transform: scale(1); | ||
filter: blur(0); | ||
} | ||
100% { | ||
opacity: 0; | ||
transform: scale(2); | ||
filter: blur(8px); | ||
} | ||
} | ||
</style> |
Oops, something went wrong.