Skip to content

Commit

Permalink
Merge pull request #2 from emiliosheinz/feat/keyboard-support
Browse files Browse the repository at this point in the history
feat: keyboard command bar
  • Loading branch information
emiliosheinz authored Jan 6, 2024
2 parents ad80ed2 + 4a68dce commit 622eb89
Show file tree
Hide file tree
Showing 13 changed files with 365 additions and 32 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"eslint": "8.47.0",
"eslint-config-next": "13.4.18",
"framer-motion": "^10.16.0",
"kbar": "^0.1.0-beta.44",
"next": "13.4.18",
"next-contentlayer": "^0.3.4",
"postcss": "8.4.28",
Expand Down
6 changes: 5 additions & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

@layer base {
::-webkit-scrollbar {
@apply w-3 h-3;
@apply w-1 h-1;
}

::-webkit-scrollbar-track {
Expand Down Expand Up @@ -85,4 +85,8 @@
a {
@apply hover:text-dodgerBlue underline underline-offset-4 transition-all duration-300;
}

kbd {
@apply bg-codGray-300 rounded px-2 py-0.5 border border-white border-opacity-10;
}
}
17 changes: 10 additions & 7 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Toaster } from 'react-hot-toast'
import { Roboto_Mono } from 'next/font/google'
import { Header } from '~/components/header'
import { classNames } from '~/utils/css.utils'
import { CommandBar } from '~/components/command-bar'

const robotoMono = Roboto_Mono({
subsets: ['latin'],
Expand Down Expand Up @@ -77,13 +78,15 @@ export default function RootLayout({ children }: RootLayoutProps) {
'bg-codGray-500 text-white scroll-smooth'
)}
>
<body className={'pb-10 pt-32 sm:pt-48 px-5 max-w-6xl m-auto'}>
<Header />
{children}
<CustomToaster />
<Analytics />
<SpeedInsights />
</body>
<CommandBar>
<body className='pb-10 pt-32 lg:pt-48 px-5 max-w-6xl m-auto'>
<Header />
{children}
<CustomToaster />
<Analytics />
<SpeedInsights />
</body>
</CommandBar>
</html>
)
}
40 changes: 26 additions & 14 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Slider } from '~/components/slider'
import { socialMedias } from '~/data/social-medias'
import { currentExperience } from '~/content/experiences'
import { getLastFivePosts } from '~/content/posts'
import { Image } from '~/components/image'
import { CommandBarTriggerFull } from '~/components/command-bar'

