Author SHA1 Message Date
marcelines f142068fc0 f 2024-10-28 09:36:12 +00:00
marcelines dcdcd02044 f: add react-fiber to test keycard 3d model in hero 2024-10-28 09:34:19 +00:00
marcelines 438ee4c3bc f: imports 2024-10-25 09:00:39 +01:00
marcelines 929c882d3f f: root layout 2024-10-25 08:54:33 +01:00
marcelines a8812b4da5 f: change files structure 2024-10-25 08:46:10 +01:00
marcelines 646e36e263 f: vercel comments 2024-10-24 17:17:04 +01:00
marcelines 76b0887165 f: fixes 2024-10-24 12:31:13 +01:00
marcelines f16e7704c4 f: replaces react-spring with framer motion 2024-10-24 09:28:22 +01:00
marcelines a27a5484f0 f 2024-10-24 09:14:38 +01:00
marcelines d8139c4b35 f: background color and blur transition in navbar 2024-10-24 08:22:10 +01:00
marcelines 3c3cac949d f: buy cards section and more fixes 2024-10-23 19:48:54 +01:00
marcelines def1c8e155 f: footer styles and other minor fixes 2024-10-23 18:41:30 +01:00
marcelines 79e0d52600 f: faqs section 2024-10-23 16:06:42 +01:00
marcelines 5569b04403 f 2024-10-23 10:41:20 +01:00
marcelines 5bf1c9debe f: fixes comparision table section and adds tooltip 2024-10-23 10:39:59 +01:00
marcelines edd85f62d3 f: fix features section 2024-10-22 20:05:56 +01:00
marcelines be7435c9b2 f: fix first sections 2024-10-22 15:34:29 +01:00
marcelines d3dbffb2c5 f: header, hero and keycardpro section styles 2024-10-22 11:05:57 +01:00
marcelines 51c22329ea f: adds missings sections wip 2024-10-22 09:08:46 +01:00
marcelines a3ec0f5ece f: adds navbar and hero draft 2024-10-21 18:06:43 +01:00
30 changed files with 4632 additions and 2537 deletions
+11 -1
View File
@@ -10,10 +10,20 @@
"check": "tsc --noEmit"
},
"dependencies": {
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.3",
"@react-three/drei": "^9.115.0",
"@react-three/fiber": "^8.17.10",
"@types/three": "^0.169.0",
"cva": "1.0.0-beta.1",
"framer-motion": "^11.11.9",
"framer-motion-3d": "^11.11.10",
"next": "14.2.15",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"three": "^0.169.0",
"three-stdlib": "^2.33.0",
"ts-pattern": "^5.5.0"
},
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
+3108 -2523
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.
@@ -0,0 +1,101 @@
import { ButtonLink } from '~components/button-link'
import { cx } from 'cva'
import Image from 'next/image'
import React from 'react'
type Props = {
image: string
imageClassName: string
name: string
secondName?: string
title: string
description: string
buttons: React.ReactNode
info: string
}
const Section = (props: Props) => {
const {
image,
name,
title,
description,
buttons,
info,
imageClassName,
secondName,
} = props
return (
<div className="relative w-full overflow-clip">
<div className="absolute inset-0 z-10 h-1/2 w-full bg-gradient-to-b from-dark-100 to-[transparent]" />
<Image
src={image}
alt={name}
width="900"
height="600"
draggable={false}
className={cx([
'pointer-events-none absolute left-1/2 top-6 z-20 max-w-[549px] -translate-x-1/2 select-none',
imageClassName,
])}
/>
<div className="flex size-full flex-col items-center justify-end rounded-28 border border-white-6 bg-white-3 px-[98px] pb-[120px] pt-[300px]">
<div className="container relative z-10 flex max-w-[434px] flex-col items-center justify-center px-4">
<p className="pb-2 text-24 font-600 text-white-95">
{name} <span className="font-200">{secondName}</span>
</p>
<h1 className="flex pb-4 text-center font-lora text-48 font-400">
{title}
</h1>
<p className="pb-8 text-center text-20 font-300 text-white-80">
{description}
</p>
<div className="flex space-x-4">{buttons}</div>
<p className="pt-6 text-16 font-300 text-white-60">{info}</p>
</div>
</div>
</div>
)
}
const BuyCards = () => {
return (
<section className="flex flex-col gap-2 overflow-clip pb-2 pt-[200px] lg:flex-row">
<Section
image="/assets/bottom-keycard.png"
imageClassName="-mb-72 w-full"
name="keycard"
title="Best in class secure element"
description="Something will say here about this product. Certainly, you don't want to miss it."
buttons={
<>
<ButtonLink href="/">Buy Keycard</ButtonLink>
<ButtonLink href="/" variant="secondary">
Learn more
</ButtonLink>
</>
}
info="Starts from $25"
/>
<Section
image="/assets/bottom-keycard-pro.png"
imageClassName="-mb-20 w-full"
name="keycard"
secondName="pro"
title="A modular hardware wallet"
description="Something will say here about this product. Certainly you don't want to miss it"
buttons={
<>
<ButtonLink href="/">Get notified</ButtonLink>
<ButtonLink href="/" variant="secondary">
Learn more
</ButtonLink>
</>
}
info="Coming 2025"
/>
</section>
)
}
export { BuyCards }
@@ -0,0 +1,483 @@
import { Tooltip } from '~components/tooltip'
import { cx } from 'cva'
import React from 'react'
const Shield = () => (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_1252_12446)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.6 0.5V2.4062C11.246 2.42007 11.8458 2.45799 12.4 2.5276V1H13.6V2.74837C14.5768 2.99632 15.3706 3.39705 15.9868 4.01324C16.6547 4.68118 17.0695 5.55778 17.3109 6.65H19V7.85H17.5013C17.5527 8.33344 17.582 8.84997 17.5938 9.4H19.5V10.6H17.5938C17.5778 11.3443 17.5299 12.0273 17.4384 12.65H19V13.85H17.1836C16.9326 14.7131 16.5495 15.424 15.9868 15.9868C15.3706 16.603 14.5768 17.0037 13.6 17.2516V19H12.4V17.4724C11.8458 17.542 11.246 17.5799 10.6 17.5938V19.5H9.4V17.5938C8.75401 17.5799 8.15424 17.542 7.6 17.4724V19H6.4V17.2516C5.42319 17.0037 4.62942 16.603 4.01324 15.9868C3.45048 15.424 3.06744 14.7131 2.81637 13.85H1L1 12.65H2.56163C2.47009 12.0273 2.42218 11.3443 2.4062 10.6H0.5V9.4H2.4062C2.41801 8.84997 2.44725 8.33344 2.49865 7.85H1L1 6.65H2.68909C2.93052 5.55778 3.34529 4.68118 4.01324 4.01324C4.62942 3.39705 5.42319 2.99632 6.4 2.74837V1H7.6V2.5276C8.15424 2.45799 8.75401 2.42007 9.4 2.4062V0.5H10.6ZM3.6 10C3.6 7.25657 3.98658 5.73694 4.86176 4.86176C5.73694 3.98658 7.25657 3.6 10 3.6C12.7434 3.6 14.2631 3.98658 15.1382 4.86176C16.0134 5.73694 16.4 7.25657 16.4 10C16.4 12.7434 16.0134 14.2631 15.1382 15.1382C14.2631 16.0134 12.7434 16.4 10 16.4C7.25657 16.4 5.73694 16.0134 4.86176 15.1382C3.98658 14.2631 3.6 12.7434 3.6 10ZM6.6 9.6C6.6 8.83071 6.63205 8.27111 6.71996 7.8532C6.80568 7.4457 6.93406 7.22455 7.09159 7.08102C7.25554 6.93164 7.5208 6.80264 8.0029 6.7174C8.48731 6.63175 9.13076 6.6 10 6.6C10.8704 6.6 11.5102 6.63195 11.9905 6.72244C12.4649 6.81183 12.7244 6.9479 12.8882 7.11176C13.0521 7.27562 13.1882 7.53506 13.2776 8.00953C13.3681 8.48983 13.4 9.1296 13.4 10C13.4 10.8704 13.3681 11.5102 13.2776 11.9905C13.1882 12.4649 13.0521 12.7244 12.8882 12.8882C12.7244 13.0521 12.4649 13.1882 11.9905 13.2776C11.5102 13.3681 10.8704 13.4 10 13.4C9.12843 13.4 8.49237 13.3678 8.01611 13.2726C7.54962 13.1793 7.29458 13.0365 7.12942 12.8567C6.9585 12.6705 6.81633 12.3716 6.72427 11.8319C6.63186 11.2902 6.6 10.5712 6.6 9.6ZM10 5.4C9.11924 5.4 8.38769 5.43075 7.79397 5.53573C7.19795 5.64111 6.68196 5.83086 6.28341 6.19398C5.87844 6.56295 5.66307 7.04805 5.54566 7.60618C5.43045 8.15389 5.4 8.81929 5.4 9.6C5.4 10.5788 5.43064 11.3848 5.54136 12.0337C5.65242 12.6847 5.854 13.242 6.24558 13.6683C6.64292 14.101 7.16913 14.327 7.78077 14.4493C8.38263 14.5697 9.12157 14.6 10 14.6C10.8796 14.6 11.6148 14.5694 12.2127 14.4568C12.8163 14.3431 13.3381 14.1354 13.7368 13.7368C14.1354 13.3381 14.3431 12.8163 14.4568 12.2127C14.5694 11.6148 14.6 10.8796 14.6 10C14.6 9.1204 14.5694 8.38517 14.4568 7.78735C14.3431 7.18369 14.1354 6.66188 13.7368 6.26324C13.3381 5.8646 12.8163 5.65692 12.2127 5.54319C11.6148 5.43055 10.8796 5.4 10 5.4Z"
fill="white"
fillOpacity="0.6"
/>
</g>
<defs>
<clipPath id="clip0_1252_12446">
<rect width="20" height="20" fill="white" />
</clipPath>
</defs>
</svg>
)
const Check = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="24" fill="none">
<path
fill="#FF6400"
d="m10.7 17.4-.392.454.49.424.385-.522-.483-.356Zm4.2-5.7-.483-.356.483.356Zm-6.4 3.8-.398.45.006.004.392-.454Zm2.683 2.256 4.2-5.7-.966-.712-4.2 5.7.966.712Zm4.2-5.7 4.2-5.7-.966-.712-4.2 5.7.966.712Zm-9.88 1.593 2.6 2.3.795-.898-2.6-2.3-.796.898Zm2.605 2.305 2.2 1.9.784-.908-2.2-1.9-.784.908Z"
/>
</svg>
)
}
const Minus = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="24" fill="none">
<path
stroke="#fff"
strokeOpacity=".4"
strokeWidth="1.2"
d="m8.5 11.85 8-0"
/>
</svg>
)
}
const Info = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="none"
className="ml-2 flex-shrink-0 text-white-40 transition-colors hover:text-white-60"
>
<path
fill="currentColor"
fillRule="evenodd"
d="M3.6 10a6.4 6.4 0 1 1 12.8 0 6.4 6.4 0 0 1-12.8 0ZM10 2.4a7.6 7.6 0 1 0 0 15.2 7.6 7.6 0 0 0 0-15.2Zm-.55 6.1-.2 6h1.5l-.2-6h-1.1Zm-.2-2.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z"
clipRule="evenodd"
/>
</svg>
)
}
const features: Array<Feature> = [
{
name: 'Competitive pricing',
tooltip: 'Pricing information',
keycardPro: {
label: 'TBD',
},
keycard: {
label: '$25',
},
tangem: {
label: '$55',
},
ledger: {
label: '$80',
},
},
{
name: 'EVM chain compatible',
keycardPro: {
featured: true,
},
keycard: {
featured: true,
},
tangem: {
featured: true,
},
ledger: {
featured: true,
},
},
{
name: 'Bitcoin compatible',
keycardPro: {
featured: true,
},
keycard: {
featured: true,
tooltip: 'Bitcoin only',
},
tangem: {
featured: true,
},
ledger: {
featured: true,
},
},
{
name: 'Hardware level protection',
tooltip: 'Security certification level',
keycardPro: {
badge: {
text: 'EAL 6+',
gradient: true,
icon: Shield,
},
},
keycard: { badge: { text: 'EAL 6+', gradient: true, icon: Shield } },
tangem: { badge: { text: 'EAL 6+', gradient: true, icon: Shield } },
ledger: { badge: { text: 'EAL 5+' } },
},
{
name: 'Mobile friendly',
tooltip: 'Compatible with mobile devices',
keycardPro: {
featured: true,
tooltip: 'iOS and Android',
},
keycard: {
featured: true,
},
tangem: {
featured: true,
},
ledger: {
featured: false,
},
},
{
name: 'Unlimited master keys',
keycardPro: {
featured: true,
},
keycard: {
featured: true,
tooltip: 'No limit on master keys',
},
tangem: {
featured: true,
tooltip: 'No limit on master keys',
},
ledger: {
featured: false,
},
tooltip: 'No limit on master keys',
},
{
name: 'Easy back up of master keys',
keycardPro: {
featured: true,
},
keycard: {
featured: true,
},
tangem: {
featured: true,
},
ledger: {
featured: false,
},
},
{
name: 'Open ecosystem',
tooltip: 'Part of an open ecosystem',
keycardPro: {
featured: true,
},
keycard: {
featured: true,
},
tangem: {
featured: false,
},
ledger: {
featured: true,
},
},
{
name: 'Open source secure element',
keycardPro: {
featured: true,
tooltip: 'Secure element is open source',
},
keycard: {
featured: false,
},
tangem: {
featured: false,
},
ledger: {
featured: false,
},
},
{
name: "Secure element can't be upgraded",
keycardPro: {
featured: true,
},
keycard: {
featured: false,
},
tangem: {
featured: false,
tooltip: 'Not applicable',
},
ledger: {
featured: false,
tooltip: 'Not applicable',
},
},
{
name: 'QR transaction signing with any wallet',
tooltip: 'Sign transactions using QR codes',
keycardPro: {
featured: true,
},
keycard: {
featured: true,
tooltip: 'Only with Keycard wallet',
},
tangem: {
featured: false,
},
ledger: {
featured: false,
},
},
{
name: 'Airgap capable',
tooltip: 'Can operate without direct connection',
keycardPro: {
featured: true,
},
keycard: {
featured: false,
},
tangem: {
featured: false,
},
ledger: {
featured: false,
},
},
{
name: 'Modular architecture',
keycardPro: {
featured: true,
},
keycard: {
featured: false,
},
tangem: {
featured: false,
},
ledger: {
featured: false,
},
},
{
name: 'Secure element has its own usages',
keycardPro: {
featured: true,
tooltip: 'Multiple use cases for secure element',
},
keycard: {
featured: 'n/a',
},
tangem: {
featured: 'n/a',
},
ledger: {
featured: false,
},
},
{
name: 'Can be used with future signing schemes',
tooltip: 'Future-proof design',
keycardPro: {
featured: true,
},
keycard: {
featured: 'n/a',
},
tangem: {
featured: 'n/a',
},
ledger: {
featured: false,
},
},
{
name: 'Removable battery',
tooltip: 'Battery can be replaced',
keycardPro: {
featured: true,
},
keycard: {
featured: 'n/a',
tooltip: 'Not applicable',
},
tangem: {
featured: 'n/a',
tooltip: 'Not applicable',
},
ledger: {
featured: false,
},
},
{
name: 'USBC + Camera',
keycardPro: {
featured: true,
},
keycard: {
featured: 'n/a',
},
tangem: {
featured: 'n/a',
},
ledger: {
featured: false,
},
},
{
name: 'Oshwa label',
keycardPro: {
featured: true,
},
keycard: {
featured: false,
},
tangem: {
featured: false,
},
ledger: {
featured: false,
},
},
] as const
type FeatureVariant = {
label?: string
badge?: {
text: string
icon?: React.FC
gradient?: boolean
}
featured?: boolean | 'n/a'
tooltip?: string
}
type Feature = {
name: string
tooltip?: string
keycardPro: FeatureVariant
keycard: FeatureVariant
tangem: FeatureVariant
ledger: FeatureVariant
}
const FeatureInfo = ({
variant,
}: {
variant: FeatureVariant | undefined | string
}) => {
if (!variant || typeof variant === 'string') return null
if (variant.featured === true) return <Check />
if (variant.featured === false) return <Minus />
if (variant.featured === 'n/a') return <p className="text-white-60">n/a</p>
if (variant.label) return <p className="text-white-95">{variant.label}</p>
if (variant.badge) {
return (
<div
className={cx([
'flex w-fit items-center gap-[6px] rounded-[32px] py-[5px] pl-3 pr-2 text-16 text-white-95 outline outline-white-12',
variant.badge.gradient &&
'bg-gradient-to-b from-[transparent] to-white-12 pl-2',
])}
>
{variant.badge.icon && React.createElement(variant.badge.icon)}
<span>{variant.badge.text}</span>
</div>
)
}
}
const TooltipInfo = ({
variant,
}: {
variant: FeatureVariant | undefined | string
}) => {
if (typeof variant === 'string') return null
if (!variant || !variant.tooltip) return null
return (
<Tooltip label={variant.tooltip || ''}>
<div className="absolute left-[calc(50%+10px)] flex">
<Info />
</div>
</Tooltip>
)
}
const ComparisonTable = () => {
const products: Array<keyof Feature> = [
'keycardPro',
'keycard',
'tangem',
'ledger',
]
return (
<section className="mx-auto max-w-[1352px] overflow-x-auto pt-[200px] text-white-95">
<h1 className="font-lora text-32 text-white-95">
Going one step further.
</h1>
<div className="overflow-hidden pt-20">
<div className="text-white-90 grid grid-cols-[2fr,1fr,1fr,1fr,1fr] justify-items-center font-lora text-24 font-400">
<div className="justify-self-start p-4 pb-5 pl-6">Feature</div>
<div className="p-4 pb-5">Keycard Pro</div>
<div className="p-4 pb-5">Keycard</div>
<div className="p-4 pb-5">Tangem</div>
<div className="p-4 pb-5">Ledger</div>
</div>
<div className="rounded-28 border border-white-12 bg-white-3 px-6">
{features.map((feature, index) => {
return (
<div
key={index}
className="grid grid-cols-[2fr,1fr,1fr,1fr,1fr] border-b border-dashed border-white-12 font-300 first:pt-2 last:border-b-0 last:pb-2"
>
<div className="flex items-center p-5 first:pl-0">
{feature.name}
{feature.tooltip && (
<Tooltip label={feature.tooltip}>
<Info />
</Tooltip>
)}
</div>
{products.map(product => (
<div
key={product}
className="relative flex items-center justify-center last:pr-0"
>
<FeatureInfo variant={feature[product]} />
<TooltipInfo variant={feature[product]} />
</div>
))}
</div>
)
})}
</div>
</div>
</section>
)
}
export { ComparisonTable }
+177
View File
@@ -0,0 +1,177 @@
import * as Accordion from '@radix-ui/react-accordion'
import { ButtonLink } from '~components/button-link'
import { cx } from 'cva'
import Image from 'next/image'
type IconProps = {
className?: string
}
const ChevronDownIcon = (props: IconProps) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="none"
aria-hidden
className={props.className}
>
<path
stroke="#fff"
strokeOpacity=".95"
strokeWidth="1.2"
d="m5.5 8 4.5 4.5L14.5 8"
/>
</svg>
)
}
const DiscordIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none">
<path
fill="#fff"
fillOpacity=".95"
d="M16.248 3.839a14.66 14.66 0 0 0-3.714-1.167c-.16.29-.347.678-.476.988a13.635 13.635 0 0 0-4.115 0c-.129-.31-.32-.699-.481-.988a14.611 14.611 0 0 0-3.717 1.17C1.395 7.393.758 10.856 1.076 14.27a14.845 14.845 0 0 0 4.555 2.334c.367-.505.694-1.04.975-1.606a9.592 9.592 0 0 1-1.536-.748c.13-.095.255-.195.377-.298 2.962 1.386 6.18 1.386 9.106 0 .123.103.25.203.377.298a9.566 9.566 0 0 1-1.54.75c.282.563.608 1.101.976 1.606a14.818 14.818 0 0 0 4.558-2.336c.373-3.957-.639-7.389-2.676-10.431ZM7.01 12.17c-.89 0-1.618-.83-1.618-1.841 0-1.01.713-1.842 1.618-1.842.905 0 1.634.83 1.618 1.842.002 1.01-.713 1.84-1.618 1.84Zm5.98 0c-.889 0-1.618-.83-1.618-1.841 0-1.01.713-1.842 1.618-1.842.905 0 1.634.83 1.618 1.842 0 1.01-.713 1.84-1.618 1.84Z"
/>
</svg>
)
}
const XIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none">
<path
fill="#fff"
fillOpacity=".95"
fillRule="evenodd"
d="M8.428 10.797 3.02 3h4.183L17 16.984h-4.28l-3.673-5.295L4.407 17H3l5.428-6.203Zm4.785 5.256L4.804 3.93h1.909l8.492 12.122h-1.992Z"
clipRule="evenodd"
/>
<path
fill="#fff"
fillOpacity=".95"
d="m16.625 3.014-5.223 5.98-.62-.887 4.456-5.094 1.387.001Z"
/>
</svg>
)
}
const faqs = [
{
question: 'What is Keycard for?',
answer:
'Keycard is a secure hardware wallet for storing and managing your cryptocurrencies.',
},
{
question: 'Which assets does Keycard support?',
answer:
'Keycard supports a wide range of cryptocurrencies including Bitcoin, Ethereum, and many ERC-20 tokens.',
},
{
question: 'Are my funds safe if I lose my Keycard?',
answer:
'Yes, your funds are safe. You can recover your assets using your backup seed phrase.',
},
{
question: 'What wallets support Keycard?',
answer:
'Keycard is compatible with various software wallets. Please check our documentation for a full list.',
},
{
question: 'What happens if I lose access to my Keycard?',
answer: 'You can recover your assets using your backup seed phrase.',
},
]
const Faqs = () => {
return (
<section className="mx-auto max-w-[1352px]">
<div className="flex items-start justify-between">
<div className="flex flex-1 flex-col pt-[200px]">
<h1 className="font-lora text-32 text-white-95">
Something about faqs
</h1>
<Accordion.Root
className="max-w-[549px] flex-1 pt-14"
type="single"
defaultValue="item-1"
collapsible
>
{faqs.map((faq, index) => (
<Accordion.Item
key={index}
className={cx(
'overflow-hidden border-b border-dashed border-white-20 py-6 first:mt-0',
)}
value={`item-${index + 1}`}
>
<Accordion.Header className="flex">
<Accordion.Trigger
className={cx(
'group flex flex-1 cursor-default items-center justify-between text-left font-lora text-24 leading-none text-white-95 outline-none',
)}
>
{faq.question}
<div className="rounded-[10px] border border-white-12 bg-white-6 p-[6px]">
<ChevronDownIcon className="text-white-95 transition-transform duration-300 ease-[cubic-bezier(0.87,_0,_0.13,_1)] group-data-[state=open]:rotate-180" />
</div>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content
className={cx(
'overflow-hidden text-16 font-300 text-white-80 data-[state=closed]:animate-slideUp data-[state=open]:animate-slideDown',
)}
>
{faq.answer}
</Accordion.Content>
</Accordion.Item>
))}
</Accordion.Root>
</div>
<div className="flex flex-col items-end pt-[100px]">
<Image
src="/assets/faqs.png"
alt="FAQ Background"
width="516"
height="516"
/>
<div className="flex max-w-[320px] flex-col gap-6 rounded-28 border border-white-6 bg-white-3 p-6 pt-5">
<div className="flex flex-col gap-[6px]">
<p className="font-lora text-24 font-400 text-white-95">
Still have questions?
</p>
<p className="text-16 font-300 text-white-80">
Reach out to our team or engage with our community on Discord or
X.
</p>
</div>
<div className="flex gap-3">
<ButtonLink href="/" variant="secondary">
Get in touch
</ButtonLink>
<ButtonLink
href="https://discord.com"
variant="secondary"
className="inline-flex"
>
<DiscordIcon />
</ButtonLink>
<ButtonLink
href="https://x.com"
variant="secondary"
className="inline-flex"
>
<XIcon />
</ButtonLink>
</div>
</div>
</div>
</div>
</section>
)
}
export { Faqs }
+60
View File
@@ -0,0 +1,60 @@
'use client'
import { Environment, PresentationControls, useGLTF } from '@react-three/drei'
import { Canvas } from '@react-three/fiber'
import { ButtonLink } from '~components/button-link'
import { Model } from './model'
const Hero = () => {
useGLTF.preload('/assets/keycard-transformed.glb')
return (
<section className="relative flex w-full overflow-clip rounded-t-28 border border-white-6 bg-white-3 px-[72px] py-20">
<div className="container relative z-10 flex max-w-[434px] flex-col px-4">
<p className="pb-2 text-24 font-600 text-white-95">keycard</p>
<h1 className="flex pb-4 font-lora text-48 font-400">
Best in class <br /> secure element
</h1>
<p className="pb-8 text-20 font-300 text-white-80">
Something will say here about this product. Certainly, you don&apos;t
want to miss it.
</p>
<div className="flex space-x-4">
<ButtonLink href="/">Buy Keycard</ButtonLink>
<ButtonLink href="/" variant="secondary">
Learn more
</ButtonLink>
</div>
<p className="pt-6 text-16 font-300 text-white-60">Starts from $25</p>
</div>
<div className="min-h-full flex-1">
<Canvas shadows camera={{ position: [6, 0, 10], fov: 30 }}>
<ambientLight intensity={3.6} />
<directionalLight
color="white"
intensity={100}
position={[2, 0, 2]}
/>
<PresentationControls
global
config={{ mass: 2, tension: 500 }}
snap={{ mass: 4, tension: 150 }}
polar={[-Math.PI / 3, Math.PI / 3]}
azimuth={[-Math.PI / 1.4, Math.PI / 2]}
>
<Model
rotation={[0, Math.PI, 0]}
position={[0, 1, 0]}
scale={84.495}
/>
</PresentationControls>
<Environment preset="night" />
</Canvas>
</div>
</section>
)
}
export { Hero }
@@ -0,0 +1,194 @@
import { cx } from 'cva'
import Image from 'next/image'
type Props = {
className?: string
}
const Shield = (props: Props) => (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
className={props.className}
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_1252_12446)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.6 0.5V2.4062C11.246 2.42007 11.8458 2.45799 12.4 2.5276V1H13.6V2.74837C14.5768 2.99632 15.3706 3.39705 15.9868 4.01324C16.6547 4.68118 17.0695 5.55778 17.3109 6.65H19V7.85H17.5013C17.5527 8.33344 17.582 8.84997 17.5938 9.4H19.5V10.6H17.5938C17.5778 11.3443 17.5299 12.0273 17.4384 12.65H19V13.85H17.1836C16.9326 14.7131 16.5495 15.424 15.9868 15.9868C15.3706 16.603 14.5768 17.0037 13.6 17.2516V19H12.4V17.4724C11.8458 17.542 11.246 17.5799 10.6 17.5938V19.5H9.4V17.5938C8.75401 17.5799 8.15424 17.542 7.6 17.4724V19H6.4V17.2516C5.42319 17.0037 4.62942 16.603 4.01324 15.9868C3.45048 15.424 3.06744 14.7131 2.81637 13.85H1L1 12.65H2.56163C2.47009 12.0273 2.42218 11.3443 2.4062 10.6H0.5V9.4H2.4062C2.41801 8.84997 2.44725 8.33344 2.49865 7.85H1L1 6.65H2.68909C2.93052 5.55778 3.34529 4.68118 4.01324 4.01324C4.62942 3.39705 5.42319 2.99632 6.4 2.74837V1H7.6V2.5276C8.15424 2.45799 8.75401 2.42007 9.4 2.4062V0.5H10.6ZM3.6 10C3.6 7.25657 3.98658 5.73694 4.86176 4.86176C5.73694 3.98658 7.25657 3.6 10 3.6C12.7434 3.6 14.2631 3.98658 15.1382 4.86176C16.0134 5.73694 16.4 7.25657 16.4 10C16.4 12.7434 16.0134 14.2631 15.1382 15.1382C14.2631 16.0134 12.7434 16.4 10 16.4C7.25657 16.4 5.73694 16.0134 4.86176 15.1382C3.98658 14.2631 3.6 12.7434 3.6 10ZM6.6 9.6C6.6 8.83071 6.63205 8.27111 6.71996 7.8532C6.80568 7.4457 6.93406 7.22455 7.09159 7.08102C7.25554 6.93164 7.5208 6.80264 8.0029 6.7174C8.48731 6.63175 9.13076 6.6 10 6.6C10.8704 6.6 11.5102 6.63195 11.9905 6.72244C12.4649 6.81183 12.7244 6.9479 12.8882 7.11176C13.0521 7.27562 13.1882 7.53506 13.2776 8.00953C13.3681 8.48983 13.4 9.1296 13.4 10C13.4 10.8704 13.3681 11.5102 13.2776 11.9905C13.1882 12.4649 13.0521 12.7244 12.8882 12.8882C12.7244 13.0521 12.4649 13.1882 11.9905 13.2776C11.5102 13.3681 10.8704 13.4 10 13.4C9.12843 13.4 8.49237 13.3678 8.01611 13.2726C7.54962 13.1793 7.29458 13.0365 7.12942 12.8567C6.9585 12.6705 6.81633 12.3716 6.72427 11.8319C6.63186 11.2902 6.6 10.5712 6.6 9.6ZM10 5.4C9.11924 5.4 8.38769 5.43075 7.79397 5.53573C7.19795 5.64111 6.68196 5.83086 6.28341 6.19398C5.87844 6.56295 5.66307 7.04805 5.54566 7.60618C5.43045 8.15389 5.4 8.81929 5.4 9.6C5.4 10.5788 5.43064 11.3848 5.54136 12.0337C5.65242 12.6847 5.854 13.242 6.24558 13.6683C6.64292 14.101 7.16913 14.327 7.78077 14.4493C8.38263 14.5697 9.12157 14.6 10 14.6C10.8796 14.6 11.6148 14.5694 12.2127 14.4568C12.8163 14.3431 13.3381 14.1354 13.7368 13.7368C14.1354 13.3381 14.3431 12.8163 14.4568 12.2127C14.5694 11.6148 14.6 10.8796 14.6 10C14.6 9.1204 14.5694 8.38517 14.4568 7.78735C14.3431 7.18369 14.1354 6.66188 13.7368 6.26324C13.3381 5.8646 12.8163 5.65692 12.2127 5.54319C11.6148 5.43055 10.8796 5.4 10 5.4Z"
fill="white"
fillOpacity="0.6"
/>
</g>
<defs>
<clipPath id="clip0_1252_12446">
<rect width="20" height="20" fill="white" />
</clipPath>
</defs>
</svg>
)
const Github = (props: Props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="none"
className={props.className}
>
<circle cx="10" cy="10" r="8" fill="#010101" />
<path
fill="#fff"
fillRule="evenodd"
d="M11.876 17.78a8.024 8.024 0 0 1-3.734.004v-.27a503.28 503.28 0 0 1-.008-1.826c-1.884.356-2.371-.471-2.521-.904a2.794 2.794 0 0 0-.77-1.086c-.262-.144-.637-.5-.009-.51.24.027.47.113.671.25.2.137.366.322.483.539a1.59 1.59 0 0 0 1.588.833c.21-.025.412-.093.596-.199.032-.39.202-.756.478-1.028-1.669-.192-3.412-.856-3.412-3.797a3.036 3.036 0 0 1 .768-2.066 2.828 2.828 0 0 1 .075-2.038s.628-.202 2.063.788a6.902 6.902 0 0 1 3.75 0c1.434-1 2.062-.788 2.062-.788.278.644.305 1.373.075 2.038.506.562.781 1.302.769 2.066 0 2.951-1.753 3.605-3.422 3.797.18.186.317.41.404.655.088.245.122.507.102.768 0 .684-.004 1.722-.007 2.417l-.001.356Z"
clipRule="evenodd"
/>
</svg>
)
const Infinity = (props: Props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="none"
className={props.className}
>
<path
fill="#fff"
fillOpacity=".95"
d="M5.26 5.5c1.668 0 2.934.884 3.798 1.8.37.36.68.753.957 1.113.247-.36.494-.688.927-1.113.864-.916 2.13-1.8 3.828-1.8C17.117 5.5 19 7.496 19 9.984c0 2.52-1.883 4.516-4.23 4.516-1.698 0-2.964-.884-3.828-1.767a9.573 9.573 0 0 1-.927-1.146c-.277.36-.586.753-.957 1.146-.864.883-2.13 1.767-3.797 1.767C2.883 14.5 1 12.504 1 9.984 1 7.496 2.883 5.5 5.26 5.5Zm-2.5 4.484c0 1.472 1.111 2.65 2.5 2.65 1.02 0 1.853-.556 2.563-1.276a9.66 9.66 0 0 0 1.08-1.374c-.277-.393-.586-.851-1.08-1.342-.679-.72-1.543-1.277-2.562-1.277-1.39 0-2.501 1.179-2.501 2.619Zm14.48 0c0-1.44-1.111-2.619-2.47-2.619-1.05 0-1.914.557-2.593 1.277-.556.556-.927 1.145-1.08 1.342.277.425.617.883 1.08 1.374.71.72 1.543 1.276 2.593 1.276 1.359 0 2.47-1.178 2.47-2.65Z"
/>
</svg>
)
const features = [
{
title: 'Best in class security',
description:
'Our secure element has the highest level of security EAL6+ certified by Common Criteria.',
image: '/assets/feature-keycard.png',
badge: { icon: Shield, text: 'EAL 6+', gradient: true },
className: 'row-span-2 col-span-1',
},
{
title: '100% open source',
description:
'We have nothing to hide! Our software, hardware and construction is fully open source.',
button: { icon: Github, text: 'View on GitHub' },
className: 'col-span-1',
},
{
title: 'Fully airgapped',
description:
"Through KeyPro's camera or Keycard's contactless nature, our products are truly airgapped.",
image: '/assets/feature-keycard-pro.png',
badge: { text: '0 cables' },
className: 'row-span-2 col-span-1 flex-col-reverse',
gradient: true,
},
{
title: 'Made to last',
description:
'Your keycard has a life expectancy of 25+ years, resists water and dust. It will still securely store your keys.',
badge: { text: '25+ years' },
className: 'col-span-1',
},
{
title: 'Easy to backup',
description:
'Create cards to back up your master key and store them in a safe place instead of the typical piece of paper.',
badge: { icon: Infinity, text: 'backups' },
className: 'col-span-1',
},
{
title: 'Discreet',
description:
'With its light, small and discreet form factor your Keycard can go unnoticed in your wallet.',
badge: { text: 'Credit card format' },
className: 'col-span-1',
},
]
const UnderlinedWord = ({ children }: { children: React.ReactNode }) => (
<div className="relative inline-block">
<span className="relative z-10 [text-shadow:_-3px_2px_black,_4px_1px_black,_2px_0px_black,_3px_2px_black]">
{children}
</span>
<div className="absolute bottom-1 left-0 -z-10 h-px w-full bg-orange" />
</div>
)
const KeycardFeatures = () => {
return (
<section className="mx-auto max-w-[1352px] pt-[200px]">
<h1 className="max-w-[665px] font-lora text-32 text-white-95">
Join the <UnderlinedWord>open source</UnderlinedWord> revolution of the
most <UnderlinedWord>modular</UnderlinedWord> and{' '}
<UnderlinedWord>future proof</UnderlinedWord> hardware wallet system
ever conceived.
</h1>
<div className="grid grid-cols-4 gap-6 pt-20">
{features.map((feature, index) => (
<div
key={index}
className={cx([
'relative flex flex-col justify-between overflow-clip rounded-28 bg-white-3 outline outline-white-6',
feature.className,
])}
>
{feature.image && (
<div className={`flex items-center justify-center`}>
<Image
src={feature.image}
alt={feature.title}
className="size-full rounded-12 object-cover"
width="500"
height="500"
/>
</div>
)}
{feature.gradient && (
<div className="absolute -bottom-10 left-0 h-1/2 w-full rounded-12 bg-gradient-to-b from-[transparent] to-white-6" />
)}
<div className="p-6">
<div>
<h3 className="mb-[6px] font-lora text-24 font-400">
{feature.title}
</h3>
{feature.description && (
<p className="mb-6 text-16 font-300 text-white-60">
{feature.description}
</p>
)}
{feature.badge && (
<div
className={cx([
'flex w-fit items-center gap-[6px] rounded-[32px] py-2 pl-[14px] pr-4 text-16 font-300 text-white-95 outline outline-white-12',
feature.badge.gradient &&
'bg-gradient-to-b from-[transparent] to-white-12',
])}
>
{feature.badge.icon && <feature.badge.icon />}
<span>{feature.badge.text}</span>
</div>
)}
{feature.button && (
<button className="mt-4 flex items-center gap-[6px] rounded-12 bg-white-100 px-4 py-2 text-16 font-500 text-dark-100">
<span>{feature.button.text}</span>
<feature.button.icon />
</button>
)}
</div>
</div>
</div>
))}
</div>
</section>
)
}
export { KeycardFeatures }
@@ -0,0 +1,44 @@
import { ButtonLink } from '~components/button-link'
import Image from 'next/image'
const KeycardProSection = () => {
return (
<section className="relative flex items-center overflow-hidden rounded-b-28 border border-t-0 border-white-6 bg-white-3 backdrop-blur-[20px]">
<div className="flex max-h-[510px] flex-1 items-start justify-center overflow-hidden">
<Image
src="/assets/keycard-pro.png"
alt="Keycard Pro Hardware Wallet"
width="900"
height="600"
className="h-full w-auto object-contain"
priority
/>
</div>
<div className="container relative z-10 flex max-w-[434px] flex-col py-20">
<p className="pb-2 text-24 font-600 text-white-95">
keycard <span className="font-200">pro</span>
</p>
<h1 className="flex pb-4 font-lora text-48 font-400">
A modular
<br />
hardware wallet
</h1>
<p className="pb-8 text-20 font-300 text-white-80">
Something will say here about this product.
<br />
Certainly you don&apos;t want to miss it
</p>
<div className="flex space-x-4">
<ButtonLink href="/">Get notified</ButtonLink>
<ButtonLink href="/" variant="secondary">
Learn more
</ButtonLink>
</div>
<p className="pt-6 text-16 font-300 text-white-60">Coming 2025</p>
</div>
</section>
)
}
export { KeycardProSection }
+59
View File
@@ -0,0 +1,59 @@
import { useGLTF } from '@react-three/drei'
import { useFrame } from '@react-three/fiber'
import React, { useRef } from 'react'
import * as THREE from 'three'
import { GLTF } from 'three-stdlib'
type GLTFResult = GLTF & {
nodes: {
Cube002: THREE.Mesh
Cube002_1: THREE.Mesh
Cube002_2: THREE.Mesh
}
materials: {
['Card.image.001']: THREE.MeshStandardMaterial
['Card.light.001']: THREE.MeshStandardMaterial
['Material.001']: THREE.MeshStandardMaterial
}
}
export function Model(props: JSX.IntrinsicElements['group']) {
const { nodes, materials } = useGLTF(
'assets/keycard-transformed.glb',
) as GLTFResult
const group = useRef<THREE.Group>(null)
useFrame(state => {
const t = state.clock.getElapsedTime()
if (group.current) {
// Floating up and down animation
group.current.position.y = Math.sin(t * 2) * 0.1
// Smooth rotation on all axes
group.current.rotation.x = Math.cos(t / 4) / 20
group.current.rotation.y = Math.sin(t / 2) / 20
group.current.rotation.z = Math.sin(t / 4) / 30
}
})
return (
<group ref={group} {...props} dispose={null}>
<group>
<mesh
geometry={nodes.Cube002.geometry}
material={materials['Card.image.001']}
/>
<mesh
geometry={nodes.Cube002_1.geometry}
material={materials['Card.light.001']}
/>
<mesh
geometry={nodes.Cube002_2.geometry}
material={materials['Material.001']}
/>
</group>
</group>
)
}
+19
View File
@@ -0,0 +1,19 @@
import { BuyCards } from '~/app/(homepage)/_components/buy-cards'
import { ComparisonTable } from '~/app/(homepage)/_components/comparison-table'
import { Faqs } from '~/app/(homepage)/_components/faqs'
import { Hero } from '~/app/(homepage)/_components/hero'
import { KeycardFeatures } from '~/app/(homepage)/_components/keycard-features'
import { KeycardProSection } from '~/app/(homepage)/_components/keycard-pro-section'
export default async function HomePage() {
return (
<div className="relative w-full">
<Hero />
<KeycardProSection />
<KeycardFeatures />
<ComparisonTable />
<Faqs />
<BuyCards />
</div>
)
}
+42
View File
@@ -0,0 +1,42 @@
import { cva, cx } from 'cva'
import { Link } from './link'
type Props = {
variant?: 'primary' | 'secondary'
children: React.ReactNode
active?: boolean
} & React.ComponentProps<typeof Link>
const buttonStyles = cva({
base: 'flex cursor-pointer select-none items-center rounded-12 border text-white-95 px-[14px] pb-[10px] pt-2 backdrop-blur-[20px] transition-colors',
variants: {
variant: {
primary: 'bg-orange border-[transparent] hover:bg-orange-dark',
secondary: 'bg-white-6 border-white-12 hover:bg-white-12',
},
active: {
true: '',
false: '',
},
},
compoundVariants: [
{
variant: 'secondary',
active: true,
className: 'bg-dark-60 ',
},
],
})
const ButtonLink = (props: Props) => {
const { children, variant = 'primary', className, active, ...rest } = props
return (
<Link
className={cx([buttonStyles({ variant, active }), className])}
{...rest}
>
{children}
</Link>
)
}
export { ButtonLink }
+28
View File
@@ -0,0 +1,28 @@
import { ROUTES } from '~/config/routes'
import { Logo } from '~components/logo'
import Link from 'next/link'
import { Section } from './section'
export const Footer = () => {
return (
<footer className="rounded-28 bg-orange pt-8">
<div className="hidden grid-cols-4 divide-x divide-dashed divide-white-20 lg:grid lg:grid-cols-6">
<div className="flex items-start border-b border-dashed border-white-20 p-6 pr-0 pt-0">
<Link href="/">
<Logo />
</Link>
</div>
{Object.entries(ROUTES).map(([title, links]) => (
<Section key={title} title={title} routes={links} />
))}
</div>
<div className="flex items-center justify-between px-6 py-4">
<span className="text-16 font-300 text-white-80">© Keycard 2024</span>
<p className="text-16 font-300 text-white-80">
Keycard is part of the Institute of Free Technology
</p>
</div>
</footer>
)
}
+55
View File
@@ -0,0 +1,55 @@
import { Routes } from '~/config/routes'
import { Link } from '~components/link'
type Props = {
title: string
routes: Routes
}
const ExternalIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="none"
className="-mb-0.5 transition-transform group-hover:-translate-y-px group-hover:translate-x-px"
>
<path
fill="currentColor"
fillOpacity=".6"
fillRule="evenodd"
d="M11.551 7.6H7V6.4h6.6V13h-1.2V8.448l-6.476 6.476-.848-.848L11.55 7.6Z"
clipRule="evenodd"
/>
</svg>
)
const Section = (props: Props) => {
const { title, routes } = props
return (
<div className="relative border-b border-dashed border-white-20">
<div className="relative grid gap-6 px-6 pb-12">
<p className="text-12 uppercase text-white-80">{title}</p>
<ul className="grid gap-1">
{routes.map(route => {
const external = route.href.startsWith('http')
return (
<li key={route.name}>
<Link href={route.href} className="group flex items-center">
<span className="text-16 font-500 text-white-95 transition-colors group-hover:text-white-60 group-data-[active='true']:text-white-40">
{route.name}
</span>
{external && <ExternalIcon />}
</Link>
</li>
)
})}
</ul>
</div>
</div>
)
}
export { Section }
+30
View File
@@ -0,0 +1,30 @@
import NextLink from 'next/link'
import { forwardRef } from 'react'
const Link = (
props: React.ComponentPropsWithRef<typeof NextLink>,
ref: React.Ref<HTMLAnchorElement>,
) => {
const url = typeof props.href === 'string' ? props.href : props.href.pathname!
const external = url?.startsWith('http')
if (external) {
return (
<a
{...props}
href={url}
target="_blank"
rel="noopener noreferrer"
ref={ref}
>
{props.children}
</a>
)
}
return <NextLink {...props} ref={ref} />
}
const _Link = forwardRef(Link)
export { _Link as Link }
+18
View File
@@ -0,0 +1,18 @@
const Logo = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="23" height="40" fill="none">
<path
fill="#fff"
fillOpacity=".95"
d="m18.352 40-4.767-6.884-1.43 1.662V40h-4.29V15.905h4.29v13.531h.239l5.243-6.172h5.005l-6.197 7.122L23 40h-4.648Z"
/>
<path
fill="#fff"
fillOpacity=".95"
d="M9.892 19.822C4.409 19.822 0 15.312 0 9.852 0 4.392 4.41 0 9.892 0c5.482 0 9.891 4.392 9.891 9.852.12 5.46-4.41 9.97-9.891 9.97Zm0-15.905c-3.337 0-5.96 2.73-5.96 5.935 0 3.323 2.742 5.934 5.96 5.934 3.336 0 5.958-2.73 5.958-5.934.12-3.205-2.622-5.935-5.958-5.935Z"
/>
</svg>
)
}
export { Logo }
+75
View File
@@ -0,0 +1,75 @@
'use client'
import {
motion,
useMotionValueEvent,
useScroll,
useTransform,
} from 'framer-motion'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useState } from 'react'
import { ButtonLink } from './button-link'
import { Logo } from './logo'
const NAV_BAR_HEIGHT = 92
const Navbar = () => {
const pathname = usePathname()
const active = pathname!.includes('/buy-keycard')
const [variant, setVariant] = useState<'primary' | 'secondary'>('secondary')
const { scrollY } = useScroll()
const backgroundColor = useTransform(
scrollY,
[0, NAV_BAR_HEIGHT],
['transparent', 'rgba(255, 255, 255, 0.03)'],
)
const backdropFilter = useTransform(
scrollY,
[0, NAV_BAR_HEIGHT],
['blur(0px)', 'blur(20px)'],
)
useMotionValueEvent(scrollY, 'change', latest => {
if (latest > NAV_BAR_HEIGHT) {
return setVariant('primary')
}
return setVariant('secondary')
})
return (
<motion.nav
className="sticky top-0 z-30 flex items-center justify-between p-6 text-white-95"
style={{
backgroundColor,
backdropFilter,
}}
>
<Link href="/">
<Logo />
</Link>
<div className="flex items-center space-x-6">
<Link
href="/keycard"
className="rounded-12 border border-[transparent] px-[14px] pb-[10px] pt-2 transition-colors hover:border-white-6 hover:bg-white-6"
>
Keycard
</Link>
<Link
href="/keycard-pro"
className="rounded-12 border border-[transparent] px-[14px] pb-[10px] pt-2 transition-colors hover:border-white-6 hover:bg-white-6"
>
Keycard Pro
</Link>
<ButtonLink href="/buy-keycard" variant={variant} active={active}>
Buy Keycard
</ButtonLink>
</div>
</motion.nav>
)
}
export { Navbar }
+34
View File
@@ -0,0 +1,34 @@
import * as Tooltip from '@radix-ui/react-tooltip'
type Props = {
children: React.ReactNode
label: string
side?: Tooltip.TooltipContentProps['side']
hidden?: boolean
}
const TooltipBase = (props: Props) => {
const { children, label, hidden, side = 'top' } = props
return (
<Tooltip.Provider delayDuration={300}>
<Tooltip.Root>
<Tooltip.Trigger asChild>{children}</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content
hidden={hidden}
className="relative select-none rounded-12 border bg-white-95 px-3 py-2 text-16 font-500 leading-none text-dark-100 backdrop-blur-[20px] will-change-[transform,opacity] data-[state=delayed-open]:animate-slideDownAndFade"
side={side}
sideOffset={4}
align="center"
>
{label}
<Tooltip.Arrow className="fill-white-95" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
)
}
export { TooltipBase as Tooltip }
+8 -2
View File
@@ -2,6 +2,8 @@ import { cx } from 'cva'
import type { Metadata } from 'next'
import { Inter, Lora } from 'next/font/google'
import './globals.css'
import { Footer } from '~components/footer/footer'
import { Navbar } from '~components/nav-bar'
const lora = Lora({
variable: '--font-lora',
@@ -34,10 +36,14 @@ export default function RootLayout({ children }: Props) {
className={cx(
lora.variable,
inter.variable,
'bg-dark-100 font-inter text-white-100 antialiased',
'bg-dark-100 p-2 font-inter text-white-100 antialiased',
)}
>
{children}
<Navbar />
<div className="flex justify-center">
<div className="w-full max-w-[1512px]">{children}</div>
</div>
<Footer />
</body>
</html>
)
-10
View File
@@ -1,10 +0,0 @@
export default function Home() {
return (
<div>
<h1 className="font-lora text-48 font-400">Keycard</h1>
<a className="rounded-12 bg-orange px-4 py-2 text-16 font-500 text-white-100">
Buy Keycard
</a>
</div>
)
}
+30
View File
@@ -0,0 +1,30 @@
export const ROUTES = {
Products: [
{ name: 'Keycard', href: '/keycard' },
{ name: 'Keycard Pro', href: '/keycard-pro' },
],
Info: [
{ name: 'Get started', href: '/get-started' },
{ name: 'Documentation', href: '/docs' },
{ name: 'Blog', href: '/blog' },
],
Contacts: [
{ name: 'Get in touch', href: '/contact' },
{ name: 'Discord', href: 'https://discord.com' },
{ name: 'X', href: 'https://x.com' },
],
Legal: [
{ name: 'Privacy policy', href: '/privacy-policy' },
{ name: 'Terms of use', href: '/terms-of-use' },
],
Network: [
{ name: 'status.app', href: 'https://status.app' },
{
name: 'Logos',
href: 'https://logos.co',
},
{ name: 'Codex', href: 'https://codex.storage/' },
],
} as const
export type Routes = (typeof ROUTES)[keyof typeof ROUTES]
+55 -1
View File
@@ -15,9 +15,11 @@ export default {
},
fontWeight: {
200: '200',
300: '300',
400: '400',
500: '500',
600: '600',
},
fontSize: {
@@ -28,6 +30,27 @@ export default {
letterSpacing: '-0.06rem',
},
],
32: [
'2rem',
{
lineHeight: '2.75rem',
letterSpacing: '-0.06rem',
},
],
24: [
'1.5rem',
{
lineHeight: '2rem',
letterSpacing: '-0.02rem',
},
],
20: [
'1.25rem',
{
lineHeight: '1.75rem',
letterSpacing: '-0.02rem',
},
],
16: [
'1rem',
{
@@ -35,10 +58,18 @@ export default {
letterSpacing: '-0.02rem',
},
],
12: [
'0.75rem',
{
lineHeight: '1.25rem',
letterSpacing: '-0.02rem',
},
],
},
colors: {
orange: 'rgba(255, 100, 0, 1)',
'orange-dark': 'rgba(225, 88, 0, 1)',
dark: {
100: 'rgba(1, 1, 1, 1)',
60: 'rgba(1, 1, 1, 0.6)',
@@ -60,11 +91,34 @@ export default {
borderRadius: {
12: '12px',
28: '28px',
full: '9999px',
},
opacity: {},
extend: {},
extend: {
keyframes: {
slideDownAndFade: {
from: { opacity: '0', transform: 'translateY(-2px)' },
to: { opacity: '1', transform: 'translateY(0)' },
},
slideDown: {
from: { height: '0px' },
to: { height: 'var(--radix-accordion-content-height)' },
},
slideUp: {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0px' },
},
},
animation: {
slideDownAndFade:
'slideDownAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
slideDown: 'slideDown 300ms cubic-bezier(0.87, 0, 0.13, 1)',
slideUp: 'slideUp 300ms cubic-bezier(0.87, 0, 0.13, 1)',
},
},
},
plugins: [],
+1
View File
@@ -34,6 +34,7 @@
}
],
"paths": {
"~components/*": ["./src/app/_components/*"],
"~/*": ["./src/*"]
}
},