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
2 changes: 2 additions & 0 deletions src/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ declare global {
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useInjectedScroll: typeof import('./composables/useInjectedScroll')['useInjectedScroll']
const useInjectedSidebar: typeof import('./composables/useInjectedSidebar')['useInjectedSidebar']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
Expand Down Expand Up @@ -508,6 +509,7 @@ declare module 'vue' {
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
readonly useInjectedScroll: UnwrapRef<typeof import('./composables/useInjectedScroll')['useInjectedScroll']>
readonly useInjectedSidebar: UnwrapRef<typeof import('./composables/useInjectedSidebar')['useInjectedSidebar']>
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
Expand Down
1 change: 1 addition & 0 deletions src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ declare module 'vue' {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ScrollIndicator: typeof import('./components/partials/ScrollIndicator.vue')['default']
SidebarOverlay: typeof import('./components/SidebarOverlay.vue')['default']
SocialCard: typeof import('./components/partials/about/SocialCard.vue')['default']
SocialLink: typeof import('./components/partials/about/SocialLink.vue')['default']
ToggleButton: typeof import('./components/partials/ToggleButton.vue')['default']
Expand Down
11 changes: 7 additions & 4 deletions src/components/AppLogo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { breakpointsTailwind } from '@vueuse/core'
import { useInjectedScroll } from '~/composables/useInjectedScroll'

const { isOnTop, isOnBottom } = useInjectedScroll()
const { isSidebarOpen } = useInjectedSidebar()

const notTopBottom = computed(() => {
return !isOnTop.value && !isOnBottom.value
})
Expand All @@ -21,8 +23,8 @@ const onLgBreakpoint = computed(() => {
</script>

<template>
<RouterLink to="/" flex gap="12" pointer-events="auto" select="none" class="swipe" :class="onLgBreakpoint ? `scale-80` : `to-bottom`">
<h1 :data-scrolled="onLgBreakpoint" aria-label="SIkhlas" aria-description="Sajid Ikhlas Personal Website Title Logo" truncate tracking="wider" font="extralight" text="5xl nowrap" select="none" class="title">
<RouterLink to="/" pointer-events="auto" select="none" class="swipe" :class="onLgBreakpoint || isSidebarOpen ? `scale-90 sm:scale-100 lg:scale-80` : `*:scale-125 *:sm:scale-100 to-bottom`">
<h1 :data-scrolled="onLgBreakpoint" aria-label="SIkhlas" aria-description="Sajid Ikhlas Personal Website Title Logo" truncate tracking="wider" transition="composite" font="extralight" text="3xl sm:5xl nowrap" select="none" class="title">
<span>S<span tracking="widest" class="widen">I</span>k</span>hlas
</h1>
</RouterLink>
Expand All @@ -37,7 +39,8 @@ const onLgBreakpoint = computed(() => {
}

.to-bottom {
transform: translateY(calc(100dvh - 100% - 13rem)) translateX(calc(100vw - 100% - 2rem - 1rem - 1rem / 2));
transform: translateY(calc(100dvh - 100% - 13rem - 1rem))
translateX(calc(100vw - 100% - 2rem - 1rem - 1rem / 2 - 1rem));
}
@media (min-width: 640px) {
.to-bottom {
Expand All @@ -47,7 +50,7 @@ const onLgBreakpoint = computed(() => {
/* 784px instead of 768px for flex wrapping footer content */
@media (min-width: 768px) {
.to-bottom {
transform: translateY(calc(100dvh - 100% - 7.5rem));
transform: translateY(calc(100dvh - 100% - 7.5rem - 1rem / 2));
}
}

Expand Down
37 changes: 20 additions & 17 deletions src/components/HeaderOverlay.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { LabelHrefItem } from '~/types'
import { breakpointsTailwind } from '@vueuse/core'
import { useInjectedScroll } from '~/composables/useInjectedScroll'
import { HEADER } from '~/constants'

const { isOnTop, isOnBottom } = useInjectedScroll()
const { isSidebarOpen } = useInjectedSidebar()

const route = useRoute()

Expand Down Expand Up @@ -35,31 +35,30 @@ const notTopBottom = computed(() => {
return !isOnTop.value && !isOnBottom.value
})

const breakpoints = useBreakpoints(breakpointsTailwind)
const isLg = breakpoints.lg

const returnInert = computed(() => {
if (!isLg.value)
return !isOnBottom.value
else return false
const returnSectionClass = computed(() => {
if (isSidebarOpen.value)
return 'opacity-0 swipe-first swipe-last'
else if (notTopBottom.value)
return 'swipe-first'
return null
})
</script>

<template>
<header :inert="returnInert" gap="1.5" transition="composite" duration="500" lg="flex flex-col translate-y-0 justify-end opacity-100" :class="!isOnBottom && `translate-y-80 md:opacity-0`">
<header gap="1.5" transition="composite" duration="500" lg="flex flex-col translate-y-0 justify-end opacity-100" :class="!isOnBottom && `translate-y-80 md:opacity-0`">
<div flex gap="4 lg:16" text="sm" class="justify-between md:items-end">
<section v-for="(section, i) in HEADER" :key="i" transition="composite" duration="500" first="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-(x-8 y-4)" last="flex-(~ wrap) flex-col lg:flex-row items-start md:items-end justify-end gap-2 lg:gap-8" class="max-w-fit" :class="notTopBottom && `swipe-left`">
<section v-for="(section, i) in HEADER" :key="i" transition="composite" duration="500" first="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-(x-8 y-4)" last="flex-(~ wrap) flex-col lg:flex-row items-start md:items-end justify-end gap-2 lg:gap-8" class="max-w-fit" :class="returnSectionClass">
<div v-for="(card, idx) in section" :key="idx" flex="~ col" w="40" gap="1.5">
<template v-for="item in card" :key="item.label">
<template v-if="item.href">
<a v-if="isExternalUrl(item.href, true)" :href="item.href" target="_blank" flex gap="4" p="y-0.5" pointer-events="auto" class="smooth-transition group items-center hover:scale-x-110" :class="notTopBottom && `opacity-75 hover:opacity-100`">
<a v-if="isExternalUrl(item.href, true)" :href="item.href" target="_blank" flex gap="4" p="y-0.5" pointer-events="auto" class="variable-transition group items-center hover:scale-x-110" :class="notTopBottom && `opacity-75 hover:opacity-100`">
<span v-if="getHrefNumLabel(item)" aria-hidden="true" font="light" select="none" tabular-nums>{{ `0${getHrefNumLabel(item)}` }}</span>
<h4 font="medium">
{{ item.label }}
</h4>
<span aria-hidden="true" block i="carbon-arrow-up-right" origin="top-left" transition="composite" duration="300" class="opacity-25 scale-75 -translate-x-full" group-hover="opacity-50 scale-100 translate-0" />
</a>
<RouterLink v-else :to="item.href" :data-active="currentPath === item.href" flex gap="4" p="y-0.5" pointer-events="auto" select="none" class="smooth-transition route-line items-center hover:scale-x-110" :class="notTopBottom && `to-top`">
<RouterLink v-else :to="item.href" :data-active="currentPath === item.href" flex gap="4" p="y-0.5" pointer-events="auto" select="none" class="variable-transition line-divider items-center hover:scale-x-110" :class="notTopBottom && `to-top`">
<span aria-hidden="true" font="light" select="none" tabular-nums>{{ `0${getHrefNumLabel(item)}` }}</span>
<h4 font="medium">
{{ item.label }}
Expand All @@ -77,16 +76,20 @@ const returnInert = computed(() => {
</template>

<style scoped>
.swipe-left:first-child {
.swipe-first:first-child {
transform: translateX(calc(-10rem - 2rem));
}

.route-line[data-active='true']::after,
.route-line:hover::after {
.swipe-last:last-child {
transform: translateX(calc(100% + 2rem));
}

.line-divider[data-active='true']::after,
.line-divider:hover::after {
scale: 1;
}

.route-line::after {
.line-divider::after {
content: '';
width: 100%;
height: 1px;
Expand All @@ -97,7 +100,7 @@ const returnInert = computed(() => {
border-radius: 100%;
}

.smooth-transition {
.variable-transition {
transition:
scale var(--ease-o-back) 0.3s,
translate var(--ease-io-quint) 0.5s;
Expand Down
2 changes: 1 addition & 1 deletion src/components/LinkCTA.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const spanClassName = 'text-(lg nowrap) tracking-widest font-bold select-none lg
</script>

<template>
<RouterLink relative :to="to" p="y-1 x-2 sm:y-2 sm:x-3" class="wrapper-img group/button text-center *:first:absolute-overlay" border="2" :class="wrapperColorMap[color]">
<RouterLink relative :to="to" active-class="*:first:opacity-0" p="y-1 x-2 sm:y-2 sm:x-3" class="wrapper-img group/button text-center *:first:absolute-overlay" border="2" :class="wrapperColorMap[color]">
<slot name="overlay">
<div aria-hidden="true" ease="out-expo" duration="500" transition="composite" class="flex-centered group-focus/button:opacity-0 group-hover/button:opacity-0" :class="overlayColorMap[color]">
<span :class="spanClassName">{{ label }}</span>
Expand Down
36 changes: 27 additions & 9 deletions src/components/NavOverlay.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<script setup lang="ts">
import { useInjectedSidebar } from '~/composables/useInjectedSidebar'
import { availableLocales, loadLanguageAsync } from '~/modules/i18n'

const isDark = useDark()
const { t, locale } = useI18n()
const { isSidebarOpen, toggle: toggleSidebar } = useInjectedSidebar()

async function toggleLocale() {
const newLocale = availableLocales[(availableLocales.indexOf(locale.value) + 1) % availableLocales.length]
Expand All @@ -12,19 +14,35 @@ async function toggleLocale() {
</script>

<template>
<nav flex pointer-events="none" class="justify-between">
<nav flex="~ justify-between" pointer-events="none">
<slot name="left">
<div />
</slot>
<slot name="right">
<div class="tracking-wider flex h-full w-30 items-center relative md:w-80">
<ToggleButton class="flex-1 pointer-events-auto hover:bg-primary/10" :state="locale === 'en'" truthy-label="ENG" falsy-label="IDN" @click="toggleLocale">
<span aria-hidden="true" i="carbon-language" block />
<span class="text-sm truncate sr-only md:not-sr-only">{{ t('button.toggle_lang') }}</span>
</ToggleButton>
<ToggleButton class="flex-1 pointer-events-auto hover:bg-primary/10" :state="isDark" :truthy-label="t('button.toggle_theme.dark')" :falsy-label="t('button.toggle_theme.light')" @click="toggleDark()">
<span aria-hidden="true" block i="carbon-sun dark:carbon-moon" />
<span class="text-sm truncate sr-only md:not-sr-only">{{ t('button.toggle_theme._label') }}</span>
<div class="tracking-wider flex gap-1 h-full relative">
<div flex>
<ToggleButton block class="p-1" :state="locale === 'en'" initial-label="EN" toggled-label="ID" @click="toggleLocale">
<template #leading>
<span aria-hidden="true" i="carbon-language" class="hidden sm:block" />
</template>
</ToggleButton>
<ToggleButton block class="p-1 capitalize" :state="isDark" :label-width="6" @click="toggleDark()">
<template #leading>
<span aria-hidden="true" block i="carbon-sun dark:carbon-moon" />
</template>
<template #content>
<Transition mode="out-in" name="slide-y">
<span v-if="isDark" class="hidden sm:inline-block">{{ t('button.toggle_theme.dark') }}</span>
<span v-else class="hidden sm:inline-block">{{ t('button.toggle_theme.light') }}</span>
</Transition>
</template>
</ToggleButton>
</div>
<ToggleButton class="p-1" @click="toggleSidebar">
<template #leading>
<span inline-block :class="isSidebarOpen ? `i-carbon-close-large` : `i-carbon-menu`" />
<span>Menu</span>
</template>
</ToggleButton>
</div>
</slot>
Expand Down
109 changes: 109 additions & 0 deletions src/components/SidebarOverlay.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<script setup lang="ts">
const { t } = useI18n()

interface SocialType {
platform: string
label?: string
href: string
}

const SOCIALS: SocialType[] = [
{
platform: 'Instagram',
label: 'say1ddd',
href: 'https://www.instagram.com/say1ddd',
},
{
platform: 'LinkedIn',
label: '/in/sajid-ikhlas',
href: 'https://www.linkedin.com/in/sajid-ikhlas',
},
{
platform: 'Github',
label: 'say1ddd',
href: 'https://www.github.com/say1ddd/',
},
{
platform: 'Email',
label: 'hello@sikhlas.com',
href: 'mailto:hello@sikhlas.com',
},
]

function getIconType(platform: string) {
function isPlatform(value: string) {
return platform.toLowerCase() === value
}
if (isPlatform('linkedin') || isPlatform('email'))
return 'link'
else return 'at'
}
</script>

<template>
<aside fixed z="25" p="t-24 sm:t-26 b-20" flex="~ col" class="border-l-2 rounded bg-background/90 h-full w-full right-0 top-0 dark:bg-background/95 sm:w-md">
<ul aria-hidden="true" absolute class="h-full events-none right-full top-0" flex="~ col justify-evenly">
<li v-for="i in 5" :key="i" i="carbon-chevron-left" class="text-xl opacity-50" />
</ul>
<div p="y-12 sm:y-6" flex="~ col 1 justify-items-center gap-8" border="y dashed foreground/50" class="overflow-x-hidden overflow-y-auto">
<ul relative m="x-1" p="x-4 y-4" flex="~ col gap-2" class="nav-border">
<li aria-hidden="true" absolute flex="~ items-center gap-2" p="x-1" bg="primary" text="xs black" class="events-none translate-y-1/2 bottom-full left-0">
<span block font="medium jetbrains-mono" class="uppercase">
tap to open
</span>
<div i="carbon-arrow-down" />
</li>
<li>
<LinkCTA to="/about" :replace="true" :label="t('button.about')" color="secondary" class="w-full inline-block uppercase scale-105" />
</li>
<li>
<LinkCTA to="/resume" :label="t('button.resume')" color="secondary" class="w-full inline-block uppercase scale-105" />
</li>
</ul>
<div flex="~ col" gap="3">
<div m="x-2" p="x-2" border="l-2 primary" class="line-divider events-none">
<span class="tracking-wider uppercase" font="medium">MY SOCIALS</span>
</div>
<ul flex="~ col gap-2" p="x-6">
<li v-for="social in SOCIALS" :key="social.href" relative>
<SocialCard>
<SocialLink :title="social.platform" :subtitle="social.label" :href="social.href" :icon-type="getIconType(social.platform)" class="rounded shadow shadow-foreground/5" />
</SocialCard>
</li>
</ul>
</div>
<div text="center" p="x-2">
<span text="sm" class="*:text-primary">
handcrafted with love<span>&hearts;</span>
</span>
</div>
</div>
</aside>
</template>

<style scoped>
.nav-border::after {
content: '';
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
border: 1px dotted rgb(var(--color-foreground-raw) / 25%);
transform: scale(105%);
pointer-events: none;
}

.line-divider {
display: flex;
align-items: center;
gap: 0.5rem;
}

.line-divider::after {
content: '';
flex: 1;
height: 1px;
background-color: var(--color-foreground);
}
</style>
2 changes: 1 addition & 1 deletion src/components/partials/PreviousButton.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<RouterLink to="/" pointer-events="auto" class="group border-none opacity-80 flex size-16 scale-80 items-center justify-center rounded-none! md:(opacity-100 scale-100)">
<span class="sr-only">Previous</span>
<span aria-hidden="true" transition="composite" border="~ dashed" class="rotate-clockwise rounded-full h-full w-full duration-500 group-focus:(opacity-100 scale-100) group-hover:(opacity-100 scale-100) sm:(opacity-0 scale-75)" />
<span aria-hidden="true" transition="composite" border="~ dashed" bg="background" class="rotate-clockwise rounded-full h-full w-full duration-500 group-focus:(opacity-100 scale-100) group-hover:(opacity-100 scale-100) sm:(opacity-0 scale-75)" />
<svg
viewBox="0 0 128 128"
aria-hidden="true"
Expand Down
Loading
Loading