type SectionProps = {
id: string
Expand All @@ -24,20 +26,30 @@ function Section({ id, title, children }: SectionProps) {
export default function HomePage() {
return (
<main className='flex flex-col space-y-16 sm:space-y-24'>
<div className='flex flex-col space-y-6 sm:space-y-8' id='about'>
<h1 className='font-bold text-4xl sm:text-5xl'>
{'Hello '}
<span className='text-5xl sm:text-6xl inline-block origin-bottom-right animate-waving-hand'>
👋
</span>
{`, I'm Emilio.`}
</h1>
<p className='text-xl sm:text-2xl max-w-3xl'>
As an experienced Software Engineer graduated with a B.Sc. degree in
Computer Science, I have been working on the development of
applications that are daily accessed by thousands of users since 2019.
I bring ideas to life, I turn coffee into code ☕️.
</p>
<div className='flex items-center flex-col lg:flex-row' id='about'>
<Image
src='/images/profile.png'
width={225}
height={225}
className='rounded-full mb-10 lg:mb-0 lg:mr-10'
alt="Emilio Schaedler Heinzmann's picture in black and white with a blue background"
/>
<div className='flex flex-col space-y-6 sm:space-y-8 items-start'>
<h1 className='font-bold text-4xl sm:text-5xl'>
{'Hello '}
<span className='text-5xl sm:text-6xl inline-block origin-bottom-right animate-waving-hand'>
👋
</span>
{`, I'm Emilio.`}
</h1>
<p className='text-xl sm:text-2xl lg:max-w-3xl'>
As an experienced Software Engineer graduated with a B.Sc. degree in
Computer Science, I have been working on the development of
applications that are daily accessed by thousands of users since
2019. I bring ideas to life, I turn coffee into code ☕️.
</p>
<CommandBarTriggerFull />
</div>
</div>

<Section title='Experience' id='experience'>
Expand Down
68 changes: 68 additions & 0 deletions src/components/command-bar/command-bar-trigger.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use client'

import { useKBar } from 'kbar'
import { useEffect, useState } from 'react'
import { FaArrowRightLong } from 'react-icons/fa6'

export function CommandBarTriggerFull() {
const { query } = useKBar()
const [isMounted, setIsMounted] = useState(false)

useEffect(() => {
setIsMounted(true)
}, [])

const renderLabel = () => {
const isMac = /(Mac)/i.test(navigator.userAgent)
const isMobile = /iPhone|iPad|Android/i.test(navigator.userAgent)

if (isMobile) {
return <span>Tap to start</span>
}

if (isMac) {
return (
<span>
Press <kbd></kbd> <kbd>K</kbd> to start
</span>
)
}

return (
<span>
Press <kbd>ctrl</kbd> <kbd>K</kbd> to start
</span>
)
}

if (!isMounted) {
return <div className='h-10 w-64 bg-codGray-300 animate-pulse rounded-md' />
}

return (
<button
className='px-4 h-10 rounded border bg-white border-white bg-opacity-0 border-opacity-0 hover:bg-opacity-5 hover:border-opacity-20 transition-all ease-in-out'
onClick={query.toggle}
>
{renderLabel()}
<FaArrowRightLong
data-testid='arrow-right-icon'
className='inline transition-all ease-in-out group-hover:translate-x-1 group-hover:text-dodgerBlue ml-3'
/>
</button>
)
}

export function CommandBarTriggerLite() {
const { query } = useKBar()

return (
<button
className='px-2.5 rounded border bg-white border-white bg-opacity-0 border-opacity-0 hover:bg-opacity-5 hover:border-opacity-20 transition-all ease-in-out'
onClick={query.toggle}
title='Open command bar'
>
<span className='text-3xl text-white'></span>
</button>
)
}
36 changes: 36 additions & 0 deletions src/components/command-bar/command-bar.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use client'

import {
KBarProvider,
KBarPortal,
KBarPositioner,
KBarAnimator,
KBarSearch,
} from 'kbar'

import { useActions } from './use-actions.hook'
import { Results } from './results.component'

type CommandBarProps = {
children: React.ReactNode
}

export function CommandBar(props: CommandBarProps) {
const { children } = props

const actions = useActions()

return (
<KBarProvider actions={actions}>
<KBarPortal>
<KBarPositioner className='z-40 bg-black bg-opacity-80 backdrop-blur-sm'>
<KBarAnimator className='w-full max-w-xl rounded-md bg-codGray-500 bg-opacity-80 backdrop-blur-xl'>
<KBarSearch className='w-full h-12 p-5 bg-transparent rounded-md outline-none' />
<Results />
</KBarAnimator>
</KBarPositioner>
</KBarPortal>
{children}
</KBarProvider>
)
}
5 changes: 5 additions & 0 deletions src/components/command-bar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { CommandBar } from './command-bar.component'
export {
CommandBarTriggerFull,
CommandBarTriggerLite,
} from './command-bar-trigger.component'
36 changes: 36 additions & 0 deletions src/components/command-bar/results.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { KBarResults, useMatches } from 'kbar'
import { classNames } from '~/utils/css.utils'

export function Results() {
const { results } = useMatches()

return (
<KBarResults
items={results}
onRender={({ item, active }) => {
const itemClassNames = classNames(
'px-5 py-3 flex gap-5 items-center',
active ? 'bg-codGray-300 bg-opacity-50' : 'bg-transparent'
)

if (typeof item === 'string') {
return <div className={itemClassNames}>{item}</div>
}

return (
<div className={itemClassNames}>
{item.icon}
<span
className={classNames(
'text-white',
active ? 'opacity-100' : 'opacity-50'
)}
>
{item.name}
</span>
</div>
)
}}
/>
)
}
87 changes: 87 additions & 0 deletions src/components/command-bar/use-actions.hook.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Action } from 'kbar'
import { useRouter } from 'next/navigation'
import {
HiOutlineAtSymbol,
HiOutlineBookOpen,
HiOutlineCodeBracket,
HiOutlineHome,
HiOutlineLightBulb,
HiOutlineLink,
} from 'react-icons/hi2'
import { socialMedias } from '~/data/social-medias'
import { notify } from '~/utils/toast.utils'

export function useActions(): Action[] {
const router = useRouter()

const utilActions: Action[] = [
{
id: 'copy-url',
name: 'Copy URL',
keywords: 'copy url',
perform: () => {
navigator.clipboard.writeText(window.location.href)
notify.success('URL copied to clipboard')
},
icon: <HiOutlineLink className='w-5 h-5 text-white' />,
},
{
id: 'source-code',
name: 'Source code',
keywords: 'source code',
perform: () => {
window.open(
'https://github.com/emiliosheinz/emiliosheinz.com',
'_blank'
)
},
icon: <HiOutlineCodeBracket className='w-5 h-5 text-white' />,
},
{
id: 'email',
name: 'Send me an email',
keywords: 'email',
perform: () => {
window.open('mailto:[email protected]', '_blank')
},
icon: <HiOutlineAtSymbol className='w-5 h-5 text-white' />,
},
].map(action => ({ ...action, section: 'Util' }))

const socialMediaActions: Action[] = socialMedias.map(
({ Icon, url, name }) => ({
name,
id: name.toLowerCase(),
keywords: name.toLocaleLowerCase(),
perform: () => window.open(url, '_blank'),
icon: <Icon className='w-5 h-5 text-white' />,
section: 'Social Media',
})
)

const goToActions: Action[] = [
{
id: 'home',
name: 'Home',
keywords: 'home page start initial',
perform: () => router.push('/'),
icon: <HiOutlineHome className='w-5 h-5 text-white' />,
},
{
id: 'experience',
name: 'Experience',
keywords: 'experience work jobs',
perform: () => router.push('/experiences'),
icon: <HiOutlineLightBulb className='w-5 h-5 text-white' />,
},
{
id: 'blog-posts',
name: 'Blog Posts',
keywords: 'blog posts articles',
perform: () => router.push('/posts'),
icon: <HiOutlineBookOpen className='w-5 h-5 text-white' />,
},
].map(action => ({ ...action, section: 'Go to' }))

return [...utilActions, ...socialMediaActions, ...goToActions]
}
8 changes: 8 additions & 0 deletions src/components/header/__tests__/header.component.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ let mockPreviousRoute: string | undefined
jest.mock('~/hooks/usePreviousRoute', () => ({
usePreviousRoute: jest.fn(() => mockPreviousRoute),
}))
jest.mock('~/components/command-bar', () => ({
CommandBarTriggerLite: () => <div>CommandBarTriggerLite</div>,
}))

describe('Header', () => {
it('should render a header element as the root node', () => {
Expand Down Expand Up @@ -70,4 +73,9 @@ describe('Header', () => {
expect(mockReplace).toHaveBeenCalledTimes(1)
expect(mockReplace).toHaveBeenCalledWith('/')
})

it('should render the lite command bar trigger', () => {
render(<Header />)
expect(screen.getByText('CommandBarTriggerLite')).toBeVisible()
})
})
15 changes: 5 additions & 10 deletions src/components/header/header.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Image } from '../image'
import { FaArrowLeftLong } from 'react-icons/fa6'
import { usePreviousRoute } from '~/hooks/usePreviousRoute'
import { headerLinks } from './header.constants'
import { CommandBarTriggerLite } from '../command-bar'

export function Header() {
const pathname = usePathname()
Expand Down Expand Up @@ -65,16 +66,10 @@ export function Header() {
return (
<header className='fixed bg-codGray-500 top-0 left-0 right-0 z-40'>
<div className='flex items-center w-full max-w-6xl m-auto py-2 sm:py-5 px-5 overflow-y-scroll'>
<div className='flex flex-1 space-x-5 mr-10'>{renderLinks()}</div>
<Link href='/' className='min-w-max'>
<Image
src='/images/profile.png'
width={62}
height={62}
className='rounded-full'
alt="Emilio Schaedler Heinzmann's picture in black and white with a blue background"
/>
</Link>
<div className='flex flex-1 space-x-5'>{renderLinks()}</div>
<div className='ml-5'>
<CommandBarTriggerLite />
</div>
</div>
</header>
)
Expand Down
Loading

0 comments on commit 622eb89

Please sign in to comment.