Files
supabase/apps/www/components/Nav/RightClickBrandLogo.tsx
2024-07-04 09:37:18 +02:00

316 lines
9.9 KiB
TypeScript

import React, { Fragment, MouseEvent, ReactNode, useRef, useState } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useClickAway, useKey } from 'react-use'
import { CheckIcon, ClipboardIcon } from '@heroicons/react/outline'
import { cn, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from 'ui'
import * as supabaseLogoWordmarkDark from 'common/assets/images/supabase-logo-wordmark--dark.png'
import * as supabaseLogoWordmarkLight from 'common/assets/images/supabase-logo-wordmark--light.png'
/**
* Right click on the Supabase logo in the website navbar
* for quick access to brand assets.
*/
const RightClickBrandLogo = () => {
const ref = useRef(null)
const router = useRouter()
const [open, setOpen] = useState<boolean>(false)
const [copied, setCopied] = useState<boolean>(false)
/**
* Close dropdown by clicking outside
*/
useClickAway(ref, () => {
setOpen(false)
})
/**
* Close dropdown by using the Escape key
*/
useKey('Escape', () => setOpen(false))
/**
* Open dropdown by right clicking on the Supabase logo
*/
const handleRightClick = (e: MouseEvent) => {
e.preventDefault()
if (e.type === 'contextmenu' || e.nativeEvent.button === 2) {
setOpen(true)
}
}
/**
* A11y open dropdown by typing Shift+Enter when the logo is focused
*/
const handleKeyboardOpen = () => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.shiftKey && e.key === 'Enter') {
setOpen(true)
}
}
document?.addEventListener('keydown', onKeyDown)
return () => {
document?.removeEventListener('keydown', onKeyDown)
}
}
/**
* Copy to clipboard logo SVG
*/
const handleCopyToClipboard = (menuItem: MenuItemProps) => {
navigator.clipboard.writeText(menuItem.clipboard ?? '').then(() => {
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 2000)
})
}
return (
<DropdownMenu open={open}>
<DropdownMenuTrigger asChild>
<Link
href="/"
onContextMenu={handleRightClick}
onFocus={handleKeyboardOpen}
onKeyDown={(e) => e.key === 'Enter' && router.push('/')}
className="block w-auto h-6 focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-foreground-lighter focus-visible:ring-offset-4 focus-visible:ring-offset-background-alternative focus-visible:rounded-sm"
>
<Image
src={supabaseLogoWordmarkLight}
width={124}
height={24}
alt="Supabase Logo"
className="dark:hidden"
priority
/>
<Image
src={supabaseLogoWordmarkDark}
width={124}
height={24}
alt="Supabase Logo"
className="hidden dark:block"
priority
/>
</Link>
</DropdownMenuTrigger>
<DropdownMenuContent
autoFocus
ref={ref}
align="start"
side="bottom"
className="mt-2 p-0 w-52"
>
{menuItems.map((section, sectionIdx) => (
<Fragment key={`cxtMenu-section-${sectionIdx}`}>
{sectionIdx !== 0 && <DropdownSeparator />}
<div className="p-1">
{section.map((menuItem, i) => (
<DropdownMenuItem
autoFocus
asChild
key={menuItem.label}
className="w-full flex justify-between gap-2 items-center !p-2"
>
{menuItem.type === 'download' || menuItem.type === 'link' ? (
<Link
href={menuItem.href ?? ''}
download={menuItem.type === 'download'}
className="group/menu-item w-full text-left flex justify-between gap-2 items-center"
>
<MenuItemContent {...menuItem} />
</Link>
) : (
menuItem.type === 'clipboard' && (
<button
className="group/menu-item w-full text-left flex justify-between gap-2 items-center"
onClick={() => handleCopyToClipboard(menuItem)}
>
<MenuItemContent copied={copied} {...menuItem} />
</button>
)
)}
</DropdownMenuItem>
))}
</div>
</Fragment>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
const DropdownSeparator = () => <div className="h-px w-full bg-border" />
interface MenuItemProps {
label: string
type: 'clipboard' | 'download' | 'link'
icon?: ReactNode
href?: string
clipboard?: string
}
/**
* Brand assets dropdown menu items.
* First array is to divide the menu by sections;
* Second array is the list of items inside each section.
*/
const menuItems: MenuItemProps[][] = [
[
{
label: 'Copy logo as SVG',
type: 'clipboard',
icon: (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.95127 2.53536C7.94394 2.04707 7.3269 1.83752 7.02259 2.21997L2.43035 7.99124C1.88831 8.67243 2.37446 9.67704 3.24613 9.67704H7.99525L8.05109 13.3962C8.05842 13.8844 8.67544 14.094 8.97976 13.7116L13.572 7.94026C14.114 7.25907 13.6279 6.25446 12.7562 6.25446H7.97574L7.95127 2.53536Z"
fill="currentColor"
/>
</svg>
),
clipboard: `<svg width="109" height="113" viewBox="0 0 109 113" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint0_linear)"/>
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint1_linear)" fill-opacity="0.2"/>
<path d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z" fill="#3ECF8E"/>
<defs>
<linearGradient id="paint0_linear" x1="53.9738" y1="54.974" x2="94.1635" y2="71.8295" gradientUnits="userSpaceOnUse">
<stop stop-color="#249361"/>
<stop offset="1" stop-color="#3ECF8E"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="36.1558" y1="30.578" x2="54.4844" y2="65.0806" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>
`,
},
{
label: 'Download wordmark',
type: 'download',
icon: (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14 10V12.6667C14 13.0203 13.8595 13.3594 13.6095 13.6095C13.3594 13.8595 13.0203 14 12.6667 14H3.33333C2.97971 14 2.64057 13.8595 2.39052 13.6095C2.14048 13.3594 2 13.0203 2 12.6667V10"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4.66669 6.66675L8.00002 10.0001L11.3334 6.66675"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M8 10V2"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
href: '/supabase-wordmark.zip',
},
{
label: 'Download brand assets',
type: 'download',
href: '/brand-assets.zip',
icon: (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14 10V12.6667C14 13.0203 13.8595 13.3594 13.6095 13.6095C13.3594 13.8595 13.0203 14 12.6667 14H3.33333C2.97971 14 2.64057 13.8595 2.39052 13.6095C2.14048 13.3594 2 13.0203 2 12.6667V10"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4.66669 6.66675L8.00002 10.0001L11.3334 6.66675"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M8 10V2"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
},
],
[
{
label: 'Brand Assets',
type: 'link',
href: '/brand-assets',
},
],
]
interface MenuItemContentProps {
icon?: ReactNode
type: string
label: string
copied?: boolean
}
/**
* MenuItem content
*/
const MenuItemContent = ({ icon, type, label, copied }: MenuItemContentProps) => (
<>
{icon && <span className="text-foreground-lighter">{icon}</span>}
<span className="grow">{label}</span>
{type === 'clipboard' && (
<span className="w-4 opacity-0 flex items-center justify-center group-hover/menu-item:opacity-100 group-focus/menu-item:opacity-100 group-focus-visible/menu-item:opacity-100">
{!copied ? (
<ClipboardIcon
strokeWidth={1}
className={cn(
'h-3 transition-opacity opacity-0 duration-300',
!copied && 'opacity-100'
)}
/>
) : (
<CheckIcon
strokeWidth={1}
className={cn('h-3 transition-opacity opacity-0 duration-300', copied && 'opacity-100')}
/>
)}
</span>
)}
</>
)
export default RightClickBrandLogo