Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f142068fc0 | ||
|
|
dcdcd02044 | ||
|
|
438ee4c3bc | ||
|
|
929c882d3f | ||
|
|
a8812b4da5 | ||
|
|
646e36e263 | ||
|
|
76b0887165 | ||
|
|
f16e7704c4 | ||
|
|
a27a5484f0 | ||
|
|
d8139c4b35 | ||
|
|
3c3cac949d | ||
|
|
def1c8e155 | ||
|
|
79e0d52600 | ||
|
|
5569b04403 | ||
|
|
5bf1c9debe | ||
|
|
edd85f62d3 | ||
|
|
be7435c9b2 | ||
|
|
d3dbffb2c5 | ||
|
|
51c22329ea | ||
|
|
a3ec0f5ece |
@@ -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",
|
||||
|
||||
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 462 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 286 KiB |
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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'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'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 }
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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]
|
||||
@@ -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: [],
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"~components/*": ["./src/app/_components/*"],
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
|
||||