Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ae975fbfc | ||
|
|
4f21bb87c6 | ||
|
|
37f474cdfd | ||
|
|
30a684e164 | ||
|
|
270224fb77 | ||
|
|
29ec47908b | ||
|
|
5328aef1fb | ||
|
|
bc63b4d9d1 | ||
|
|
76ca4ca576 | ||
|
|
264fafe1de | ||
|
|
8ff442a611 | ||
|
|
1f5e69f31c | ||
|
|
830afae870 | ||
|
|
f1703349c0 | ||
|
|
dd75005415 | ||
|
|
d299aa08d5 | ||
|
|
c960f368cb |
@@ -1 +1 @@
|
||||
pnpm dlx lint-staged
|
||||
pnpx lint-staged
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
20.18.0
|
||||
@@ -1,7 +0,0 @@
|
||||
; https://pnpm.io/npmrc#node-options
|
||||
; why: https://github.com/vercel/next.js/discussions/70423
|
||||
node-options=--network-family-autoselection-attempt-timeout=1000
|
||||
; https://pnpm.io/npmrc#node-version
|
||||
node-version=20.18.0
|
||||
; https://pnpm.io/npmrc#engine-strict
|
||||
engine-strict=true
|
||||
@@ -25,7 +25,7 @@
|
||||
// note: config https://github.com/microsoft/vscode-js-debug/blob/e620efe1841742402a332b532df2aa2ec95361f6/OPTIONS.md
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "NODE_OPTIONS='' pnpm dev --port 3030",
|
||||
"command": "yarn dev -- --port 3030",
|
||||
"serverReadyAction": {
|
||||
"pattern": "\\s- Local:\\s+(https?://\\S+)",
|
||||
"action": "startDebugging",
|
||||
|
||||
@@ -5,9 +5,9 @@ title: Card Setup
|
||||
|
||||
# Preparing the card
|
||||
|
||||
Keycard ships with all applets fully installed but not initialized. The most convenient way to initialize and manage the card is to install the [Keycard Connect](https://github.com/keycard-tech/keycard-connect) Android app. It allows to fully initialize a card with pairing key, PIN, PUK and BIP39 seed. It also provides basic functionality like changing credentials, changing or removing keys and clear pairing slots. The last functionality is especially useful if you are integrating with Keycard and accidentally fill all pairing slots. Using the "Unpair others" button will remove all pairing except the Keycard Connect's one.
|
||||
Keycard ships with all applets fully installed but not initialized. The most convenient way to initialize and manage the card is to install the [Keycard Connect](https://github.com/status-im/keycard-connect) Android app. It allows to fully initialize a card with pairing key, PIN, PUK and BIP39 seed. It also provides basic functionality like changing credentials, changing or removing keys and clear pairing slots. The last functionality is especially useful if you are integrating with Keycard and accidentally fill all pairing slots. Using the "Unpair others" button will remove all pairing except the Keycard Connect's one.
|
||||
|
||||
In some cases you might want to reset the applet status completely for testing purposes. This is done by reinstalling the applet, which can be accomplished using the [Keycard CLI](https://github.com/keycard-tech/keycard-cli). To use the Keycard CLI, a USB reader is needed to be able to communicate with the card. The repository contains example scripts for applet reinstallation.
|
||||
In some cases you might want to reset the applet status completely for testing purposes. This is done by reinstalling the applet, which can be accomplished using the [Keycard CLI](https://github.com/status-im/keycard-cli). To use the Keycard CLI, a USB reader is needed to be able to communicate with the card. The repository contains example scripts for applet reinstallation.
|
||||
|
||||
## Card requirements
|
||||
|
||||
|
||||
@@ -30,5 +30,6 @@ The current version of the applet is 3.1. This documentation applies to said ver
|
||||
You are at the right place to find informations about Keycard and get you going.
|
||||
|
||||
If you need more and want to discuss:
|
||||
- to check or raise issues/bugs/feature requests please do so in our [GitHub repos](resources)
|
||||
- get in touch with the team by [email](mailto:support@keycard.tech)
|
||||
- for live discussion and help, go to [Keycard Status channel](https://join.status.im/chat/public/status-keycard)
|
||||
- to check or raise issues/bugs/feature requests please do so in our [GitHub repos](https://keycard.tech/docs/resources.html)
|
||||
- check our latest Keycard news on [Discuss](https://discuss.status.im/c/keycard)
|
||||
|
||||
@@ -9,15 +9,15 @@ Here you find the links to all repositories and tools related to Keycard develop
|
||||
|
||||
## GitHub repositories
|
||||
|
||||
* [Javacard applet](https://github.com/keycard-tech/status-keycard)
|
||||
* [Java SDK](https://github.com/keycard-tech/status-keycard-java)
|
||||
* [Go SDK](https://github.com/keycard-tech/keycard-go/)
|
||||
* [Swift SDK](https://github.com/keycard-tech/Keycard.swift)
|
||||
* [Keycard on Ledger](https://github.com/keycard-tech/keycard-ledger)
|
||||
* [React Native library](https://github.com/keycard-tech/react-native-status-keycard)
|
||||
* [Go Status library](https://github.com/keycard-tech/status-keycard-go)
|
||||
* [Javacard applet](https://github.com/status-im/status-keycard)
|
||||
* [Java SDK](https://github.com/status-im/status-keycard-java)
|
||||
* [Go SDK](https://github.com/status-im/keycard-go/)
|
||||
* [Swift SDK](https://github.com/status-im/Keycard.swift)
|
||||
* [Keycard on Ledger](https://github.com/status-im/keycard-ledger)
|
||||
* [React Native library](https://github.com/status-im/react-native-status-keycard)
|
||||
* [Go Status library](https://github.com/status-im/status-keycard-go)
|
||||
|
||||
## Tools
|
||||
|
||||
* [Keycard CLI](https://github.com/keycard-tech/keycard-cli)
|
||||
* [Keycard Desktop](https://github.com/choppu/keycard-desktop)
|
||||
* [Keycard CLI](https://github.com/status-im/keycard-cli)
|
||||
* [Keycard Desktop](https://github.com/choppu/keycard-desktop)
|
||||
@@ -33,10 +33,10 @@ When however the data to be signed contains `uint256 amount` and `address curren
|
||||
|
||||

|
||||
|
||||
If the currency field address is considered to be the address of an ERC20 token. If this address is zero, the amount is interpreted as plain ETH. This transaction format is used by the [Keycard Payment Network](https://github.com/keycard-tech/payment-network-apps) project.
|
||||
If the currency field address is considered to be the address of an ERC20 token. If this address is zero, the amount is interpreted as plain ETH. This transaction format is used by the [Keycard Payment Network](https://github.com/status-im/payment-network-apps) project.
|
||||
|
||||
Another example of a custom screen, is the one displayed for the [Keycard Redeem](https://github.com/keycard-tech/keycard-redeem) project. In this case we recognize the presence of the fields `address recipient` and `bytes32 code` field as a redeem request and we display the recipient field address clearly in the signature screen. If the recipient field in account managed by Status, instead of the plain address, the account name is shown. See screenshot below
|
||||
Another example of a custom screen, is the one displayed for the [Keycard Redeem](https://github.com/status-im/keycard-redeem) project. In this case we recognize the presence of the fields `address recipient` and `bytes32 code` field as a redeem request and we display the recipient field address clearly in the signature screen. If the recipient field in account managed by Status, instead of the plain address, the account name is shown. See screenshot below
|
||||
|
||||

|
||||
|
||||
One thing to keep in mind however, is that these custom screens are only there to provide better UX for some specific use case we are aware of. Any unrecognized message format can still be displayed generically and signed. If you are implementing a web3 browser you can adopt any strategy you prefer on this. Conversely, if you are developing SmartContract and dApps, you might want consider naming the fields of the message in a way that will trigger a specific interface on a browser. For example if your use case scenario involve transferring a certain `amount` of `currency` in a transaction, naming the message fields like that will show an easy to understand screen in Status.
|
||||
One thing to keep in mind however, is that these custom screens are only there to provide better UX for some specific use case we are aware of. Any unrecognized message format can still be displayed generically and signed. If you are implementing a web3 browser you can adopt any strategy you prefer on this. Conversely, if you are developing SmartContract and dApps, you might want consider naming the fields of the message in a way that will trigger a specific interface on a browser. For example if your use case scenario involve transferring a certain `amount` of `currency` in a transaction, naming the message fields like that will show an easy to understand screen in Status.
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Keycard - Privacy Policy
|
||||
title: Keycard Presale Website - Privacy Policy
|
||||
---
|
||||
|
||||
This Privacy Policy is intended to inform users of our approach to privacy in respect of this website (“**Website**”). In this regard, if you are visiting our Website, this Privacy Policy applies to you.
|
||||
@@ -24,7 +24,7 @@ In this regard, we collect and process the following personal data from your use
|
||||
|
||||
* **IP address**: As part of such use of the Website we briefly process your IP address but we have no way of identifying you. We however have a legitimate interest in processing such IP addresses to ensure the technical functionality and enhance the security measures of the Website. This IP address is not stored by us over time.
|
||||
|
||||
* **Collection of email addresses**: We only collect your email address on our website for the purpose of sending you information in relation to the development of and the potential sale of Keycard Shell. Your email address is securely stored on our self-hosted servers, and we have implemented measures to protect such personal data by securing it with appropriate technical and organisational measures. We will not share such personal data to third parties without your consent, except as required by law. We will only collect and process your email address on the basis of you providing your consent for us to do so. We will retain your email address for as long as necessary to provide you with updates about Keycard Shell, unless you withdraw your consent or request deletion earlier
|
||||
* **Collection of email addresses**: We only collect your email address on our website for the purpose of sending you information in relation to the development of and the potential sale of Keycard Pro. Your email address is securely stored on our self-hosted servers, and we have implemented measures to protect such personal data by securing it with appropriate technical and organisational measures. We will not share such personal data to third parties without your consent, except as required by law. We will only collect and process your email address on the basis of you providing your consent for us to do so. We will retain your email address for as long as necessary to provide you with updates about Keycard Pro, unless you withdraw your consent or request deletion earlier
|
||||
|
||||
## 3) Third party processing of personal data
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Keycard - Terms of Use
|
||||
title: Keycard Presale - Terms of Use
|
||||
---
|
||||
|
||||
These website terms of use (“**Website Terms of Use**”) are entered into by you and us, and they govern your access and use of this Website, including any content and functionality contained in the Website.
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import type * as React from 'react'
|
||||
|
||||
// why: https://github.com/mdx-js/mdx/issues/2487#issuecomment-2407661869
|
||||
// why: https://react.dev/blog/2024/04/25/react-19-upgrade-guide#the-jsx-namespace-in-typescript
|
||||
declare global {
|
||||
namespace JSX {
|
||||
// type ElementClass = React.JSX.ElementClass
|
||||
// type Element = React.JSX.Element
|
||||
type IntrinsicElements = React.JSX.IntrinsicElements
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'framer-motion' {
|
||||
export interface MotionProps {
|
||||
className?: string
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,6 @@ const config = {
|
||||
hostname: 'demo.ghost.io',
|
||||
pathname: '/**',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'news.keycard.tech',
|
||||
pathname: '/**',
|
||||
},
|
||||
],
|
||||
},
|
||||
transpilePackages: [
|
||||
|
||||
@@ -16,19 +16,11 @@
|
||||
"@radix-ui/react-accordion": "^1.2.1",
|
||||
"@radix-ui/react-checkbox": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.1",
|
||||
"@radix-ui/react-tooltip": "^1.1.3",
|
||||
"@react-three/drei": "^9.117.2",
|
||||
"@react-three/fiber": "9.0.0-rc.1",
|
||||
"@react-three/postprocessing": "^2.16.3",
|
||||
"@status-im/icons": "^1.0.2",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tanstack/react-query": "^5.59.17",
|
||||
"@tryghost/content-api": "^1.11.21",
|
||||
"@types/three": "^0.170.0",
|
||||
"@types/unist": "^3.0.3",
|
||||
"@vercel/analytics": "^1.4.1",
|
||||
"cva": "1.0.0-beta.1",
|
||||
"d3-time-format": "^4.1.0",
|
||||
"embla-carousel-react": "^8.3.1",
|
||||
@@ -37,16 +29,13 @@
|
||||
"github-slugger": "^2.0.0",
|
||||
"google-auth-library": "^9.14.2",
|
||||
"google-spreadsheet": "^4.1.4",
|
||||
"next": "15.0.3",
|
||||
"next": "15.0.2",
|
||||
"next-mdx-remote": "^5.0.0",
|
||||
"react": "19.0.0-rc.1",
|
||||
"react": "^18.3.1",
|
||||
"react-children-utilities": "^2.10.0",
|
||||
"react-dom": "19.0.0-rc.1",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.53.1",
|
||||
"server-only": "^0.0.1",
|
||||
"three": "^0.170.0",
|
||||
"three-stdlib": "^2.34.0",
|
||||
"ts-pattern": "^5.5.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
@@ -55,13 +44,13 @@
|
||||
"@types/d3-time-format": "^4.0.3",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/tryghost__content-api": "^1.3.17",
|
||||
"@types/unist": "^3.0.3",
|
||||
"embla-carousel": "^8.3.1",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-next": "15.0.3",
|
||||
"eslint-config-next": "15.0.2",
|
||||
"eslint-plugin-tailwindcss": "^3.17.5",
|
||||
"husky": "^9.1.6",
|
||||
"lint-staged": "^15.2.10",
|
||||
@@ -81,9 +70,6 @@
|
||||
"unified": "^10.1.2",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20.x"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,mjs,ts,tsx}": [
|
||||
"eslint --fix",
|
||||
@@ -95,14 +81,5 @@
|
||||
"*.{md,mdx}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@react-three/postprocessing": "patches/@react-three__postprocessing.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
diff --git a/dist/EffectComposer.js b/dist/EffectComposer.js
|
||||
index 335539d470b862efff4d34818a28d1ed78255005..c4e47fad5ea95dd28ef1570919bbcba0b1883ad8 100644
|
||||
--- a/dist/EffectComposer.js
|
||||
+++ b/dist/EffectComposer.js
|
||||
@@ -77,9 +77,9 @@ const EffectComposer = React.memo(
|
||||
useLayoutEffect(() => {
|
||||
const passes = [];
|
||||
if (group.current && instance.current && composer) {
|
||||
- const children2 = instance.current.objects;
|
||||
+ const children2 = instance.current.children;
|
||||
for (let i = 0; i < children2.length; i++) {
|
||||
- const child = children2[i];
|
||||
+ const child = children2[i].object;
|
||||
if (child instanceof Effect) {
|
||||
const effects = [child];
|
||||
if (!isConvolution(child)) {
|
||||
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 550 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 859 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 369 KiB |
|
Before Width: | Height: | Size: 360 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 903 KiB After Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 347 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 282 KiB |
|
Before Width: | Height: | Size: 232 KiB |
|
Before Width: | Height: | Size: 219 KiB |
|
Before Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 338 KiB |
|
Before Width: | Height: | Size: 313 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 173 KiB |
@@ -1,11 +1,10 @@
|
||||
import { KEYCARD_PRODUCTS } from '~/app/_constants/shopify/products'
|
||||
import { formatPrice } from '~/app/_utils/format-price'
|
||||
import { Button } from '~components/button'
|
||||
import { ButtonLink } from '~components/button-link'
|
||||
import { BuyKeycardDialog } from '~components/buy-keycard-dialog'
|
||||
import { GetNotifiedDialog } from '~components/get-notified-dialog'
|
||||
import { Image } from '~components/image'
|
||||
import { cx } from 'cva'
|
||||
import Image from 'next/image'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
image: string
|
||||
@@ -43,14 +42,14 @@ const Section = (props: Props) => {
|
||||
imageClassName,
|
||||
])}
|
||||
/>
|
||||
<div className="flex size-full flex-col items-center justify-end rounded-28 border border-white-8 bg-white-4 px-5 pb-20 pt-[300px] lg:px-10 lg:pb-[120px] xl:px-[98px]">
|
||||
<div className="container relative z-10 flex max-w-[434px] flex-col items-center justify-center px-2">
|
||||
<div className="flex size-full flex-col items-center justify-end rounded-28 border border-white-8 bg-white-3 px-5 pb-20 pt-[300px] lg:px-10 lg:pb-[120px] xl:px-[98px]">
|
||||
<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>
|
||||
<h2 className="flex pb-4 text-center font-lora text-32 font-400 lg:text-48">
|
||||
<h1 className="flex pb-4 text-center font-lora text-32 font-400 lg:text-48">
|
||||
{title}
|
||||
</h2>
|
||||
</h1>
|
||||
<p className="pb-8 text-center text-20 font-300 text-white-80">
|
||||
{description}
|
||||
</p>
|
||||
@@ -73,7 +72,7 @@ const BuyCards = () => {
|
||||
A card shaped <br /> hardware wallet
|
||||
</>
|
||||
}
|
||||
description="Store and trade your crypto in a simple, secure and slim hardware wallet."
|
||||
description="Something will say here about this product. Certainly, you don't want to miss it."
|
||||
buttons={
|
||||
<>
|
||||
<BuyKeycardDialog>
|
||||
@@ -84,9 +83,7 @@ const BuyCards = () => {
|
||||
</ButtonLink>
|
||||
</>
|
||||
}
|
||||
info={`From ${formatPrice({
|
||||
amount: KEYCARD_PRODUCTS.ONE_CARD_SET.price,
|
||||
})}`}
|
||||
info="Starts from $25"
|
||||
/>
|
||||
<Section
|
||||
image="/assets/bottom-keycard-shell.png"
|
||||
@@ -95,10 +92,10 @@ const BuyCards = () => {
|
||||
secondName="shell"
|
||||
title={
|
||||
<>
|
||||
One device for all <br /> your Keycards
|
||||
One device for all <br /> your keycards
|
||||
</>
|
||||
}
|
||||
description="Unrivalled security with an infinite number of removable Keycards, each with their own key."
|
||||
description="Something will say here about this product. Certainly you don't want to miss it"
|
||||
buttons={
|
||||
<>
|
||||
<GetNotifiedDialog>
|
||||
@@ -109,7 +106,7 @@ const BuyCards = () => {
|
||||
</ButtonLink>
|
||||
</>
|
||||
}
|
||||
info="Coming 2025 · Bundled with Keycard"
|
||||
info="Coming 2025"
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
@@ -1,31 +1,23 @@
|
||||
import {
|
||||
CheckIcon,
|
||||
FirmwareIcon,
|
||||
InfoIcon,
|
||||
RemoveIcon,
|
||||
} from '@status-im/icons/20'
|
||||
import { KEYCARD_PRODUCTS } from '~/app/_constants/shopify/products'
|
||||
import { formatPrice } from '~/app/_utils/format-price'
|
||||
import { Tag } from '~components/tag'
|
||||
import { Tooltip } from '~components/tooltip'
|
||||
import { Check, Info, Minus, Shield } from '~icons'
|
||||
import { createElement } from 'react'
|
||||
|
||||
const features: Array<Feature> = [
|
||||
{
|
||||
name: 'Competitive pricing',
|
||||
tooltip: 'Pricing information',
|
||||
keycardShell: {
|
||||
label: 'TBD',
|
||||
},
|
||||
keycard: {
|
||||
label: formatPrice({
|
||||
amount: KEYCARD_PRODUCTS.ONE_CARD_SET.price,
|
||||
}),
|
||||
label: '$25',
|
||||
},
|
||||
tangem: {
|
||||
label: '€50',
|
||||
label: '$55',
|
||||
},
|
||||
ledger: {
|
||||
label: '€80',
|
||||
label: '$80',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -50,6 +42,7 @@ const features: Array<Feature> = [
|
||||
},
|
||||
keycard: {
|
||||
featured: true,
|
||||
tooltip: 'Bitcoin only',
|
||||
},
|
||||
tangem: {
|
||||
featured: true,
|
||||
@@ -60,15 +53,16 @@ const features: Array<Feature> = [
|
||||
},
|
||||
{
|
||||
name: 'Hardware level protection',
|
||||
tooltip: 'Security certification level',
|
||||
keycardShell: {
|
||||
badge: {
|
||||
text: 'EAL 6+',
|
||||
gradient: true,
|
||||
icon: FirmwareIcon,
|
||||
icon: Shield,
|
||||
},
|
||||
},
|
||||
keycard: { badge: { text: 'EAL 6+', gradient: true, icon: FirmwareIcon } },
|
||||
tangem: { badge: { text: 'EAL 6+', gradient: true, icon: FirmwareIcon } },
|
||||
keycard: { badge: { text: 'EAL 6+', gradient: true, icon: Shield } },
|
||||
tangem: { badge: { text: 'EAL 6+', gradient: true, icon: Shield } },
|
||||
ledger: { badge: { text: 'EAL 5+' } },
|
||||
},
|
||||
{
|
||||
@@ -76,6 +70,7 @@ const features: Array<Feature> = [
|
||||
tooltip: 'Compatible with mobile devices',
|
||||
keycardShell: {
|
||||
featured: true,
|
||||
tooltip: 'iOS and Android',
|
||||
},
|
||||
keycard: {
|
||||
featured: true,
|
||||
@@ -94,13 +89,16 @@ const features: Array<Feature> = [
|
||||
},
|
||||
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',
|
||||
@@ -130,13 +128,14 @@ const features: Array<Feature> = [
|
||||
featured: false,
|
||||
},
|
||||
ledger: {
|
||||
featured: false,
|
||||
featured: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Open source secure element',
|
||||
keycardShell: {
|
||||
featured: true,
|
||||
tooltip: 'Secure element is open source',
|
||||
},
|
||||
keycard: {
|
||||
featured: false,
|
||||
@@ -158,9 +157,11 @@ const features: Array<Feature> = [
|
||||
},
|
||||
tangem: {
|
||||
featured: false,
|
||||
tooltip: 'Not applicable',
|
||||
},
|
||||
ledger: {
|
||||
featured: false,
|
||||
tooltip: 'Not applicable',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -170,7 +171,8 @@ const features: Array<Feature> = [
|
||||
featured: true,
|
||||
},
|
||||
keycard: {
|
||||
featured: false,
|
||||
featured: true,
|
||||
tooltip: 'Only with Keycard wallet',
|
||||
},
|
||||
tangem: {
|
||||
featured: false,
|
||||
@@ -214,6 +216,7 @@ const features: Array<Feature> = [
|
||||
name: 'Secure element has its own usages',
|
||||
keycardShell: {
|
||||
featured: true,
|
||||
tooltip: 'Multiple use cases for secure element',
|
||||
},
|
||||
keycard: {
|
||||
featured: 'n/a',
|
||||
@@ -249,9 +252,11 @@ const features: Array<Feature> = [
|
||||
},
|
||||
keycard: {
|
||||
featured: 'n/a',
|
||||
tooltip: 'Not applicable',
|
||||
},
|
||||
tangem: {
|
||||
featured: 'n/a',
|
||||
tooltip: 'Not applicable',
|
||||
},
|
||||
ledger: {
|
||||
featured: false,
|
||||
@@ -272,6 +277,21 @@ const features: Array<Feature> = [
|
||||
featured: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Oshwa label',
|
||||
keycardShell: {
|
||||
featured: true,
|
||||
},
|
||||
keycard: {
|
||||
featured: false,
|
||||
},
|
||||
tangem: {
|
||||
featured: false,
|
||||
},
|
||||
ledger: {
|
||||
featured: false,
|
||||
},
|
||||
},
|
||||
] as const
|
||||
|
||||
type FeatureVariant = {
|
||||
@@ -301,9 +321,9 @@ const FeatureInfo = ({
|
||||
}) => {
|
||||
if (!variant || typeof variant === 'string') return null
|
||||
|
||||
if (variant.featured === true) return <CheckIcon className="text-orange" />
|
||||
if (variant.featured === true) return <Check className="text-orange" />
|
||||
if (variant.featured === false)
|
||||
return <RemoveIcon className="size-4 text-white-60" />
|
||||
return <Minus className="size-4 text-white-60" />
|
||||
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) {
|
||||
@@ -319,6 +339,24 @@ const FeatureInfo = ({
|
||||
}
|
||||
}
|
||||
|
||||
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 className="ml-2 flex-shrink-0 text-white-40 transition-colors hover:text-white-60" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const ComparisonTable = () => {
|
||||
const products: Array<keyof Feature> = [
|
||||
'keycardShell',
|
||||
@@ -328,10 +366,10 @@ const ComparisonTable = () => {
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="mx-auto max-w-[1352px] pt-[120px] text-white-95 lg:pt-[200px]">
|
||||
<h2 className="px-3 font-lora text-32 text-white-95 min-[1512px]:px-0">
|
||||
<section className="mx-auto max-w-[1352px] overflow-x-hidden pt-[120px] text-white-95 lg:pt-[200px]">
|
||||
<h1 className="px-3 font-lora text-32 text-white-95 min-[1512px]:px-0">
|
||||
Going one step further.
|
||||
</h2>
|
||||
</h1>
|
||||
<div className="overflow-x-auto px-3 scrollbar-none min-[1512px]:px-0">
|
||||
<div className="w-[684px] pt-14 md:w-full lg:w-full lg:pt-20">
|
||||
<div className="grid grid-cols-[1.33fr,1fr,1fr,1fr,1fr] justify-items-center text-center font-lora text-24 font-400 text-white-90 lg:grid-cols-[2fr,1fr,1fr,1fr,1fr] lg:text-24">
|
||||
@@ -342,7 +380,7 @@ const ComparisonTable = () => {
|
||||
<div className="p-4 pb-5">Ledger</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-28 border border-white-8 bg-white-4 px-6">
|
||||
<div className="rounded-28 border border-white-12 bg-white-3 px-6">
|
||||
{features.map((feature, index) => {
|
||||
return (
|
||||
<div
|
||||
@@ -356,7 +394,7 @@ const ComparisonTable = () => {
|
||||
{feature.name.split(' ').slice(-1)[0]}
|
||||
{feature.tooltip && (
|
||||
<Tooltip label={feature.tooltip}>
|
||||
<InfoIcon className="ml-2 flex-shrink-0 text-white-40 transition-colors hover:text-white-60" />
|
||||
<Info className="ml-2 flex-shrink-0 text-white-40 transition-colors hover:text-white-60" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
@@ -368,6 +406,7 @@ const ComparisonTable = () => {
|
||||
className="relative flex items-center justify-center last:pr-0"
|
||||
>
|
||||
<FeatureInfo variant={feature[product]} />
|
||||
<TooltipInfo variant={feature[product]} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -1,10 +1,8 @@
|
||||
import * as Accordion from '@radix-ui/react-accordion'
|
||||
import { ChevronDownIcon } from '@status-im/icons/20'
|
||||
import { TwitterIcon } from '@status-im/icons/social'
|
||||
import { ButtonLink } from '~components/button-link'
|
||||
import { Image } from '~components/image'
|
||||
import { Link } from '~components/link'
|
||||
import { ChevronDown, Discord, X } from '~icons'
|
||||
import { cx } from 'cva'
|
||||
import Image from 'next/image'
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
@@ -14,19 +12,8 @@ const faqs = [
|
||||
},
|
||||
{
|
||||
question: 'Which assets does Keycard support?',
|
||||
answer: (
|
||||
<>
|
||||
Keycard works seamlessly with Ethereum and EVM based assets. It supports
|
||||
all assets on{' '}
|
||||
<Link
|
||||
href="https://tokenlists.org/token-list?url=https://ipfs.io/ipns/tokens.uniswap.org"
|
||||
className="underline decoration-1 underline-offset-2 transition-colors hover:text-orange"
|
||||
>
|
||||
Uniswap Labs default token list
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
),
|
||||
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?',
|
||||
@@ -39,18 +26,17 @@ const faqs = [
|
||||
'Keycard is compatible with various software wallets. Please check our documentation for a full list.',
|
||||
},
|
||||
{
|
||||
question: 'Why should I get a Keycard?',
|
||||
answer:
|
||||
'Keycard is a secure, pocket-sized hardware wallet with a provably genuine, immutable chip to protect your digital assets.',
|
||||
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] pt-[144px]">
|
||||
<section className="mx-auto max-w-[1352px]">
|
||||
<div className="flex flex-col items-start justify-between px-3 lg:flex-row lg:items-end min-[1512px]:px-0">
|
||||
<div className="flex w-full flex-1 flex-col lg:w-auto">
|
||||
<div className="flex justify-center pt-[120px] lg:hidden">
|
||||
<div className="flex w-full flex-col lg:pt-[200px]">
|
||||
<div className="block pt-[120px] lg:hidden">
|
||||
<Image
|
||||
src="/assets/faqs-mobile.png"
|
||||
alt="FAQ Background"
|
||||
@@ -59,9 +45,9 @@ const Faqs = () => {
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<h2 className="mt-14 font-lora text-32 text-white-95 lg:mt-0">
|
||||
<h1 className="font-lora text-32 text-white-95">
|
||||
Frequently asked questions
|
||||
</h2>
|
||||
</h1>
|
||||
<Accordion.Root
|
||||
className="flex-1 pt-8 lg:max-w-[549px] lg:pt-14"
|
||||
type="single"
|
||||
@@ -83,35 +69,34 @@ const Faqs = () => {
|
||||
)}
|
||||
>
|
||||
{faq.question}
|
||||
<div className="rounded-[10px] border border-white-12 bg-white-8 p-[6px] hover:border-white-20 hover:bg-white-12">
|
||||
<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 className="rounded-[10px] border border-white-12 bg-white-8 p-[6px]">
|
||||
<ChevronDown 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 pr-14 text-16 font-300 text-white-80 data-[state=closed]:animate-slideUp data-[state=open]:animate-slideDown',
|
||||
'overflow-hidden text-16 font-300 text-white-80 data-[state=closed]:animate-slideUp data-[state=open]:animate-slideDown',
|
||||
)}
|
||||
>
|
||||
<div className="pt-4">{faq.answer}</div>
|
||||
<span className="flex pt-4">{faq.answer}</span>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion.Root>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col lg:w-auto lg:items-end">
|
||||
<div className="relative hidden lg:block">
|
||||
<div className="absolute right-[-245px] top-[-20px] z-10 h-[400px] w-full bg-gradient-to-tr from-[transparent] via-dark-100 via-[83%] to-dark-100" />
|
||||
<div className="flex flex-col items-end">
|
||||
<div className="relative -right-1/4 -mt-48 hidden translate-x-[156px] translate-y-10 lg:block">
|
||||
<div className="absolute top-32 z-10 h-1/2 w-full bg-gradient-to-t from-[transparent] to-dark-100" />
|
||||
<Image
|
||||
src="/assets/faqs.png"
|
||||
alt="FAQ Background"
|
||||
height={790}
|
||||
width={1010}
|
||||
className="relative right-[-245px] top-[-40px]"
|
||||
width={736}
|
||||
height={736}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-14 flex flex-1 flex-col gap-6 rounded-28 border border-white-8 bg-white-4 p-6 pt-5 lg:mt-10 lg:max-w-[320px]">
|
||||
<div className="mt-14 flex w-full flex-col gap-6 rounded-28 border border-white-8 bg-white-3 p-6 pt-5 lg:mt-0 lg:max-w-[320px]">
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<p className="font-lora text-24 font-400 text-white-95">
|
||||
Still have questions?
|
||||
@@ -122,19 +107,24 @@ const Faqs = () => {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<ButtonLink
|
||||
href="mailto:support@keycard.tech"
|
||||
variant="secondary"
|
||||
>
|
||||
<ButtonLink href="/" variant="secondary">
|
||||
Get in touch
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
href="https://discord.com" // TODO: fix this link
|
||||
variant="secondary"
|
||||
className="px-[9px]"
|
||||
aria-label="Keycard on Discord"
|
||||
>
|
||||
<Discord />
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
href="https://x.com/keycard_"
|
||||
variant="secondary"
|
||||
className="px-[9px]"
|
||||
aria-label="Keycard on X"
|
||||
>
|
||||
<TwitterIcon />
|
||||
<X />
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,13 +1,11 @@
|
||||
import { KEYCARD_PRODUCTS } from '~/app/_constants/shopify/products'
|
||||
import { formatPrice } from '~/app/_utils/format-price'
|
||||
import { Button } from '~components/button'
|
||||
import { ButtonLink } from '~components/button-link'
|
||||
import { BuyKeycardDialog } from '~components/buy-keycard-dialog'
|
||||
import { Image } from '~components/image'
|
||||
import Image from 'next/image'
|
||||
|
||||
const Hero = () => {
|
||||
return (
|
||||
<section className="relative flex h-[calc(100svh-80px)] flex-col justify-end overflow-clip rounded-t-28 border border-white-8 bg-white-4 px-0 pt-0 full-view-port lg:h-auto lg:px-[72px] lg:py-20 lg:remove-full-view-port">
|
||||
<section className="relative flex h-[calc(100svh-80px)] flex-col justify-end overflow-clip rounded-t-28 border border-white-8 bg-white-3 px-0 pt-0 full-view-port lg:h-auto lg:px-[72px] lg:py-20 lg:remove-full-view-port">
|
||||
<Image
|
||||
alt="Keycard"
|
||||
src="/assets/hero-bg.png"
|
||||
@@ -25,16 +23,14 @@ const Hero = () => {
|
||||
priority
|
||||
/>
|
||||
|
||||
<div className="relative z-10 flex max-w-[434px] flex-col px-5 lg:max-w-[500px] lg:pt-0">
|
||||
<div className="relative z-10 flex max-w-[434px] flex-col px-5 lg:pt-0">
|
||||
<p className="pb-2 text-24 font-600 text-white-95">keycard</p>
|
||||
<h1 className="flex pb-8 font-lora text-32 font-400 lg:pb-4 lg:text-48">
|
||||
Lightweight design
|
||||
<br />
|
||||
heavyweight security
|
||||
A card shaped <br /> hardware wallet
|
||||
</h1>
|
||||
<p className="pb-8 text-20 font-300 text-white-80">
|
||||
Trade swiftly and safely back up your assets with Keycard hardware
|
||||
wallets.
|
||||
Store and trade your crypto with a simple, secure and slim hardware
|
||||
wallet.
|
||||
</p>
|
||||
<div className="flex space-x-4">
|
||||
<BuyKeycardDialog>
|
||||
@@ -45,10 +41,7 @@ const Hero = () => {
|
||||
</ButtonLink>
|
||||
</div>
|
||||
<p className="pb-8 pt-6 text-16 font-300 text-white-60 lg:pb-0">
|
||||
From{' '}
|
||||
{formatPrice({
|
||||
amount: KEYCARD_PRODUCTS.ONE_CARD_SET.price,
|
||||
})}
|
||||
From $25
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,33 +1,31 @@
|
||||
import { FirmwareIcon } from '@status-im/icons/20'
|
||||
import { GithubIcon } from '@status-im/icons/social'
|
||||
import { ButtonLink } from '~components/button-link'
|
||||
import { Image } from '~components/image'
|
||||
import { Tag } from '~components/tag'
|
||||
import { InfinityIcon } from '~icons/infinity'
|
||||
import { Github, Infinity, Shield } from '~icons'
|
||||
import { cx } from 'cva'
|
||||
import Image from 'next/image'
|
||||
import { createElement } from 'react'
|
||||
|
||||
const FEATURES = [
|
||||
const features = [
|
||||
{
|
||||
title: 'Best-in-class security',
|
||||
title: 'Best in class security',
|
||||
description:
|
||||
'Keycard secure element has the highest level of certification provided by Common Criteria.',
|
||||
'Our secure element has the highest level of security EAL6+ certified by Common Criteria.',
|
||||
image: '/assets/feature-keycard.png',
|
||||
imageClassName: '!justify-start',
|
||||
badge: { icon: FirmwareIcon, text: 'EAL 6+', gradient: true },
|
||||
badge: { icon: Shield, text: 'EAL 6+', gradient: true },
|
||||
className: 'row-span-2 col-span-1',
|
||||
},
|
||||
{
|
||||
title: 'Fully open source',
|
||||
title: '100% open source',
|
||||
description:
|
||||
'Open-source code on an open framework, making it the most open way to design a secure element.',
|
||||
button: { icon: GithubIcon, text: 'View on GitHub' },
|
||||
'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: 'Airgapped',
|
||||
title: 'Fully airgapped',
|
||||
description:
|
||||
'Through Keycard Shell’s camera or Keycard’s contactless nature, our products can be airgapped.',
|
||||
"Through Keycard Shell's camera or Keycard's contactless nature, our products are truly airgapped.",
|
||||
image: '/assets/feature-keycard-shell.png',
|
||||
badge: { text: '0 cables' },
|
||||
className: 'row-span-2 col-span-1 flex-col-reverse',
|
||||
@@ -42,8 +40,8 @@ const FEATURES = [
|
||||
{
|
||||
title: 'Easy to backup',
|
||||
description:
|
||||
'Securely store your private key on multiple cards. Encrypted, instead of the typical piece of paper.',
|
||||
badge: { icon: InfinityIcon, text: 'backups' },
|
||||
'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',
|
||||
},
|
||||
{
|
||||
@@ -67,18 +65,18 @@ const UnderlinedWord = ({ children }: { children: React.ReactNode }) => (
|
||||
const KeycardFeatures = () => {
|
||||
return (
|
||||
<section className="max-w-[1352px] px-3 pt-[120px] lg:mx-auto lg:pt-[200px] min-[1512px]:px-0">
|
||||
<h2 className="max-w-[665px] font-lora text-32 text-white-95">
|
||||
<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.
|
||||
</h2>
|
||||
</h1>
|
||||
<div className="grid grid-cols-1 gap-5 pt-20 lg:grid-cols-4 lg:gap-6">
|
||||
{FEATURES.map((feature, index) => (
|
||||
{features.map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cx([
|
||||
'relative flex flex-col justify-between overflow-clip rounded-28 bg-white-4 outline outline-1 outline-white-8',
|
||||
'relative flex flex-col justify-between overflow-clip rounded-28 bg-white-3 outline outline-1 outline-white-8',
|
||||
feature.className,
|
||||
])}
|
||||
>
|
||||
@@ -98,7 +96,7 @@ const KeycardFeatures = () => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-6 pt-5">
|
||||
<div className="p-6 pt-5 lg:pt-6">
|
||||
<div>
|
||||
<h3 className="mb-[6px] font-lora text-24 font-400">
|
||||
{feature.title}
|
||||
@@ -120,8 +118,8 @@ const KeycardFeatures = () => {
|
||||
)}
|
||||
{feature.button && (
|
||||
<ButtonLink
|
||||
href="https://github.com/keycard-tech/keycard-pro"
|
||||
className="font-500 [&_path]:hover:fill-white-dark"
|
||||
href="https://github.com/status-im/status-keycard"
|
||||
className="mt-4 font-500 [&_path]:hover:fill-white-dark"
|
||||
variant="white"
|
||||
icon={<feature.button.icon />}
|
||||
>
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Button } from '~components/button'
|
||||
import { ButtonLink } from '~components/button-link'
|
||||
import { GetNotifiedDialog } from '~components/get-notified-dialog'
|
||||
import { Image } from '~components/image'
|
||||
import Image from 'next/image'
|
||||
|
||||
const KeycardShell = () => {
|
||||
return (
|
||||
<section className="relative flex h-[calc(100svh-80px)] flex-col justify-end overflow-hidden rounded-b-28 border border-t-0 border-white-8 bg-white-4 backdrop-blur-[20px] full-view-port lg:h-auto lg:flex-row lg:items-center lg:justify-normal lg:remove-full-view-port">
|
||||
<section className="relative flex flex-col justify-end overflow-hidden rounded-b-28 border border-t-0 border-white-8 bg-white-3 backdrop-blur-[20px] full-view-port lg:flex-row lg:items-center lg:justify-normal lg:remove-full-view-port">
|
||||
<div className="hidden max-h-[510px] flex-1 items-start justify-center overflow-hidden lg:flex">
|
||||
<Image
|
||||
src="/assets/keycard-shell.png"
|
||||
alt="Keycard Shell Hardware Wallet"
|
||||
width="758"
|
||||
height="758"
|
||||
className="relative left-0 top-[-86px] object-cover object-center xl:top-[-128px] xl:object-contain"
|
||||
width="900"
|
||||
height="600"
|
||||
className="object-contain"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
@@ -26,18 +26,18 @@ const KeycardShell = () => {
|
||||
priority
|
||||
/>
|
||||
|
||||
<div className="relative z-10 -mt-16 flex max-w-[506px] flex-col px-5 pb-8 pt-0 lg:mt-0 lg:py-20 lg:pl-0 lg:pr-[72px]">
|
||||
<div className="relative z-10 -mt-16 flex max-w-[434px] flex-col px-5 pb-8 pt-0 lg:mt-0 lg:py-20 lg:pl-0">
|
||||
<p className="pb-2 text-24 font-600 text-white-95">
|
||||
keycard <span className="font-200">shell</span>
|
||||
</p>
|
||||
<p className="flex pb-8 font-lora text-32 font-400 lg:pb-4 lg:text-48">
|
||||
One secure device,
|
||||
<h1 className="flex pb-8 font-lora text-32 font-400 lg:pb-4 lg:text-48">
|
||||
One device for all
|
||||
<br />
|
||||
infinite backups
|
||||
</p>
|
||||
your Keycards
|
||||
</h1>
|
||||
<p className="pb-8 text-20 font-300 text-white-80">
|
||||
Unrivalled security with an infinite number of removable Keycards,
|
||||
each with their own key.
|
||||
A modular and stateless hardware wallet that
|
||||
<br /> relies on Keycard as the secure element.
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
<GetNotifiedDialog>
|
||||
@@ -47,10 +47,7 @@ const KeycardShell = () => {
|
||||
Learn more
|
||||
</ButtonLink>
|
||||
</div>
|
||||
<p className="flex items-center gap-2 pt-6 text-16 font-300 text-white-60">
|
||||
Coming 2025 <span className="size-1 rounded-full bg-white-40" />{' '}
|
||||
Bundled with Keycard
|
||||
</p>
|
||||
<p className="pt-6 text-16 font-300 text-white-60">Coming 2025</p>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
'use client'
|
||||
|
||||
import { GetNotifiedDialog } from '~components/get-notified-dialog'
|
||||
import { useState } from 'react'
|
||||
import * as Dialog from '../../_components/dialog'
|
||||
|
||||
export const ThankYouDialog = () => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
// TODO - Check on load for some parameter in the URL from shopify in order to open the dialog
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Content>
|
||||
<Dialog.Success
|
||||
title="Thanks for choosing Keycard!"
|
||||
description="You have successfully purchased a Keycard set. It will arrive shortly!"
|
||||
image="/assets/sign-up-success.png"
|
||||
onClose={() => setOpen(false)}
|
||||
footer={{
|
||||
title: 'Want to up your game?',
|
||||
description: 'Keycard Shell is coming 2025',
|
||||
Dialog: GetNotifiedDialog,
|
||||
buttonText: 'Get notified',
|
||||
}}
|
||||
/>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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 { KeycardShell } from '~/app/(homepage)/_components/keycard-shell'
|
||||
import { ThankYouDialog } from './_components/thank-you-dialog'
|
||||
|
||||
export default async function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<ThankYouDialog />
|
||||
<Hero />
|
||||
<KeycardShell />
|
||||
<KeycardFeatures />
|
||||
<ComparisonTable />
|
||||
<Faqs />
|
||||
<BuyCards />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { BuyKeycardDialog } from '~components/buy-keycard-dialog'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export const AutoOpenBuyKeycardDialog = () => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const buyKeycard = new URLSearchParams(window.location.search).get(
|
||||
'buyKeycard',
|
||||
)
|
||||
|
||||
if (!buyKeycard || buyKeycard !== 'true') {
|
||||
return
|
||||
}
|
||||
|
||||
setOpen(true)
|
||||
}, [])
|
||||
|
||||
return <BuyKeycardDialog open={open} onOpenChange={setOpen} />
|
||||
}
|
||||
|
Before Width: | Height: | Size: 372 KiB |
@@ -1,28 +0,0 @@
|
||||
import { Metadata } from '~/app/_metadata'
|
||||
import { AutoOpenBuyKeycardDialog } from './_components/auto-open-buy-keycard-dialog'
|
||||
import { BuyCards } from './_components/buy-cards'
|
||||
import { ComparisonTable } from './_components/comparison-table'
|
||||
import { Faqs } from './_components/faqs'
|
||||
import { Hero } from './_components/hero'
|
||||
import { KeycardFeatures } from './_components/keycard-features'
|
||||
import { KeycardShell } from './_components/keycard-shell'
|
||||
|
||||
export const metadata = Metadata({
|
||||
title: {
|
||||
absolute: 'Keycard: Secure Crypto & Hardware Wallet',
|
||||
},
|
||||
})
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<AutoOpenBuyKeycardDialog />
|
||||
<Hero />
|
||||
<KeycardShell />
|
||||
<KeycardFeatures />
|
||||
<ComparisonTable />
|
||||
<Faqs />
|
||||
<BuyCards />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Button } from '~components/button'
|
||||
import { GetNotifiedDialog } from '~components/get-notified-dialog'
|
||||
import { Image } from '~components/image'
|
||||
|
||||
const Hero = () => {
|
||||
return (
|
||||
<section className="relative flex h-svh overflow-y-clip full-view-port lg:remove-full-view-port 2xl:h-[839px]">
|
||||
<Image
|
||||
alt="Keycard Shell"
|
||||
src="/assets/keycard-shell/bg-hero-keycard-shell.png"
|
||||
width={986}
|
||||
height={986}
|
||||
priority
|
||||
className="absolute left-10 top-[43%] z-0 h-auto w-fit -translate-y-1/2 scale-[120%] bg-contain bg-top bg-no-repeat md:scale-100 lg:left-[56%] lg:-translate-x-1/2"
|
||||
/>
|
||||
<div className="pointer-events-none relative z-20 flex flex-1 flex-col justify-end px-5 pb-5 align-bottom lg:pb-20 xl:px-20">
|
||||
<div className="flex flex-col items-start justify-between lg:flex-row">
|
||||
<div className="pointer-events-auto flex max-w-[664px] flex-col lg:self-end">
|
||||
<p className="pb-2 text-24 font-600 text-white-95">
|
||||
keycard <span className="font-200">shell</span>
|
||||
</p>
|
||||
<h1 className="flex font-lora text-32 font-400 text-white-95 lg:text-44">
|
||||
One secure device,
|
||||
<br />
|
||||
infinite backups
|
||||
</h1>
|
||||
<p className="max-w-[435px] pb-8 pt-4 text-20 font-300 text-white-80">
|
||||
Unrivalled security with an infinite number of removable Keycards,
|
||||
each with their own key.
|
||||
</p>
|
||||
<div className="flex flex-col items-start gap-5 md:flex-row md:items-center">
|
||||
<GetNotifiedDialog>
|
||||
<Button variant="primary">Get notified</Button>
|
||||
</GetNotifiedDialog>
|
||||
<p className="flex items-center gap-2 text-16 font-300 text-white-60">
|
||||
Coming 2025 <span className="size-1 rounded-full bg-white-40" />{' '}
|
||||
Bundled with Keycard
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-x-0 bottom-0 z-10 h-1/2 bg-gradient-to-t from-[#010101] to-[transparent] lg:h-1/3" />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export { Hero }
|
||||
@@ -1,49 +0,0 @@
|
||||
import { Button } from '~components/button'
|
||||
import { GetNotifiedDialog } from '~components/get-notified-dialog'
|
||||
import { Image } from '~components/image'
|
||||
import { cx } from 'cva'
|
||||
|
||||
const Prefooter = () => {
|
||||
return (
|
||||
<section className="relative mb-2 mt-28 flex flex-col px-5 pb-20 pt-0 full-view-port lg:my-0 lg:mb-[160px] lg:flex-row lg:pb-0 lg:pt-[100px] lg:remove-full-view-port xl:gap-8 2xl:gap-16">
|
||||
<div className="relative z-20 order-2 flex flex-col items-start pt-[365px] text-left lg:order-1 lg:pt-[100px] xl:pl-[290px]">
|
||||
<p className="pb-2 text-24 font-600 text-white-95">
|
||||
keycard <span className="font-200">shell</span>
|
||||
</p>
|
||||
<h2 className="flex pb-4 font-lora text-32 font-400 text-white-95 md:text-48">
|
||||
One device for all
|
||||
<br /> your Keycards
|
||||
</h2>
|
||||
<p className="max-w-[435px] pb-10 text-20 font-300 text-white-80">
|
||||
Unrivalled security with an infinite number of removable Keycards,
|
||||
each with their own key.
|
||||
</p>
|
||||
<div className="flex flex-col items-start gap-5 sm:min-w-[410px] sm:flex-row sm:items-center">
|
||||
<GetNotifiedDialog>
|
||||
<Button variant="primary">Get notified</Button>
|
||||
</GetNotifiedDialog>
|
||||
<p className="flex items-center gap-2 text-16 font-300 text-white-60 md:gap-2">
|
||||
Coming 2025 <span className="size-1 rounded-full bg-white-40" />{' '}
|
||||
Bundled with Keycard
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="order-1 flex lg:right-[-20px] lg:order-2">
|
||||
<div className="absolute z-20 block h-[360px] w-full bg-gradient-to-b from-dark-100 to-[transparent] lg:h-[220px]" />
|
||||
<Image
|
||||
src="/assets/faqs.png"
|
||||
alt="Keycard Shell"
|
||||
width={791}
|
||||
height={521}
|
||||
draggable={false}
|
||||
className={cx([
|
||||
'pointer-events-none absolute left-[80%] top-0 z-0 h-auto max-w-[549px] -translate-x-1/2 select-none md:left-1/2 lg:relative lg:left-1/4 lg:top-12 lg:translate-x-0',
|
||||
'aspect-[791/521] object-contain lg:scale-125',
|
||||
])}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export { Prefooter }
|
||||
|
Before Width: | Height: | Size: 322 KiB |
@@ -1,245 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as Accordion from '@radix-ui/react-accordion'
|
||||
import { useIntersectionObserver } from '~/app/_hooks/use-intersection-observer'
|
||||
import { DownloadStatusForDesktop } from '~components/download-status-for-desktop'
|
||||
import { DownloadStatusForMobile } from '~components/download-status-for-mobile'
|
||||
import { FeaturesDisclaimer } from '~components/features-disclaimer'
|
||||
import { Image } from '~components/image'
|
||||
import { cx } from 'cva'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { match } from 'ts-pattern'
|
||||
|
||||
type Props = {
|
||||
items: Array<{
|
||||
title: string
|
||||
description: React.ReactNode | string
|
||||
image: string
|
||||
tag?: string
|
||||
}>
|
||||
imageClassName?: string
|
||||
variant: 'desktop' | 'mobile'
|
||||
}
|
||||
|
||||
const LineDivider = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="944" height="2">
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeDasharray="3 4"
|
||||
strokeLinecap="round"
|
||||
d="M1 1h944"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const useAutoSwitch = ({
|
||||
items,
|
||||
interval,
|
||||
step,
|
||||
isVisible,
|
||||
variant,
|
||||
}: {
|
||||
items: Array<{ title: string }>
|
||||
interval: number
|
||||
step: number
|
||||
isVisible: boolean
|
||||
variant: 'desktop' | 'mobile'
|
||||
}) => {
|
||||
const [value, setValue] = useState(items[0].title)
|
||||
const [width, setWidth] = useState(0)
|
||||
const counterRef = useRef(0)
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout | null = null
|
||||
|
||||
if (isVisible) {
|
||||
timer = setInterval(() => {
|
||||
counterRef.current += step
|
||||
|
||||
if (counterRef.current >= interval) {
|
||||
counterRef.current = 0
|
||||
setWidth(0)
|
||||
const index = items.findIndex(item => item.title === value)
|
||||
setValue(items[(index + 1) % items.length].title)
|
||||
} else {
|
||||
setWidth((counterRef.current * 100) / interval)
|
||||
}
|
||||
}, step)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timer) clearInterval(timer)
|
||||
}
|
||||
}, [items, value, interval, step, isVisible])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible) {
|
||||
setWidth((counterRef.current * 100) / interval)
|
||||
}
|
||||
}, [isVisible, interval])
|
||||
|
||||
// Reset the value if the variant changes
|
||||
useEffect(() => {
|
||||
setValue(items[0].title)
|
||||
setWidth(0)
|
||||
counterRef.current = 0
|
||||
}, [variant, items])
|
||||
|
||||
// Reset the counter if the value changes
|
||||
useEffect(() => {
|
||||
counterRef.current = 0
|
||||
}, [value])
|
||||
|
||||
return { value, setValue, width }
|
||||
}
|
||||
|
||||
const FeaturesAccordion = (props: Props) => {
|
||||
const { items, imageClassName, variant } = props
|
||||
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
||||
const entry = useIntersectionObserver(wrapperRef, {
|
||||
rootMargin: '0%',
|
||||
})
|
||||
|
||||
const isVisible = !!entry?.isIntersecting
|
||||
|
||||
const { value, setValue, width } = useAutoSwitch({
|
||||
items,
|
||||
interval: 16000,
|
||||
step: 50,
|
||||
isVisible,
|
||||
variant,
|
||||
})
|
||||
|
||||
const selected = items.find(item => item.title === value)!
|
||||
|
||||
return (
|
||||
<div
|
||||
className="hidden items-center justify-between lg:flex"
|
||||
ref={wrapperRef}
|
||||
>
|
||||
<div
|
||||
className={cx([
|
||||
'flex flex-1 items-start justify-center gap-6 xl:gap-0',
|
||||
])}
|
||||
>
|
||||
<div className="flex flex-1 flex-col items-start">
|
||||
<Accordion.Root
|
||||
type="single"
|
||||
value={value}
|
||||
collapsible
|
||||
onValueChange={setValue}
|
||||
className="flex w-full max-w-[549px] flex-col gap-0 pt-24 lg:flex-1 lg:pt-0"
|
||||
>
|
||||
{items.map(item => {
|
||||
const isOpen = value === item.title
|
||||
|
||||
return (
|
||||
<Accordion.Item key={item.title} value={item.title}>
|
||||
<Accordion.Header>
|
||||
<Accordion.Trigger disabled={isOpen} className="w-full">
|
||||
<div className="group relative flex items-center gap-2 py-5">
|
||||
<p
|
||||
className={cx(
|
||||
'flex items-center text-left font-lora text-32 font-500 transition-opacity',
|
||||
!isOpen && 'group-hover:opacity-[50%]',
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
</p>
|
||||
{item.tag && (
|
||||
<span className="mt-2 rounded-[32px] border border-dark-8 bg-white-95 px-2 py-0.5 text-12 font-500 text-dark-100">
|
||||
{item.tag}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Header>
|
||||
<Accordion.Content
|
||||
className={cx(
|
||||
'-translate-y-5 overflow-hidden text-16 font-300 text-white-80 data-[state=closed]:animate-slideUp data-[state=open]:animate-slideDown',
|
||||
)}
|
||||
>
|
||||
<div className="pt-1">
|
||||
<p className="text-16 font-300 text-white-60">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</Accordion.Content>
|
||||
|
||||
<div className="relative w-full">
|
||||
<div
|
||||
className="absolute left-0 top-[0.5px] z-[2] h-px rounded-[2px] bg-orange text-white-100 transition-opacity duration-500"
|
||||
style={{
|
||||
opacity: isOpen ? 1 : 0,
|
||||
width: `${width}%`,
|
||||
}}
|
||||
/>
|
||||
<div className="absolute left-0 top-0 z-[1] w-full overflow-hidden text-white-20">
|
||||
<LineDivider />
|
||||
</div>
|
||||
</div>
|
||||
</Accordion.Item>
|
||||
)
|
||||
})}
|
||||
</Accordion.Root>
|
||||
<div className="w-full pt-8 lg:max-w-[549px] lg:pt-20">
|
||||
<FeaturesDisclaimer />
|
||||
</div>
|
||||
</div>
|
||||
{match(variant)
|
||||
.with('desktop', () => (
|
||||
<div className="relative flex flex-1 flex-col items-end self-end">
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 40 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 40 }}
|
||||
>
|
||||
<Image
|
||||
src={selected.image}
|
||||
width={1124}
|
||||
height={716}
|
||||
alt={selected.title}
|
||||
className={imageClassName}
|
||||
quality={75}
|
||||
priority
|
||||
/>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
<DownloadStatusForDesktop className="mt-20 lg:max-w-[549px]" />
|
||||
</div>
|
||||
))
|
||||
.with('mobile', () => (
|
||||
<div className="relative flex flex-1 flex-col items-end self-end">
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 40 }}
|
||||
>
|
||||
<Image
|
||||
src={selected.image}
|
||||
width={1124}
|
||||
height={716}
|
||||
alt={selected.title}
|
||||
className={imageClassName}
|
||||
quality={75}
|
||||
priority
|
||||
/>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
<DownloadStatusForMobile className="mt-20 lg:max-w-[549px]" />
|
||||
</div>
|
||||
))
|
||||
.exhaustive()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { FeaturesAccordion }
|
||||
export type { Props as FeaturesAccordionProps }
|
||||
@@ -1,60 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { DownloadStatusForDesktop } from '~components/download-status-for-desktop'
|
||||
import { FeaturesDisclaimer } from '~components/features-disclaimer'
|
||||
import { Image } from '~components/image'
|
||||
|
||||
type Props = {
|
||||
items: Array<{
|
||||
title: string
|
||||
description: React.ReactNode | string
|
||||
imageMobile: string
|
||||
tag?: string
|
||||
}>
|
||||
}
|
||||
|
||||
const FeaturesSliderDesktop = (props: Props) => {
|
||||
const { items } = props
|
||||
|
||||
return (
|
||||
<div className="relative block lg:hidden">
|
||||
<div className="flex snap-x snap-mandatory overflow-x-auto scrollbar-none">
|
||||
{items.map((feature, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="z-20 min-w-[calc(100vw-40px)] snap-start px-2"
|
||||
>
|
||||
<div className="mx-auto flex h-[80svh] flex-col overflow-clip rounded-[32px] border border-white-12 bg-white-4 px-6 py-5">
|
||||
<div>
|
||||
<p className="pb-[6px] text-left font-lora text-24 font-500 text-white-95">
|
||||
{feature.title}
|
||||
</p>
|
||||
<p className="text-16 font-300 text-white-60">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Image
|
||||
src={feature.imageMobile}
|
||||
alt={feature.title}
|
||||
width={2358}
|
||||
height={1871}
|
||||
priority
|
||||
className="max-w-[calc(70vh*1.26)] translate-x-[-20%]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="w-full px-2 pt-8 lg:max-w-[549px] lg:pt-20">
|
||||
<FeaturesDisclaimer />
|
||||
<DownloadStatusForDesktop className="mt-8" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { FeaturesSliderDesktop }
|
||||
@@ -1,40 +0,0 @@
|
||||
import { KEYCARD_PRODUCTS } from '~/app/_constants/shopify/products'
|
||||
import { formatPrice } from '~/app/_utils/format-price'
|
||||
import { Background } from '~components/3d/background'
|
||||
import { Button } from '~components/button'
|
||||
import { BuyKeycardDialog } from '~components/buy-keycard-dialog'
|
||||
|
||||
const Hero = () => {
|
||||
return (
|
||||
<section className="relative flex h-svh overflow-y-clip full-view-port lg:remove-full-view-port 2xl:h-[839px]">
|
||||
<Background variant="homepage" />
|
||||
<div className="pointer-events-none relative z-20 flex flex-1 flex-col justify-end px-5 pb-5 align-bottom lg:pb-[43px] xl:px-20">
|
||||
<div className="flex flex-col items-start justify-between lg:flex-row">
|
||||
<div className="pointer-events-auto flex max-w-[664px] flex-col lg:self-end">
|
||||
<p className="pb-2 text-24 font-600 text-white-95">keycard</p>
|
||||
<h1 className="flex font-lora text-32 font-400 text-white-95 lg:text-44">
|
||||
Lightweight design, <br /> heavyweight security
|
||||
</h1>
|
||||
<p className="max-w-[435px] pb-8 pt-4 text-20 font-300 text-white-80">
|
||||
Trade swiftly and safely back up your assets with Keycard hardware
|
||||
wallets.
|
||||
</p>
|
||||
<div className="flex items-center gap-5">
|
||||
<BuyKeycardDialog>
|
||||
<Button variant="primary">Buy Keycard</Button>
|
||||
</BuyKeycardDialog>
|
||||
<p className="text-16 font-300 text-white-60">
|
||||
From{' '}
|
||||
{formatPrice({
|
||||
amount: KEYCARD_PRODUCTS.ONE_CARD_SET.price,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export { Hero }
|
||||
@@ -1,236 +0,0 @@
|
||||
import { ExternalIcon } from '@status-im/icons/20'
|
||||
import { ButtonLink } from '~components/button-link'
|
||||
import { FeaturesDialog } from '~components/features-dialog'
|
||||
import { Image } from '~components/image'
|
||||
import { Link } from '~components/link'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '~components/tabs'
|
||||
import { FeaturesAccordion } from './features-accordion'
|
||||
import { FeaturesSliderDesktop } from './features-slider-desktop'
|
||||
import { FeaturesSliderMobile } from './features-slider-mobile'
|
||||
|
||||
const featuresMobile = [
|
||||
{
|
||||
title: 'Send assets and collectibles',
|
||||
description: (
|
||||
<>
|
||||
Status supports all Ethereum assets on{' '}
|
||||
<Link
|
||||
href="https://tokenlists.org/token-list?url=https://ipfs.io/ipns/tokens.uniswap.org"
|
||||
className="underline decoration-1 underline-offset-2 transition-colors hover:text-orange"
|
||||
>
|
||||
Uniswap Labs default token list.
|
||||
</Link>
|
||||
</>
|
||||
),
|
||||
image: '/assets/keycard/slider/status-app-mobile-send.png',
|
||||
imageMobile: '/assets/keycard/slider/iphone-send.png',
|
||||
},
|
||||
{
|
||||
title: 'Swap',
|
||||
description: 'Swap your favourite assets on Mainnet, Arbitrum or Optimism.',
|
||||
image: '/assets/keycard/slider/status-app-mobile-swap.png',
|
||||
imageMobile: '/assets/keycard/slider/iphone-swap.png',
|
||||
},
|
||||
{
|
||||
title: 'Bridge',
|
||||
description: 'Bridge assets across Ethereum’s most popular chains.',
|
||||
image: '/assets/keycard/slider/status-app-mobile-bridge.png',
|
||||
imageMobile: '/assets/keycard/slider/iphone-bridge.png',
|
||||
},
|
||||
{
|
||||
title: 'Dapp interactions',
|
||||
description: 'Connect to your favourite dapps using Wallet Connect.',
|
||||
tag: 'SOON',
|
||||
image: '/assets/keycard/slider/status-app-mobile-dapp.png',
|
||||
imageMobile: '/assets/keycard/slider/iphone-dapp.png',
|
||||
},
|
||||
]
|
||||
|
||||
const featuresDesktop = [
|
||||
{
|
||||
title: 'Send assets and collectibles',
|
||||
description: (
|
||||
<>
|
||||
Status supports all Ethereum assets on{' '}
|
||||
<Link
|
||||
href="https://tokenlists.org/token-list?url=https://ipfs.io/ipns/tokens.uniswap.org"
|
||||
className="underline decoration-1 underline-offset-2 transition-colors hover:text-orange"
|
||||
>
|
||||
Uniswap Labs default token list.
|
||||
</Link>
|
||||
</>
|
||||
),
|
||||
image: '/assets/keycard/slider/status-app-desktop-send.png',
|
||||
imageMobile: '/assets/keycard/slider/status-app-desktop-send.png',
|
||||
},
|
||||
{
|
||||
title: 'Swap',
|
||||
description: 'Swap your favourite assets on Mainnet, Arbitrum or Optimism.',
|
||||
image: '/assets/keycard/slider/status-app-desktop-swap.png',
|
||||
imageMobile: '/assets/keycard/slider/status-app-desktop-swap.png',
|
||||
},
|
||||
{
|
||||
title: 'Bridge',
|
||||
description: 'Bridge assets across Ethereum’s most popular chains.',
|
||||
image: '/assets/keycard/slider/status-app-desktop-bridge.png',
|
||||
imageMobile: '/assets/keycard/slider/status-app-desktop-bridge.png',
|
||||
},
|
||||
{
|
||||
title: 'Dapp interactions',
|
||||
description: (
|
||||
<>
|
||||
Connect to your favourite dApps with the{' '}
|
||||
<Link
|
||||
href="https://github.com/status-im/status-web/tree/main/apps/connector"
|
||||
className="underline transition-colors hover:text-orange"
|
||||
>
|
||||
Status Connector
|
||||
</Link>{' '}
|
||||
web extension.
|
||||
</>
|
||||
),
|
||||
tag: 'SOON',
|
||||
image: '/assets/keycard/slider/status-app-desktop-dapp.png',
|
||||
imageMobile: '/assets/keycard/slider/status-app-desktop-dapp.png',
|
||||
},
|
||||
]
|
||||
|
||||
const integrations = [
|
||||
{
|
||||
name: 'Enno Wallet',
|
||||
description: 'Native Android Ethereum Wallet',
|
||||
logo: '/assets/keycard/enno.png',
|
||||
buttonText: 'Learn more',
|
||||
href: 'https://ennowallet.com/',
|
||||
},
|
||||
{
|
||||
name: 'WallETH',
|
||||
description: 'Native Android Ethereum Wallet',
|
||||
logo: '/assets/keycard/walleth.png',
|
||||
buttonText: 'Learn more',
|
||||
href: 'https://walleth.org/',
|
||||
},
|
||||
{
|
||||
name: 'Logos Operators',
|
||||
description: '5,000 Ordinals Collection',
|
||||
logo: '/assets/keycard/logos-operators.png',
|
||||
buttonText: 'Learn more',
|
||||
href: 'https://dashboard.logos.co/',
|
||||
},
|
||||
]
|
||||
|
||||
const Integration = () => {
|
||||
return (
|
||||
<section className="mt-[200px]">
|
||||
<div className="flex justify-center border-y border-y-white-8 bg-white-4 remove-full-view-port lg:full-view-port">
|
||||
<div className="w-full max-w-[1512px] p-20 px-0 pb-5 lg:px-20 lg:pb-20">
|
||||
<div className="flex max-w-[550px] flex-col gap-2 px-3 pb-8 lg:px-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Image
|
||||
width={109}
|
||||
height={32}
|
||||
alt="Status"
|
||||
src="/assets/keycard/status-logo.png"
|
||||
/>
|
||||
<FeaturesDialog>
|
||||
<button className="flex items-center gap-1 rounded-16 border border-dashed border-[#FF640020] bg-[#FF640010] py-[5px] pl-4 pr-3 text-14 font-300 text-orange transition-colors group-hover:bg-[#FF640020]">
|
||||
Keycard features in beta - learn more
|
||||
<ExternalIcon />
|
||||
</button>
|
||||
</FeaturesDialog>
|
||||
</div>
|
||||
<h2 className="pt-1 font-lora text-32 font-400 text-white-95">
|
||||
Seamless integration with Status
|
||||
</h2>
|
||||
<p className="text-20 font-300 text-white-60">
|
||||
Keycard works straight out of the box with the Status app.
|
||||
<br />
|
||||
No need to pair it, it just works!
|
||||
</p>
|
||||
</div>
|
||||
<Tabs defaultValue="mobile">
|
||||
<TabsList className="mx-3 lg:mx-0">
|
||||
<TabsTrigger value="mobile">On mobile</TabsTrigger>
|
||||
<TabsTrigger value="desktop">On desktop</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="desktop" className="pt-20">
|
||||
<FeaturesAccordion
|
||||
variant="desktop"
|
||||
items={featuresDesktop}
|
||||
imageClassName="absolute top-0 min-w-[808px] translate-x-[45%] xl:translate-x-[43%] 2xl:translate-x-[33%] translate-y-[-87%] xl:min-w-[1008px] right-0 2xl:min-w-[1178px]"
|
||||
/>
|
||||
<FeaturesSliderDesktop items={featuresDesktop} />
|
||||
</TabsContent>
|
||||
<TabsContent value="mobile" className="pt-20">
|
||||
<FeaturesAccordion
|
||||
variant="mobile"
|
||||
items={featuresMobile}
|
||||
imageClassName="absolute top-0 translate-x-[30%] xl:translate-x-0 max-w-[604px] translate-y-[-100%] right-0"
|
||||
/>
|
||||
<FeaturesSliderMobile items={featuresMobile} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-3 pt-20 md:px-12">
|
||||
<h2 className="pb-4 pl-6 text-12 font-400 text-white-80 lg:pl-8">
|
||||
ALSO INTEGRATED WITH
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 gap-8 rounded-28 border border-white-8 bg-white-4 p-6 pt-5 text-white-95 md:grid-cols-2 lg:grid-cols-4 lg:gap-6 lg:p-8 lg:pt-9">
|
||||
{integrations.map((integration, index) => (
|
||||
<div key={index} className="flex flex-col">
|
||||
<div className="mb-3 inline-flex items-center">
|
||||
{integration.logo && (
|
||||
<Image
|
||||
src={integration.logo}
|
||||
alt={`${integration.name} logo`}
|
||||
width={24}
|
||||
height={23}
|
||||
className="mr-2"
|
||||
/>
|
||||
)}
|
||||
<h3 className="font-lora text-24 font-400">
|
||||
{integration.name}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="mb-6 text-16 font-300 text-white-80">
|
||||
{integration.description}
|
||||
</p>
|
||||
|
||||
<ButtonLink
|
||||
href={integration.href}
|
||||
variant="secondary"
|
||||
className="group w-fit pr-2"
|
||||
>
|
||||
<span className="text-white-95">{integration.buttonText}</span>
|
||||
<ExternalIcon />
|
||||
</ButtonLink>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex flex-col gap-6">
|
||||
<div>
|
||||
<h2 className="font-lora text-24 font-500 text-white-95 lg:text-32">
|
||||
Want to integrate?
|
||||
</h2>
|
||||
<p className="text-16 text-white-80">
|
||||
Read our documentation or get in touch
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<ButtonLink href="/docs">Read docs</ButtonLink>
|
||||
<ButtonLink
|
||||
href="mailto:support@keycard.tech"
|
||||
variant="secondary"
|
||||
>
|
||||
Get in touch
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export { Integration }
|
||||
@@ -1,60 +0,0 @@
|
||||
import { KEYCARD_PRODUCTS } from '~/app/_constants/shopify/products'
|
||||
import { formatPrice } from '~/app/_utils/format-price'
|
||||
import { Button } from '~components/button'
|
||||
import { BuyKeycardDialog } from '~components/buy-keycard-dialog'
|
||||
import { Image } from '~components/image'
|
||||
import { cx } from 'cva'
|
||||
|
||||
const Prefooter = () => {
|
||||
return (
|
||||
<section className="relative flex flex-col overflow-hidden px-6 lg:flex-row lg:px-14 lg:pt-[200px] xl:pl-[309px]">
|
||||
<div className="relative z-20 flex flex-1 flex-col items-start pb-[120px] pt-[320px] text-left lg:min-w-[450px] lg:py-[200px]">
|
||||
<p className="pb-2 text-24 font-600 text-white-95">keycard</p>
|
||||
<h2 className="flex pb-4 font-lora text-32 font-400 text-white-95 md:text-48">
|
||||
Get the card-shaped <br /> hardware wallet
|
||||
</h2>
|
||||
<p className="max-w-[435px] pb-10 text-20 font-300 text-white-80">
|
||||
Store and trade your crypto in a simple, secure and slim hardware
|
||||
wallet.
|
||||
</p>
|
||||
<div className="flex flex-row items-center gap-5">
|
||||
<BuyKeycardDialog>
|
||||
<Button variant="primary">Buy Keycard</Button>
|
||||
</BuyKeycardDialog>
|
||||
<p className="text-16 font-300 text-white-60">
|
||||
From{' '}
|
||||
{formatPrice({
|
||||
amount: KEYCARD_PRODUCTS.ONE_CARD_SET.price,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="order-1 flex lg:relative lg:right-[-20px] lg:order-2">
|
||||
<div className="absolute bottom-0 z-10 block h-1/2 w-full -translate-y-1 bg-gradient-to-t from-dark-100 to-[transparent] lg:hidden" />
|
||||
<Image
|
||||
src="/assets/keycard/pre-footer.png"
|
||||
alt="Keycard"
|
||||
width={744}
|
||||
height={712}
|
||||
draggable={false}
|
||||
className={cx([
|
||||
'pointer-events-none absolute left-1/2 top-12 z-0 block h-auto max-w-[549px] -translate-x-1/2 select-none lg:relative lg:left-auto lg:top-auto lg:hidden lg:translate-x-0',
|
||||
'aspect-[744/712] object-contain',
|
||||
])}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-0 z-10 hidden h-40 w-full -translate-y-1 bg-gradient-to-t from-dark-100 to-[transparent] lg:block" />
|
||||
<div className="absolute bottom-0 right-0 z-10 hidden h-full w-28 bg-gradient-to-r from-[transparent] to-dark-100 lg:block" />
|
||||
<Image
|
||||
src="/assets/keycard/pre-footer-raw.png"
|
||||
alt="Keycard"
|
||||
width={1387}
|
||||
height={912}
|
||||
draggable={false}
|
||||
className="pointer-events-none absolute bottom-0 right-0 z-0 hidden max-h-[912px] w-full translate-x-44 select-none object-contain lg:block"
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export { Prefooter }
|
||||
|
Before Width: | Height: | Size: 173 KiB |
@@ -1,18 +0,0 @@
|
||||
import { Footer } from '~components/footer'
|
||||
import { NavBar } from '~components/nav-bar'
|
||||
import { NavBarMobile } from '~components/nav-bar-mobile'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export default function WithNavigationLayout({ children }: Props) {
|
||||
return (
|
||||
<>
|
||||
<NavBar />
|
||||
<NavBarMobile />
|
||||
{children}
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export default function StartLayout({ children }: Props) {
|
||||
return (
|
||||
<div className="mx-auto mt-[88px] w-full max-w-[1320px]">{children}</div>
|
||||
)
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import { ExternalIcon } from '@status-im/icons/20'
|
||||
import { ButtonLink } from '~components/button-link'
|
||||
import { DownloadStatusForDesktop } from '~components/download-status-for-desktop'
|
||||
import { DownloadStatusForMobile } from '~components/download-status-for-mobile'
|
||||
import { FeaturesDisclaimer } from '~components/features-disclaimer'
|
||||
import Image from 'next/image'
|
||||
|
||||
export default function StartPage() {
|
||||
return (
|
||||
<div className="px-3 pb-[120px] pt-12 md:px-8 lg:px-20 lg:pt-20">
|
||||
<div className="mb-6 grid grid-flow-row gap-3">
|
||||
<h1 className="font-lora text-32 font-400 text-white-95 lg:text-48">
|
||||
Get started with Keycard
|
||||
</h1>
|
||||
<p className="font-inter text-20 font-300 text-white-95">
|
||||
Follow our step-by-step guides to set up your Keycard
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-14 grid grid-flow-row gap-3">
|
||||
<h2 className="mb-6 flex justify-start gap-3 font-lora text-32 font-400 text-white-95">
|
||||
<span className="text-orange">1</span> Download Status
|
||||
</h2>
|
||||
<div className="mb-6 flex flex-col gap-5 rounded-28 border border-white-8 bg-white-4 p-3 pt-5">
|
||||
<Image
|
||||
src="/assets/status-logo.png"
|
||||
alt="Status logo"
|
||||
width={120}
|
||||
height={32}
|
||||
className="ml-3"
|
||||
/>
|
||||
<div className="flex flex-col gap-3 lg:flex-row">
|
||||
<DownloadStatusForDesktop title="Desktop" />
|
||||
<DownloadStatusForMobile title="Mobile" />
|
||||
</div>
|
||||
<FeaturesDisclaimer />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="my-6 flex justify-start gap-3 font-lora text-32 font-400 text-white-95">
|
||||
<span className="text-orange">2</span> Follow our guides
|
||||
</h2>
|
||||
<div className="flex flex-col gap-3 lg:flex-row">
|
||||
<div className="flex flex-1 flex-col rounded-28 border border-white-8 bg-white-4 p-6 pt-5">
|
||||
<h3 className="mb-1 font-lora text-24 font-400 text-white-95">
|
||||
If you never used Status
|
||||
</h3>
|
||||
<p className="mb-6 font-inter text-16 font-300 text-white-80">
|
||||
Learn how to create and store your keys directly on Keycard
|
||||
</p>
|
||||
<ButtonLink
|
||||
href="https://status.app/help/keycard/create-a-status-profile-using-keycard"
|
||||
variant="secondary"
|
||||
icon={<ExternalIcon />}
|
||||
className="mt-auto"
|
||||
>
|
||||
Get started
|
||||
</ButtonLink>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col rounded-28 border border-white-8 bg-white-4 p-6 pt-5">
|
||||
<h3 className="mb-1 font-lora text-24 font-400 text-white-95">
|
||||
If you already have a Status profile
|
||||
</h3>
|
||||
<p className="mb-6 font-inter text-16 font-300 text-white-80">
|
||||
Learn how to move your keys to Keycard
|
||||
</p>
|
||||
<ButtonLink
|
||||
href="https://status.app/help/keycard/secure-your-status-profile-or-wallet-with-keycard"
|
||||
variant="secondary"
|
||||
icon={<ExternalIcon />}
|
||||
className="mt-auto"
|
||||
>
|
||||
Get started
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Environment } from '@react-three/drei'
|
||||
import { Canvas } from '@react-three/fiber'
|
||||
import {
|
||||
DepthOfField,
|
||||
EffectComposer,
|
||||
Vignette,
|
||||
} from '@react-three/postprocessing'
|
||||
import { useIntersectionObserver } from '~/app/_hooks/use-intersection-observer'
|
||||
import { useWindowFocus } from '~/app/_hooks/use-window-focus'
|
||||
import { Image } from '~components/image'
|
||||
import { motion } from 'framer-motion'
|
||||
import { useRef, useState } from 'react'
|
||||
import { ErrorBoundary } from 'react-error-boundary'
|
||||
import { KeycardModel } from './model'
|
||||
|
||||
const cardsPositions: Record<
|
||||
Props['variant'],
|
||||
Array<{
|
||||
position: [number, number, number]
|
||||
rotation: [number, number, number]
|
||||
speed: number
|
||||
scale?: number
|
||||
}>
|
||||
> = {
|
||||
'thank-you': [
|
||||
{
|
||||
position: [25, 25, -38],
|
||||
rotation: [-0.3, 0.4, -0.8],
|
||||
speed: 5.3,
|
||||
},
|
||||
{
|
||||
position: [60, 45, -68],
|
||||
rotation: [1, 0, 0],
|
||||
speed: 5.3,
|
||||
},
|
||||
{
|
||||
position: [0, 15, -15],
|
||||
rotation: [-0.3, -0.6, 2.4],
|
||||
speed: 1.4,
|
||||
},
|
||||
{
|
||||
position: [14, 4, -15],
|
||||
rotation: [0, 0, 0.6],
|
||||
speed: 2.3,
|
||||
},
|
||||
{
|
||||
position: [42, 4, -30],
|
||||
rotation: [0, 0.4, 1.2],
|
||||
speed: 2.3,
|
||||
scale: 1.4,
|
||||
},
|
||||
{
|
||||
position: [-20.5, 0, -27.5],
|
||||
rotation: [-1.1, 0.8, -0.6],
|
||||
speed: 2.75,
|
||||
},
|
||||
{
|
||||
position: [-23, 12, -30],
|
||||
rotation: [0.5, 0.4, 0.6],
|
||||
speed: 1.45,
|
||||
},
|
||||
{
|
||||
position: [-50.9, 20, -40],
|
||||
rotation: [0, 0, 0.6],
|
||||
speed: 4.25,
|
||||
},
|
||||
{
|
||||
position: [-20.9, 35, -40],
|
||||
rotation: [-1.3, -0.8, 0.6],
|
||||
speed: 4.25,
|
||||
},
|
||||
{
|
||||
position: [-107.9, 10, -80],
|
||||
rotation: [2, 0, 0.6],
|
||||
speed: 2.25,
|
||||
},
|
||||
],
|
||||
homepage: [
|
||||
{
|
||||
position: [25, 25, -38],
|
||||
rotation: [-0.3, 0.4, -0.8],
|
||||
speed: 5.3,
|
||||
},
|
||||
{
|
||||
position: [60, 45, -68],
|
||||
rotation: [1, 0, 0],
|
||||
speed: 5.3,
|
||||
},
|
||||
{
|
||||
position: [0, 15, -15],
|
||||
rotation: [-0.3, -0.6, 2.4],
|
||||
speed: 1.4,
|
||||
},
|
||||
{
|
||||
position: [7, 4, -15],
|
||||
rotation: [0, 0, 0.6],
|
||||
speed: 2.3,
|
||||
},
|
||||
{
|
||||
position: [42, 4, -30],
|
||||
rotation: [0, 0.4, 1.2],
|
||||
speed: 2.3,
|
||||
scale: 1.4,
|
||||
},
|
||||
{
|
||||
position: [-7.5, 8, -27.5],
|
||||
rotation: [-1.1, 0.8, -0.6],
|
||||
speed: 2.75,
|
||||
},
|
||||
{
|
||||
position: [-23, 12, -30],
|
||||
rotation: [0.5, 0.4, 0.6],
|
||||
speed: 1.45,
|
||||
},
|
||||
{
|
||||
position: [-50.9, 20, -40],
|
||||
rotation: [0, 0, 0.6],
|
||||
speed: 4.25,
|
||||
},
|
||||
{
|
||||
position: [-20.9, 35, -40],
|
||||
rotation: [-1.3, -0.8, 0.6],
|
||||
speed: 4.25,
|
||||
},
|
||||
{
|
||||
position: [-107.9, 10, -80],
|
||||
rotation: [2, 0, 0.6],
|
||||
speed: 2.25,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
type Props = {
|
||||
variant: 'thank-you' | 'homepage'
|
||||
}
|
||||
|
||||
const Background = (props: Props) => {
|
||||
const { variant } = props
|
||||
|
||||
return (
|
||||
<ErrorBoundary fallback={<BackgroundImage variant={variant} />}>
|
||||
<BackgroundWebGL variant={variant} />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
export { Background }
|
||||
|
||||
// WEBGL Background
|
||||
const BackgroundWebGL = (props: Props) => {
|
||||
const { variant } = props
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
||||
const entry = useIntersectionObserver(wrapperRef, {
|
||||
rootMargin: '0%',
|
||||
})
|
||||
|
||||
const isVisible = !!entry?.isIntersecting
|
||||
const isFocused = useWindowFocus()
|
||||
|
||||
return (
|
||||
<div className="absolute size-full">
|
||||
<motion.div
|
||||
animate={{ opacity: isMounted ? 0 : 1 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: 'easeInOut',
|
||||
}}
|
||||
className="absolute left-0 top-0 z-0 size-full translate-y-[20px]"
|
||||
>
|
||||
<Image
|
||||
priority
|
||||
src={
|
||||
variant === 'homepage'
|
||||
? '/assets/placeholder.png'
|
||||
: '/assets/placeholder-thank-you.png'
|
||||
}
|
||||
alt="Keycard Placeholder"
|
||||
width={5478}
|
||||
height={2166}
|
||||
className="aspect-[5478/2166] h-[74.5%] w-full object-cover blur-2xl"
|
||||
/>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
ref={wrapperRef}
|
||||
animate={{
|
||||
opacity: isMounted ? 1 : 0,
|
||||
y: isMounted ? 0 : 20,
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: 'easeInOut',
|
||||
}}
|
||||
className="absolute left-0 top-0 z-0 size-full"
|
||||
>
|
||||
<Canvas
|
||||
frameloop={isVisible && isFocused ? 'always' : 'demand'}
|
||||
onCreated={() => {
|
||||
setIsMounted(true)
|
||||
}}
|
||||
gl={{
|
||||
precision: 'lowp',
|
||||
preserveDrawingBuffer: true,
|
||||
powerPreference: 'low-power',
|
||||
}}
|
||||
>
|
||||
<Environment
|
||||
preset="night"
|
||||
environmentIntensity={4}
|
||||
environmentRotation={[0, 14, -23]}
|
||||
/>
|
||||
|
||||
<ambientLight intensity={3.5} />
|
||||
<EffectComposer multisampling={0} enabled={isVisible}>
|
||||
<DepthOfField
|
||||
target={[0, 5, -30]}
|
||||
focalLength={0.015}
|
||||
bokehScale={4}
|
||||
/>
|
||||
<Vignette eskil={false} offset={0.55} darkness={1} />
|
||||
</EffectComposer>
|
||||
|
||||
<group scale={2} position={[0, -10, 0]}>
|
||||
{cardsPositions[variant].map((card, index) => (
|
||||
<group key={index} position={card.position} scale={card.scale}>
|
||||
<KeycardModel
|
||||
initialRotation={card.rotation}
|
||||
speed={card.speed}
|
||||
/>
|
||||
</group>
|
||||
))}
|
||||
</group>
|
||||
</Canvas>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Background image when webgl is not supported
|
||||
const BackgroundImage = (props: Props) => {
|
||||
const { variant } = props
|
||||
return (
|
||||
<div className="absolute size-full translate-y-[20px]">
|
||||
<Image
|
||||
src={
|
||||
variant === 'homepage'
|
||||
? '/assets/placeholder.png'
|
||||
: '/assets/placeholder-thank-you.png'
|
||||
}
|
||||
alt="Keycard"
|
||||
width={5478}
|
||||
height={2166}
|
||||
className="aspect-[5478/2166] h-[74.5%] w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useGLTF } from '@react-three/drei'
|
||||
import { extend, useFrame } from '@react-three/fiber'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import * as THREE from 'three'
|
||||
import { GLTF } from 'three-stdlib'
|
||||
|
||||
extend({ meshTransmissionMaterial: THREE.Mesh })
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
useGLTF.preload('/assets/keycard-model.glb')
|
||||
|
||||
const KeycardModel = (
|
||||
props: React.ComponentProps<'group'> & {
|
||||
speed: number
|
||||
initialRotation?: [number, number, number]
|
||||
},
|
||||
) => {
|
||||
const { speed, initialRotation = [0, 0, 0], ...rest } = props
|
||||
const { nodes, materials } = useGLTF(
|
||||
'/assets/keycard-model.glb',
|
||||
) as GLTFResult
|
||||
|
||||
const group = useRef<THREE.Group>(null)
|
||||
const [hovered, setHovered] = useState(false)
|
||||
|
||||
const initialRotationRef = useRef(new THREE.Euler(...initialRotation))
|
||||
const currentRotation = useRef(new THREE.Euler(...initialRotation))
|
||||
const modelPosition = useRef(new THREE.Vector3())
|
||||
|
||||
useEffect(() => {
|
||||
if (group.current) {
|
||||
group.current.rotation.set(...initialRotation)
|
||||
currentRotation.current.set(...initialRotation)
|
||||
}
|
||||
}, [initialRotation])
|
||||
|
||||
useFrame(state => {
|
||||
if (!group.current) return
|
||||
|
||||
const t = state.clock.getElapsedTime() * 0.5
|
||||
group.current.position.y = Math.sin(t * (speed * 1.5)) * 0.25
|
||||
|
||||
if (hovered) {
|
||||
group.current.getWorldPosition(modelPosition.current)
|
||||
|
||||
const screenX = ((state.pointer.x + 1) / 2) * state.viewport.width
|
||||
const screenY = ((state.pointer.y + 1) / 2) * state.viewport.height
|
||||
|
||||
const maxRotation = THREE.MathUtils.degToRad(30)
|
||||
|
||||
const rotX = THREE.MathUtils.clamp(
|
||||
((screenY - state.viewport.height / 2) / state.viewport.height) *
|
||||
maxRotation *
|
||||
1.5,
|
||||
-maxRotation,
|
||||
maxRotation,
|
||||
)
|
||||
const rotY = THREE.MathUtils.clamp(
|
||||
((screenX - state.viewport.width / 2) / state.viewport.width) *
|
||||
maxRotation *
|
||||
1.5,
|
||||
-maxRotation,
|
||||
maxRotation,
|
||||
)
|
||||
|
||||
currentRotation.current.x = initialRotationRef.current.x + rotX
|
||||
currentRotation.current.y = initialRotationRef.current.y + rotY
|
||||
currentRotation.current.z = initialRotationRef.current.z
|
||||
} else {
|
||||
currentRotation.current.x = THREE.MathUtils.lerp(
|
||||
currentRotation.current.x,
|
||||
initialRotationRef.current.x,
|
||||
0.15,
|
||||
)
|
||||
currentRotation.current.y = THREE.MathUtils.lerp(
|
||||
currentRotation.current.y,
|
||||
initialRotationRef.current.y,
|
||||
0.15,
|
||||
)
|
||||
currentRotation.current.z = THREE.MathUtils.lerp(
|
||||
currentRotation.current.z,
|
||||
initialRotationRef.current.z,
|
||||
0.15,
|
||||
)
|
||||
}
|
||||
|
||||
group.current.rotation.x = THREE.MathUtils.lerp(
|
||||
group.current.rotation.x,
|
||||
currentRotation.current.x,
|
||||
0.2,
|
||||
)
|
||||
group.current.rotation.y = THREE.MathUtils.lerp(
|
||||
group.current.rotation.y,
|
||||
currentRotation.current.y,
|
||||
0.2,
|
||||
)
|
||||
group.current.rotation.z = THREE.MathUtils.lerp(
|
||||
group.current.rotation.z,
|
||||
currentRotation.current.z,
|
||||
0.2,
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<group
|
||||
ref={group}
|
||||
scale={100}
|
||||
{...rest}
|
||||
onPointerOver={e => {
|
||||
e.stopPropagation()
|
||||
setHovered(true)
|
||||
}}
|
||||
onPointerOut={e => {
|
||||
e.stopPropagation()
|
||||
setHovered(false)
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
export { KeycardModel }
|
||||
@@ -10,14 +10,14 @@ type Props = {
|
||||
} & React.ComponentProps<typeof Link>
|
||||
|
||||
const buttonStyles = cva({
|
||||
base: 'inline-flex cursor-pointer gap-[6px] font-500 text-16 select-none items-center rounded-12 border transition-colors w-fit disabled:opacity-[0.3] disabled:cursor-default',
|
||||
base: 'inline-flex cursor-pointer gap-[6px] text-16 select-none items-center rounded-12 border transition-colors w-fit disabled:opacity-[0.3] disabled:cursor-default',
|
||||
variants: {
|
||||
variant: {
|
||||
primary: 'bg-orange border-white-12 hover:bg-orange-dark text-white-95',
|
||||
secondary:
|
||||
'bg-white-4 border-white-8 hover:bg-white-8 hover:border-white-12 text-white-95',
|
||||
'bg-white-3 border-white-8 hover:bg-white-8 hover:border-white-12 text-white-95',
|
||||
white: 'bg-white-100 hover:bg-white-dark text-dark-100',
|
||||
dark: 'border-[transparent] bg-white-4 hover:border-white-8 hover:bg-white-8 text-white-95',
|
||||
dark: 'border-[transparent] bg-white-3 hover:border-white-8 hover:bg-white-8 text-white-95',
|
||||
},
|
||||
withIcon: {
|
||||
true: 'pl-[14px] pr-[10px] py-2',
|
||||
@@ -52,6 +52,8 @@ const ButtonLink = (props: Props) => {
|
||||
...rest
|
||||
} = props
|
||||
|
||||
console.log(props)
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={cx([
|
||||
|
||||
@@ -10,15 +10,15 @@ type Props = {
|
||||
} & React.ComponentProps<'button'>
|
||||
|
||||
const buttonStyles = cva({
|
||||
base: 'inline-flex font-500 cursor-pointer gap-1 text-16 select-none items-center rounded-12 border transition-all w-fit disabled:opacity-[0.3] disabled:cursor-default',
|
||||
base: 'inline-flex cursor-pointer gap-1 text-16 select-none items-center rounded-12 border transition-all w-fit disabled:opacity-[0.3] disabled:cursor-default',
|
||||
variants: {
|
||||
variant: {
|
||||
primary:
|
||||
'bg-orange border-white-12 hover:enabled:bg-orange-dark text-white-95',
|
||||
secondary:
|
||||
'bg-white-4 border-white-8 hover:bg-white-8 hover:border-white-12 text-white-95',
|
||||
'bg-white-8 border-white-12 hover:enabled:bg-white-12 text-white-95',
|
||||
white: 'bg-white-100 hover:bg-white-dark text-dark-100',
|
||||
dark: 'border-[transparent] bg-white-4 hover:enabled:border-white-8 hover:enabled:bg-white-8 text-white-95',
|
||||
dark: 'border-[transparent] bg-white-3 hover:enabled:border-white-8 hover:enabled:bg-white-8 text-white-95',
|
||||
},
|
||||
withIcon: {
|
||||
true: 'pl-[14px] pr-[10px] py-2',
|
||||
@@ -62,7 +62,6 @@ const Button = forwardRef<HTMLButtonElement, Props>((props, ref) => {
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
{icon}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -2,33 +2,35 @@
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import * as Checkbox from '@radix-ui/react-checkbox'
|
||||
import {
|
||||
AddIcon,
|
||||
CheckIcon,
|
||||
CloseIcon,
|
||||
InfoIcon,
|
||||
LabelsIcon,
|
||||
LoadingIcon,
|
||||
RemoveIcon,
|
||||
WorldIcon,
|
||||
} from '@status-im/icons/20'
|
||||
import { Image } from '~components/image'
|
||||
import { RecommendedIcon } from '~icons/recommended'
|
||||
import { CartInput } from '~/server/shopify/storefront/validation'
|
||||
import { cx } from 'cva'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import Image from 'next/image'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useController, useForm } from 'react-hook-form'
|
||||
import type { SubmitHandler } from 'react-hook-form'
|
||||
import { z } from 'zod'
|
||||
import { KEYCARD_PRODUCTS } from '../_constants/shopify/products'
|
||||
import {
|
||||
Check,
|
||||
Close,
|
||||
Info,
|
||||
Labels,
|
||||
Loading,
|
||||
Minus,
|
||||
Plus,
|
||||
Recommended,
|
||||
World,
|
||||
} from '../_icons'
|
||||
import { useShopifyUTMParamsContext } from '../_providers/shopify-utm-params-provider'
|
||||
import { formatPrice } from '../_utils/format-price'
|
||||
import { createCart } from '../actions'
|
||||
import { Button } from './button'
|
||||
import * as Dialog from './dialog'
|
||||
import { Form } from './form/form'
|
||||
import { Tooltip } from './tooltip'
|
||||
|
||||
const formSchema = z
|
||||
const shopifySchema = z
|
||||
.object({
|
||||
bundleId: z.enum(['ONE_CARD_SET', 'TWO_CARDS_SET', 'THREE_CARDS_SET']),
|
||||
quantity: z.number(),
|
||||
@@ -36,50 +38,87 @@ const formSchema = z
|
||||
})
|
||||
.required()
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>
|
||||
type Shopify = z.infer<typeof shopifySchema>
|
||||
|
||||
function createCheckoutUrl(values: FormValues, utmParams: URLSearchParams) {
|
||||
const url = new URL(
|
||||
`https://get.keycard.tech/cart/${KEYCARD_PRODUCTS[values.bundleId].variantId}:${values.quantity}`,
|
||||
)
|
||||
|
||||
if (values.includeKeycardReader) {
|
||||
const readerVariant = `${KEYCARD_PRODUCTS.READER.variantId}:1`
|
||||
url.pathname += `,${readerVariant}`
|
||||
}
|
||||
|
||||
utmParams.forEach((value, key) => {
|
||||
url.searchParams.append(key, value)
|
||||
})
|
||||
|
||||
return url.toString()
|
||||
type Bundle = {
|
||||
id: Shopify['bundleId']
|
||||
name: string
|
||||
price: number
|
||||
cards: number
|
||||
image: string
|
||||
tag?: string
|
||||
}
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactElement
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
children: React.ReactElement
|
||||
}
|
||||
|
||||
const bundles: Bundle[] = [
|
||||
{
|
||||
id: 'THREE_CARDS_SET',
|
||||
name: '3 card set',
|
||||
price: 64,
|
||||
cards: 3,
|
||||
image: '/assets/buy/3-card.png',
|
||||
tag: 'Best deal',
|
||||
},
|
||||
{
|
||||
id: 'TWO_CARDS_SET',
|
||||
name: '2 card set',
|
||||
price: 48,
|
||||
cards: 2,
|
||||
image: '/assets/buy/2-card.png',
|
||||
},
|
||||
{
|
||||
id: 'ONE_CARD_SET',
|
||||
name: '1 card set',
|
||||
price: 25,
|
||||
cards: 1,
|
||||
image: '/assets/buy/1-card.png',
|
||||
},
|
||||
] as const
|
||||
|
||||
const BuyKeycardDialog = (props: Props) => {
|
||||
const { children, ...rest } = props
|
||||
const { children } = props
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={setOpen} {...rest}>
|
||||
{children && <Dialog.Trigger asChild>{children}</Dialog.Trigger>}
|
||||
<Dialog.Content
|
||||
onOpenAutoFocus={event => {
|
||||
event.preventDefault()
|
||||
const router = useRouter()
|
||||
const utmParams = useShopifyUTMParamsContext()
|
||||
|
||||
const element = event.target as HTMLElement
|
||||
element.focus()
|
||||
}}
|
||||
className="fixed left-1/2 top-1/2 z-50 max-h-screen w-screen max-w-[1136px] -translate-x-1/2 -translate-y-1/2 overflow-auto focus:outline-none data-[state=open]:animate-contentShow lg:w-[90vw] lg:overflow-hidden"
|
||||
>
|
||||
const onSubmit: SubmitHandler<Shopify> = async data => {
|
||||
const products: CartInput = [
|
||||
{
|
||||
productId: KEYCARD_PRODUCTS[data.bundleId].productId,
|
||||
quantity: data.quantity,
|
||||
},
|
||||
]
|
||||
|
||||
if (data.includeKeycardReader) {
|
||||
products.push({
|
||||
productId: KEYCARD_PRODUCTS.READER.productId,
|
||||
quantity: 1,
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: consider add some ui to error handling - toast or something similar
|
||||
const shopifyCartUrl = await createCart(products)
|
||||
|
||||
const url = new URL(shopifyCartUrl)
|
||||
|
||||
utmParams.forEach((value, key) => {
|
||||
url.searchParams.append(key, value)
|
||||
})
|
||||
|
||||
router.push(url.toString())
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Trigger asChild>{children}</Dialog.Trigger>
|
||||
<Dialog.Content className="fixed left-1/2 top-1/2 z-[70] max-h-screen w-screen max-w-[1136px] -translate-x-1/2 -translate-y-1/2 overflow-auto focus:outline-none data-[state=open]:animate-contentShow lg:w-[90vw] lg:overflow-hidden">
|
||||
<Dialog.Description className="sr-only">Buy Keycard</Dialog.Description>
|
||||
<ShopifyForm />
|
||||
<ShopifyForm onSubmit={onSubmit} setOpen={setOpen} />
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
)
|
||||
@@ -87,9 +126,16 @@ const BuyKeycardDialog = (props: Props) => {
|
||||
|
||||
export { BuyKeycardDialog }
|
||||
|
||||
const ShopifyForm = () => {
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
type ShopifyFormProps = {
|
||||
onSubmit: SubmitHandler<Shopify>
|
||||
setOpen: (open: boolean) => void
|
||||
}
|
||||
|
||||
const ShopifyForm = (props: ShopifyFormProps) => {
|
||||
const { onSubmit, setOpen } = props
|
||||
|
||||
const form = useForm<Shopify>({
|
||||
resolver: zodResolver(shopifySchema),
|
||||
defaultValues: {
|
||||
bundleId: 'THREE_CARDS_SET',
|
||||
includeKeycardReader: true,
|
||||
@@ -97,14 +143,20 @@ const ShopifyForm = () => {
|
||||
},
|
||||
mode: 'onTouched',
|
||||
})
|
||||
const { formState, watch, setValue } = form
|
||||
const { isSubmitting } = formState
|
||||
|
||||
const router = useRouter()
|
||||
const utmParams = useShopifyUTMParamsContext()
|
||||
const {
|
||||
formState: { isSubmitting },
|
||||
watch,
|
||||
setValue,
|
||||
} = form
|
||||
|
||||
const submitHandler: SubmitHandler<Shopify> = async data => {
|
||||
return onSubmit(data)
|
||||
}
|
||||
|
||||
const selectedBundle = watch('bundleId')
|
||||
const quantity = watch('quantity')
|
||||
const includeReader = watch('includeKeycardReader')
|
||||
|
||||
const { field } = useController({
|
||||
control: form.control,
|
||||
@@ -112,15 +164,15 @@ const ShopifyForm = () => {
|
||||
})
|
||||
|
||||
const total = useMemo(() => {
|
||||
const bundlePrice = KEYCARD_PRODUCTS[selectedBundle].price
|
||||
const readerPrice = 0 // includeReader ? 22 : 0
|
||||
const bundlePrice = bundles.find(b => b.id === selectedBundle)?.price || 0
|
||||
const readerPrice = includeReader ? 22 : 0
|
||||
|
||||
return bundlePrice * quantity + readerPrice
|
||||
}, [selectedBundle, quantity])
|
||||
}, [selectedBundle, quantity, includeReader])
|
||||
|
||||
return (
|
||||
<div className="grid h-svh grid-cols-1 gap-6 overflow-auto bg-white-4 p-5 backdrop-blur-[20px] lg:h-auto lg:grid-cols-2 lg:overflow-clip lg:rounded-28 lg:border lg:border-white-12 lg:p-2">
|
||||
<div className="hidden h-full rounded-20 bg-[#0C0C0C] lg:block">
|
||||
<div className="grid h-svh grid-cols-1 gap-6 overflow-auto bg-white-3 p-5 backdrop-blur-[20px] lg:h-auto lg:grid-cols-2 lg:rounded-28 lg:border lg:border-white-12 lg:p-2">
|
||||
<div className="hidden h-full rounded-28 bg-dark-100 lg:block">
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
key={selectedBundle}
|
||||
@@ -131,8 +183,8 @@ const ShopifyForm = () => {
|
||||
>
|
||||
<Image
|
||||
className="w-auto"
|
||||
src={KEYCARD_PRODUCTS[selectedBundle].image}
|
||||
alt={`${KEYCARD_PRODUCTS[selectedBundle].name} keycard`}
|
||||
src={bundles.find(b => b.id === selectedBundle)!.image}
|
||||
alt={`${bundles.find(b => b.id === selectedBundle)!.name} keycard`}
|
||||
width={400}
|
||||
height={300}
|
||||
priority
|
||||
@@ -144,86 +196,76 @@ const ShopifyForm = () => {
|
||||
<div className="flex flex-col justify-start lg:p-6 lg:pl-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<Dialog.Title className="font-lora text-32">Buy Keycard</Dialog.Title>
|
||||
<Dialog.Close asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="size-10 px-[9px] text-white-95"
|
||||
aria-label="Close"
|
||||
>
|
||||
<CloseIcon className="size-5" />
|
||||
</Button>
|
||||
</Dialog.Close>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="size-10 px-[9px] text-white-95"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<Close className="size-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Form {...form} onSubmit={() => {}}>
|
||||
<Form {...form} onSubmit={submitHandler}>
|
||||
<div className="pt-12 lg:pt-10">
|
||||
<h3 className="pb-2 text-12 text-white-80 lg:pb-3">
|
||||
SELECT BUNDLE
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4 lg:gap-6">
|
||||
{Object.entries(KEYCARD_PRODUCTS)
|
||||
.filter(([title]) => title !== 'READER')
|
||||
.map(([title, product]) => {
|
||||
const selected = selectedBundle === title
|
||||
{bundles.map(bundle => {
|
||||
const selected = selectedBundle === bundle.id
|
||||
|
||||
return (
|
||||
<button
|
||||
key={title}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setValue('bundleId', title as FormValues['bundleId'])
|
||||
}}
|
||||
className={cx(
|
||||
'relative flex flex-col items-start justify-between rounded-20 bg-white-4 px-4 py-3 text-left transition-colors duration-300 hover:[&>span]:-left-1 hover:[&>span]:-top-1 hover:[&>span]:size-[calc(100%+8px)] hover:[&>span]:rounded-[24px]',
|
||||
return (
|
||||
<button
|
||||
key={bundle.name}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setValue('bundleId', bundle.id)
|
||||
}}
|
||||
className={cx(
|
||||
'relative flex flex-col items-start justify-between rounded-20 bg-white-3 px-4 py-3 text-left transition-colors duration-300 hover:[&>span]:-left-1 hover:[&>span]:-top-1 hover:[&>span]:size-[calc(100%+8px)] hover:[&>span]:rounded-[24px]',
|
||||
selected ? 'outline outline-4 outline-[transparent]' : '',
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cx([
|
||||
'absolute border transition-all',
|
||||
selected
|
||||
? 'outline outline-4 outline-[transparent]'
|
||||
: '',
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cx([
|
||||
'absolute z-0 border transition-all',
|
||||
selected
|
||||
? '-left-1 -top-1 size-[calc(100%+8px)] rounded-[24px] border-orange-dark'
|
||||
: 'left-0 top-0 size-full rounded-20 border-white-12',
|
||||
])}
|
||||
/>
|
||||
? '-left-1 -top-1 size-[calc(100%+8px)] rounded-[24px] border-orange-dark'
|
||||
: 'left-0 top-0 size-full rounded-20 border-white-12',
|
||||
])}
|
||||
/>
|
||||
|
||||
<div className="font-300 text-white-60">
|
||||
{product.cards} card set
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-between font-lora text-24 font-400">
|
||||
{formatPrice({
|
||||
amount: Number(product.price),
|
||||
})}
|
||||
{!!product.tag && (
|
||||
<Tooltip label={product.tag}>
|
||||
<div className="z-50 flex size-5 items-center justify-center rounded-full bg-orange">
|
||||
<RecommendedIcon />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})
|
||||
.reverse()}
|
||||
<div className="font-300 text-white-60">
|
||||
{bundle.cards} card set
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-between font-lora text-24 font-400">
|
||||
${bundle.price}
|
||||
{bundle.tag && (
|
||||
<Tooltip label="Best deal">
|
||||
<div className="flex size-5 items-center justify-center rounded-full bg-orange">
|
||||
<Recommended />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-8">
|
||||
<h3 className="pb-2 text-12 text-white-80">NUMBER OF SETS</h3>
|
||||
<div className="flex items-center gap-4 rounded-16 border border-white-12 bg-white-4 p-1">
|
||||
<div className="flex items-center gap-4 rounded-16 border border-white-12 bg-white-3 p-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="dark"
|
||||
onClick={() => setValue('quantity', Math.max(1, quantity - 1))}
|
||||
className="justify-center px-[9px] text-center text-white-100"
|
||||
disabled={quantity === 1}
|
||||
aria-label="Decrease quantity"
|
||||
>
|
||||
<RemoveIcon />
|
||||
<Minus />
|
||||
</Button>
|
||||
<span className="flex-1 text-center text-16">{quantity}</span>
|
||||
<Button
|
||||
@@ -231,27 +273,25 @@ const ShopifyForm = () => {
|
||||
variant="dark"
|
||||
onClick={() => setValue('quantity', quantity + 1)}
|
||||
className="justify-center px-[9px] text-center text-white-100"
|
||||
aria-label="Increase quantity"
|
||||
>
|
||||
<AddIcon />
|
||||
<Plus />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="py-8">
|
||||
<h3 className="mb-2 text-12 text-white-80">DESKTOP SUPPORT</h3>
|
||||
<div className="flex items-center justify-between space-x-3 rounded-16 border border-white-12 bg-white-4 p-3 pr-4">
|
||||
<div className="flex items-center justify-between space-x-3 rounded-16 border border-white-12 bg-white-3 p-3 pr-4">
|
||||
<div className="relative flex items-center justify-start">
|
||||
<Checkbox.Root
|
||||
{...form.register('includeKeycardReader')}
|
||||
id="includeKeycardReader"
|
||||
className="flex size-6 appearance-none items-center justify-center rounded-[8px] border border-white-20 bg-white-4 outline-none aria-checked:bg-orange aria-checked:hover:bg-orange-dark [&>svg]:aria-checked:text-white-95"
|
||||
className="flex size-6 appearance-none items-center justify-center rounded-[8px] bg-white-100 outline-none aria-checked:bg-orange aria-checked:hover:bg-orange-dark [&>svg]:aria-checked:text-white-95"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
aria-label="Include USB-C Keycard reader"
|
||||
>
|
||||
<Checkbox.Indicator className="text-white-95">
|
||||
<CheckIcon className="size-5 text-white-95" />
|
||||
<Check className="size-5 text-white-95" />
|
||||
</Checkbox.Indicator>
|
||||
</Checkbox.Root>
|
||||
|
||||
@@ -259,66 +299,42 @@ const ShopifyForm = () => {
|
||||
className="ml-3 mr-2 text-16 font-300 text-white-95"
|
||||
htmlFor="includeKeycardReader"
|
||||
>
|
||||
Include Keycard Reader (USB-C)
|
||||
Include USB-C Keycard reader
|
||||
</label>
|
||||
<Tooltip
|
||||
label={
|
||||
<>
|
||||
<p className="mb-2.5 font-400">
|
||||
For now, some features are only available via the Status
|
||||
Desktop app:
|
||||
<p className="font-400">
|
||||
For now, some features are only available on Desktop:
|
||||
</p>
|
||||
<ul className="grid list-disc gap-0.5 pl-4">
|
||||
<li className="font-300">
|
||||
Support for multiple wallet accounts
|
||||
</li>
|
||||
<li className="font-300">
|
||||
Support for non Status profile key pairs
|
||||
</li>
|
||||
<li className="font-300">Factory reset</li>
|
||||
<li className="font-300">Change PIN</li>
|
||||
<li className="font-300">Unlock</li>
|
||||
<li className="font-300">Backup</li>
|
||||
<ul className="flex list-disc flex-col gap-0.5 pl-4 pt-2">
|
||||
<li className="font-300">Something here</li>
|
||||
<li className="font-300">Amazing feature here</li>
|
||||
<li className="font-300">Great stuff here</li>
|
||||
</ul>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="flex">
|
||||
<InfoIcon className="flex-shrink-0 text-white-40 transition-colors hover:text-white-60" />
|
||||
<Info className="flex-shrink-0 text-white-40 transition-colors hover:text-white-60" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex gap-2 text-16 font-300 text-white-80">
|
||||
<span className="text-green">Free</span>
|
||||
<span className="line-through">
|
||||
{' '}
|
||||
{formatPrice({
|
||||
amount: Number(KEYCARD_PRODUCTS.READER.price),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-16 font-300 text-white-80">+$22</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-16 border border-white-12 bg-white-4 p-1">
|
||||
<div className="rounded-16 border border-white-12 bg-white-3 p-1">
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full justify-center font-500"
|
||||
onClick={() => {
|
||||
const checkoutUrl = createCheckoutUrl(
|
||||
form.getValues(),
|
||||
utmParams,
|
||||
)
|
||||
window.open(checkoutUrl, '_blank', 'noopener')
|
||||
router.push(`/thank-you?checkoutUrl=${checkoutUrl}`)
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<LoadingIcon className="my-px animate-spin text-white-100" />
|
||||
<Loading className="my-px animate-spin text-white-100" />
|
||||
) : (
|
||||
<>
|
||||
Checkout <div className="size-1 rounded-full bg-white-40" />
|
||||
{formatPrice({
|
||||
amount: total,
|
||||
})}
|
||||
Checkout <div className="size-1 rounded-full bg-white-40" /> $
|
||||
{total}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -339,22 +355,28 @@ const ShopifyForm = () => {
|
||||
height={32}
|
||||
/>
|
||||
<Image
|
||||
src="/assets/buy/amex.png"
|
||||
alt="American Express"
|
||||
src="/assets/buy/apple-pay.png"
|
||||
alt="Apple Pay"
|
||||
width={52}
|
||||
height={32}
|
||||
/>
|
||||
<Image
|
||||
src="/assets/buy/google-pay.png"
|
||||
alt="Google Pay"
|
||||
width={52}
|
||||
height={32}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-10 flex flex-col items-center gap-[10px] rounded-16 border border-dashed border-white-12 bg-white-4 px-4 py-[14px] text-14 text-white-60 lg:flex-row lg:justify-center lg:gap-2">
|
||||
<div className="mt-10 flex flex-col items-center gap-[10px] rounded-16 border border-dashed border-white-12 bg-white-3 px-4 py-[14px] text-14 text-white-60 lg:flex-row lg:justify-center lg:gap-2">
|
||||
<div className="flex items-center">
|
||||
<LabelsIcon className="mr-1 shrink-0 text-white-95" /> Prices
|
||||
don't include VAT
|
||||
<Labels className="mr-1 shrink-0" /> Prices don't include
|
||||
VAT
|
||||
</div>
|
||||
<div className="hidden size-1 rounded-full bg-white-40 lg:block" />
|
||||
|
||||
<div className="flex items-center">
|
||||
<WorldIcon className="mr-1 shrink-0 text-white-95" />
|
||||
Express shipping (3-5 days) available
|
||||
<World className="mr-1 shrink-0" />
|
||||
Delivery estimate: 3-5 business days
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { CheckIcon, CopyIcon } from '@status-im/icons/20'
|
||||
import { cx } from 'cva'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { onlyText } from 'react-children-utilities'
|
||||
import { useCopyToClipboard } from '../(with-navigation)/docs/_hooks/use-copy-to-clipboard'
|
||||
import { Check, Copy } from '../_icons'
|
||||
import { useCopyToClipboard } from '../docs/_hooks/use-copy-to-clipboard'
|
||||
|
||||
export function CodeBlock(props: React.ComponentProps<'figure'>) {
|
||||
const [, copy] = useCopyToClipboard()
|
||||
@@ -33,7 +33,7 @@ export function CodeBlock(props: React.ComponentProps<'figure'>) {
|
||||
)}
|
||||
aria-label="Copy code"
|
||||
>
|
||||
{success ? <CheckIcon /> : <CopyIcon />}
|
||||
{success ? <Check /> : <Copy />}
|
||||
</button>
|
||||
</div>
|
||||
{props.children}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { BulletIcon } from '@status-im/icons/20'
|
||||
import { Link } from '~components/link'
|
||||
import { cx } from 'cva'
|
||||
import { Children, cloneElement, ComponentProps } from 'react'
|
||||
import { match } from 'ts-pattern'
|
||||
import { Bullet } from '../_icons/bullet'
|
||||
import { renderText } from '../_utils/render-text'
|
||||
import { AnchorLink } from './anchor-link'
|
||||
import { CodeBlock } from './code-block'
|
||||
@@ -150,7 +150,7 @@ export const baseComponents = {
|
||||
{...rest}
|
||||
className={cx(
|
||||
blockquoteParagraphTextSize[size],
|
||||
'mt-5 border-l border-dashed border-white-4 !pt-0 pl-6',
|
||||
'mt-5 border-l border-dashed border-white-3 !pt-0 pl-6',
|
||||
)}
|
||||
>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
||||
@@ -264,7 +264,7 @@ export const baseComponents = {
|
||||
) => {
|
||||
const icon = match(props.parent)
|
||||
.with('ol', () => <Step value={props.order!} />)
|
||||
.otherwise(() => <BulletIcon />)
|
||||
.otherwise(() => <Bullet />)
|
||||
|
||||
return (
|
||||
<li className="flex items-start gap-2">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as BaseDialog from '@radix-ui/react-dialog'
|
||||
import { Image } from '~components/image'
|
||||
import { cx } from 'cva'
|
||||
import Image from 'next/image'
|
||||
import { forwardRef } from 'react'
|
||||
import { Button } from './button'
|
||||
|
||||
@@ -21,7 +21,10 @@ const Overlay = forwardRef<
|
||||
React.ComponentPropsWithoutRef<typeof BaseDialog.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<BaseDialog.Overlay
|
||||
className={cx('fixed inset-0 z-50 bg-dark-60 backdrop-blur-2xl', className)}
|
||||
className={cx(
|
||||
'fixed inset-0 z-[70] bg-dark-60 backdrop-blur-2xl',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
@@ -48,7 +51,7 @@ Content.displayName = BaseDialog.Content.displayName
|
||||
type Props = {
|
||||
title: string
|
||||
description: string
|
||||
// image: string
|
||||
image: string
|
||||
onClose: () => void
|
||||
footer: {
|
||||
title: string
|
||||
@@ -59,31 +62,31 @@ type Props = {
|
||||
}
|
||||
|
||||
const Success = (props: Props) => {
|
||||
const { title, description, footer, onClose } = props
|
||||
const { title, description, image, footer, onClose } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cx(
|
||||
'fixed inset-0 top-1/2 z-50 -mt-6 h-[623px] -translate-y-1/2 overflow-y-auto overflow-x-clip focus:outline-none',
|
||||
'fixed inset-0 top-1/2 z-[70] -mt-6 h-[623px] -translate-y-1/2 overflow-y-auto overflow-x-clip focus:outline-none',
|
||||
'md:left-1/2 md:mt-0 md:w-[570px] md:-translate-x-1/2',
|
||||
)}
|
||||
>
|
||||
<div className="relative grid place-items-center px-5">
|
||||
{/* <Image
|
||||
<Image
|
||||
src={image}
|
||||
alt={title}
|
||||
width={549}
|
||||
height={623}
|
||||
className="absolute left-1/2 top-0 z-0 max-w-fit -translate-x-1/2 md:max-w-full"
|
||||
/> */}
|
||||
/>
|
||||
<div className="z-10 flex flex-col items-center pt-72">
|
||||
<Title asChild>
|
||||
<h3 className="mb-3 text-center font-lora text-32 text-white-95">
|
||||
{title}
|
||||
</h3>
|
||||
</Title>
|
||||
<p className="max-w-[490px] pb-8 text-center text-20 font-300 text-white-80">
|
||||
<p className="mb-10 max-w-[490px] text-center text-20 font-300 text-white-80">
|
||||
{description}
|
||||
</p>
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
@@ -105,7 +108,7 @@ const Footer = (props: FooterProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'fixed inset-x-5 bottom-4 z-50 flex flex-col items-start justify-between gap-2 bg-white-8 p-1',
|
||||
'fixed inset-x-5 bottom-4 z-[70] flex flex-col items-start justify-between gap-2 bg-white-8 p-1',
|
||||
'rounded-20 border border-white-12',
|
||||
'md:bottom-12 md:left-1/2 md:w-[570px] md:-translate-x-1/2 md:flex-row md:items-center md:gap-4 md:pr-4',
|
||||
)}
|
||||
@@ -135,4 +138,4 @@ const Footer = (props: FooterProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
export { Content, Close, Description, Root, Title, Trigger, Success, Footer }
|
||||
export { Content, Close, Description, Root, Title, Trigger, Success }
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import {
|
||||
AppleIcon,
|
||||
ChevronDownIcon,
|
||||
LinuxIcon,
|
||||
WindowsIcon,
|
||||
} from '@status-im/icons/20'
|
||||
import {
|
||||
STATUS_APPS_DESKTOP_URL,
|
||||
STATUS_DESKTOP_DOWNLOAD_URL_LINUX,
|
||||
STATUS_DESKTOP_DOWNLOAD_URL_MACOS_INTEL,
|
||||
STATUS_DESKTOP_DOWNLOAD_URL_MACOS_SILICON,
|
||||
STATUS_DESKTOP_DOWNLOAD_URL_WINDOWS,
|
||||
} from '~/config/routes'
|
||||
import { downloadUrl } from '../_utils/download-url'
|
||||
import { Button } from './button'
|
||||
import { ButtonLink } from './button-link'
|
||||
|
||||
type Props = {
|
||||
show?: 'single' | 'all'
|
||||
}
|
||||
|
||||
export const DownloadDesktopButton = (props: Props) => {
|
||||
const { show = 'single' } = props
|
||||
|
||||
const prefix = 'Download for'
|
||||
|
||||
const macos = 'macOS'
|
||||
const windows = 'Windows'
|
||||
const linux = 'Linux'
|
||||
const desktop = 'desktop'
|
||||
|
||||
if (show === 'single') {
|
||||
return (
|
||||
<>
|
||||
<div className="hidden macos:contents">
|
||||
<MacOsPicker>
|
||||
<>
|
||||
{prefix} {macos}
|
||||
</>
|
||||
</MacOsPicker>
|
||||
</div>
|
||||
|
||||
<div className="hidden windows:contents">
|
||||
<WindowsDownloadButton>
|
||||
<>
|
||||
{prefix} {windows}
|
||||
</>
|
||||
</WindowsDownloadButton>
|
||||
</div>
|
||||
|
||||
<div className="hidden linux:contents">
|
||||
<LinuxDownloadButton>
|
||||
<>
|
||||
{prefix} {linux}
|
||||
</>
|
||||
</LinuxDownloadButton>
|
||||
</div>
|
||||
|
||||
<div className="hidden ios:contents android:contents unknown:contents">
|
||||
<DesktopDownloadButton>
|
||||
<>
|
||||
{prefix} {desktop}
|
||||
</>
|
||||
</DesktopDownloadButton>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="order-2 inline-flex macos:order-1 ios:order-1">
|
||||
<MacOsPicker>
|
||||
<span className="hidden macos:contents ios:contents">
|
||||
{prefix} {macos}
|
||||
</span>
|
||||
</MacOsPicker>
|
||||
</div>
|
||||
|
||||
<div className="order-2 inline-flex windows:order-1 android:order-1">
|
||||
<WindowsDownloadButton>
|
||||
<span className="hidden windows:contents android:contents unknown:contents">
|
||||
{prefix} {windows}
|
||||
</span>
|
||||
</WindowsDownloadButton>
|
||||
</div>
|
||||
|
||||
<div className="order-2 inline-flex linux:order-1 android:order-2 unknown:order-1">
|
||||
<LinuxDownloadButton>
|
||||
<span className="hidden linux:contents unknown:contents">
|
||||
{prefix} {linux}
|
||||
</span>
|
||||
</LinuxDownloadButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type DownloadButtonProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const LinuxDownloadButton = (props: DownloadButtonProps) => {
|
||||
const { children } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="hidden macos:contents windows:contents ios:contents android:contents">
|
||||
<ButtonLink
|
||||
variant="secondary"
|
||||
className="!p-[10px]"
|
||||
href={STATUS_DESKTOP_DOWNLOAD_URL_LINUX}
|
||||
aria-label="Download for Linux"
|
||||
>
|
||||
<LinuxIcon className="text-white-60" />
|
||||
</ButtonLink>
|
||||
</div>
|
||||
<div className="hidden linux:contents unknown:contents">
|
||||
<ButtonLink
|
||||
variant="secondary"
|
||||
href={STATUS_DESKTOP_DOWNLOAD_URL_LINUX}
|
||||
aria-label="Download for Linux"
|
||||
icon={<LinuxIcon className="text-white-60" />}
|
||||
>
|
||||
{children}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const WindowsDownloadButton = (props: DownloadButtonProps) => {
|
||||
const { children } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="hidden macos:contents linux:contents ios:contents unknown:contents">
|
||||
<ButtonLink
|
||||
variant="secondary"
|
||||
className="!p-[10px]"
|
||||
href={STATUS_DESKTOP_DOWNLOAD_URL_WINDOWS}
|
||||
aria-label="Download for Windows"
|
||||
>
|
||||
<WindowsIcon className="text-white-60" />
|
||||
</ButtonLink>
|
||||
</div>
|
||||
<div className="hidden windows:contents android:contents">
|
||||
<ButtonLink
|
||||
variant="secondary"
|
||||
href={STATUS_DESKTOP_DOWNLOAD_URL_WINDOWS}
|
||||
aria-label="Download for Windows"
|
||||
icon={<WindowsIcon className="text-white-60" />}
|
||||
>
|
||||
{children}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const MacOsPicker = (props: DownloadButtonProps) => {
|
||||
const { children } = props
|
||||
return (
|
||||
<DropdownMenu.Root modal={false}>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={
|
||||
<div className="ml-1 flex size-[14px] items-center justify-center rounded-full bg-white-12">
|
||||
<ChevronDownIcon />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<AppleIcon className="text-white-60" /> {children}
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
sideOffset={4}
|
||||
align="start"
|
||||
className="flex w-[225px] flex-col gap-1 rounded-12 border border-white-12 bg-dark-60 p-1"
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
className="cursor-pointer rounded-[8px] bg-white-4 px-3 py-1 transition-colors hover:bg-white-8"
|
||||
onSelect={() =>
|
||||
downloadUrl(STATUS_DESKTOP_DOWNLOAD_URL_MACOS_SILICON)
|
||||
}
|
||||
>
|
||||
Apple Silicon
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
className="cursor-pointer rounded-[8px] bg-white-4 px-3 py-1 transition-colors hover:bg-white-8"
|
||||
onSelect={() =>
|
||||
downloadUrl(STATUS_DESKTOP_DOWNLOAD_URL_MACOS_INTEL)
|
||||
}
|
||||
>
|
||||
Intel
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
}
|
||||
|
||||
const DesktopDownloadButton = (props: DownloadButtonProps) => {
|
||||
const { children } = props
|
||||
return (
|
||||
<>
|
||||
<div className="hidden ios:contents android:contents unknown:contents">
|
||||
<ButtonLink
|
||||
variant="secondary"
|
||||
href={STATUS_APPS_DESKTOP_URL}
|
||||
aria-label="Download for desktop"
|
||||
>
|
||||
{children}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { DownloadDesktopButton } from '~components/download-desktop-button'
|
||||
import { cx } from 'cva'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
const DownloadStatusForDesktop = (props: Props) => {
|
||||
const { className, title = 'Download Status for desktop' } = props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx([
|
||||
'flex w-full flex-col gap-6 rounded-28 border border-white-8 bg-white-4 p-6 pt-5',
|
||||
className,
|
||||
])}
|
||||
>
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<p className="font-lora text-24 font-400 text-white-95">{title}</p>
|
||||
<p className="font-300 text-white-80">
|
||||
Available for Mac, Windows and Linux
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<DownloadDesktopButton show="all" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { DownloadStatusForDesktop }
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Image } from '~components/image'
|
||||
import {
|
||||
STATUS_MOBILE_APP_STORE_URL,
|
||||
STATUS_MOBILE_F_DROID_URL,
|
||||
STATUS_MOBILE_GOOGLE_PLAY_URL,
|
||||
} from '~config/routes'
|
||||
import { cx } from 'cva'
|
||||
import Link from 'next/link'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
const DownloadStatusForMobile = (props: Props) => {
|
||||
const { className, title = 'Download Status for mobile' } = props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx([
|
||||
'flex w-full flex-col gap-6 rounded-28 border border-white-4 bg-white-4 p-6 pt-5',
|
||||
className,
|
||||
])}
|
||||
>
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<p className="font-lora text-24 font-400 text-white-95">{title}</p>
|
||||
<p className="font-300 text-white-80">Available for iOS or Android</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link href={STATUS_MOBILE_APP_STORE_URL}>
|
||||
<Image
|
||||
src="/assets/keycard/appstore.png"
|
||||
width={140}
|
||||
height={40}
|
||||
alt="Download on App Store"
|
||||
className="h-10 w-auto"
|
||||
/>
|
||||
</Link>
|
||||
<Link href={STATUS_MOBILE_GOOGLE_PLAY_URL}>
|
||||
<Image
|
||||
src="/assets/keycard/googleplay.png"
|
||||
width={142}
|
||||
height={40}
|
||||
className="h-10 w-auto"
|
||||
alt="Get it on Google Play"
|
||||
/>
|
||||
</Link>
|
||||
<Link href={STATUS_MOBILE_F_DROID_URL}>
|
||||
<Image
|
||||
src="/assets/keycard/fdroid.png"
|
||||
width={120}
|
||||
height={40}
|
||||
className="h-10 w-auto"
|
||||
alt="Get it on F-Droid"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { DownloadStatusForMobile }
|
||||
@@ -1,163 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { CheckIcon, CloseIcon } from '@status-im/icons/20'
|
||||
import { Button } from '~components/button'
|
||||
import * as Dialog from '~components/dialog'
|
||||
import { cx } from 'cva'
|
||||
import { useState } from 'react'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactElement
|
||||
}
|
||||
|
||||
const features: Array<Feature> = [
|
||||
{
|
||||
name: 'Create Status profile',
|
||||
mobile: true,
|
||||
desktop: true,
|
||||
},
|
||||
{
|
||||
name: 'Migrate profile to new card',
|
||||
mobile: true,
|
||||
desktop: true,
|
||||
},
|
||||
{
|
||||
name: 'Log in',
|
||||
mobile: true,
|
||||
desktop: true,
|
||||
},
|
||||
{
|
||||
name: 'Send and Bridge',
|
||||
mobile: true,
|
||||
desktop: true,
|
||||
},
|
||||
{
|
||||
name: 'Swap',
|
||||
mobile: true,
|
||||
desktop: true,
|
||||
},
|
||||
{
|
||||
name: 'dApp interactions',
|
||||
mobile: 'Q1 2025',
|
||||
desktop: true,
|
||||
},
|
||||
{
|
||||
name: 'Backup',
|
||||
mobile: 'Q1 2025',
|
||||
desktop: true,
|
||||
},
|
||||
{
|
||||
name: 'Change PIN',
|
||||
mobile: 'Q1 2025',
|
||||
desktop: true,
|
||||
},
|
||||
{
|
||||
name: 'Unlock',
|
||||
mobile: 'Q1 2025',
|
||||
desktop: true,
|
||||
},
|
||||
{
|
||||
name: 'Factory reset',
|
||||
mobile: 'Q1 2025',
|
||||
desktop: true,
|
||||
},
|
||||
] as const
|
||||
|
||||
type FeatureVariant = string | true
|
||||
|
||||
type Feature = {
|
||||
name: string
|
||||
mobile: FeatureVariant
|
||||
desktop: FeatureVariant
|
||||
}
|
||||
|
||||
const FeaturesDialog = (props: Props) => {
|
||||
const { children } = props
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const products: Array<keyof Feature> = ['desktop', 'mobile']
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Trigger asChild>{children}</Dialog.Trigger>
|
||||
<Dialog.Content>
|
||||
<div
|
||||
className={cx(
|
||||
'fixed inset-0 z-50 h-full overflow-y-auto focus:outline-none',
|
||||
'md:left-1/2 md:top-1/2 md:h-fit md:w-[570px] md:-translate-x-1/2 md:-translate-y-1/2',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
'grid w-full max-w-[570px] overflow-auto border-0 border-white-12 bg-[transparent]',
|
||||
'md:rounded-28 md:border md:bg-white-4',
|
||||
)}
|
||||
>
|
||||
<Dialog.Title asChild className="p-6">
|
||||
<h3 className="font-lora text-32">Keycard features on Status</h3>
|
||||
</Dialog.Title>
|
||||
|
||||
<Dialog.Close asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="absolute right-5 top-5 z-50 size-10 px-[9px] text-white-95 md:right-6 md:top-6"
|
||||
onClick={() => setOpen(false)}
|
||||
aria-label="Close"
|
||||
>
|
||||
<CloseIcon className="size-5" />
|
||||
</Button>
|
||||
</Dialog.Close>
|
||||
|
||||
<div className="px-6 pt-[10px]">
|
||||
<div className="grid grid-cols-[1.33fr,1fr,1fr] justify-items-center text-center font-lora text-24 font-400 text-white-90 lg:grid-cols-[2fr,1fr,1fr] lg:text-24">
|
||||
<div className="justify-self-start">Feature</div>
|
||||
<div>Desktop</div>
|
||||
<div>Mobile</div>
|
||||
</div>
|
||||
<div className="">
|
||||
{features.map((feature, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="grid grid-cols-[1.33fr,1fr,1fr] border-b border-dashed border-white-12 font-300 first:pt-2 last:border-b-0 lg:grid-cols-[2fr,1fr,1fr]"
|
||||
>
|
||||
<div className="w-full p-3 first:pl-0 lg:flex lg:items-center">
|
||||
<span className="relative inline whitespace-normal">
|
||||
{feature.name.split(' ').slice(0, -1).join(' ')}{' '}
|
||||
<span className="inline-flex items-center">
|
||||
{feature.name.split(' ').slice(-1)[0]}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
{products.map(product => (
|
||||
<div
|
||||
key={product}
|
||||
className="relative flex items-center justify-center last:pr-0"
|
||||
>
|
||||
<FeatureInfo variant={feature[product]} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { FeaturesDialog }
|
||||
|
||||
const FeatureInfo = ({ variant }: { variant: FeatureVariant }) => {
|
||||
if (variant === true) return <CheckIcon className="text-orange" />
|
||||
if (typeof variant === 'string')
|
||||
return (
|
||||
<p className="whitespace-nowrap text-center text-16 text-white-95">
|
||||
{variant}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { CustomizeIcon, UsbIcon } from '@status-im/icons/20'
|
||||
import { FeaturesDialog } from './features-dialog'
|
||||
|
||||
const FeaturesDisclaimer = () => {
|
||||
return (
|
||||
<div className="w-full @container/main">
|
||||
<div className="flex flex-col items-start justify-center gap-2 rounded-16 border border-dashed border-white-12 bg-white-4 px-4 py-[14px] @[519px]/main:flex-row @[519px]/main:items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<UsbIcon className="shrink-0" />
|
||||
<p className="text-14 font-300 text-white-60">
|
||||
Desktop requires an NFC card reader
|
||||
</p>
|
||||
</div>
|
||||
<div className="hidden size-1 rounded-full bg-white-40 @[519px]/main:block" />
|
||||
<div className="flex items-center gap-1">
|
||||
<CustomizeIcon className="shrink-0" />
|
||||
<p className="text-14 font-300 text-white-60">
|
||||
Read full{' '}
|
||||
<FeaturesDialog>
|
||||
<button className="underline transition-colors hover:text-orange">
|
||||
feature list
|
||||
</button>
|
||||
</FeaturesDialog>{' '}
|
||||
by platform
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { FeaturesDisclaimer }
|
||||
@@ -1,7 +0,0 @@
|
||||
'use client'
|
||||
|
||||
const Copyright = () => {
|
||||
return <>© Keycard {new Date().getFullYear()}</>
|
||||
}
|
||||
|
||||
export { Copyright }
|
||||
@@ -1,36 +1,25 @@
|
||||
import { ROUTES } from '~/config/routes'
|
||||
import { Link } from '~components/link'
|
||||
import { Logo } from '~components/logo'
|
||||
import { Copyright } from './copyright'
|
||||
import { Section } from './section'
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<footer className="mt-auto rounded-28 bg-orange pt-8 selection:bg-dark-60">
|
||||
<div className="relative">
|
||||
<div className="absolute flex items-start pl-6">
|
||||
<footer className="rounded-28 bg-orange pt-8">
|
||||
<div className="grid grid-cols-2 divide-x divide-dashed divide-white-20 lg:grid-cols-6">
|
||||
<div className="flex items-start border-b border-dashed border-white-20 p-6 pr-0 pt-0">
|
||||
<Link href="/" aria-label="Homepage">
|
||||
<Logo />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="m-auto max-w-[1512px]">
|
||||
<div className="grid grid-cols-2 divide-x divide-dashed divide-white-20 lg:grid-cols-5 lg:pl-[152px] lg:pr-0 xl:pl-[282px]">
|
||||
<div
|
||||
key="empty"
|
||||
title="empty"
|
||||
className="col-span-2 h-[72px] border-b border-dashed border-white-20 lg:col-span-1 lg:border-0"
|
||||
/>
|
||||
|
||||
{Object.entries(ROUTES).map(([title, links]) => (
|
||||
<Section key={title} title={title} routes={links} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{Object.entries(ROUTES).map(([title, links]) => (
|
||||
<Section key={title} title={title} routes={links} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-between gap-2 border-0 border-dashed border-white-20 px-5 py-4 md:flex-row md:gap-0 lg:border-t lg:px-6">
|
||||
<div className="flex flex-col items-center justify-between gap-2 px-5 py-4 md:flex-row md:gap-0 lg:px-6">
|
||||
<span className="text-16 font-300 text-white-80">
|
||||
<Copyright />
|
||||
© Keycard {new Date().getFullYear()}
|
||||
</span>
|
||||
<p className="text-center text-16 font-300 text-white-80 md:text-left">
|
||||
Keycard is part of the{' '}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ExternalIcon } from '@status-im/icons/20'
|
||||
import { Routes } from '~/config/routes'
|
||||
import { Link } from '~components/link'
|
||||
import { External } from '~icons'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
@@ -11,7 +11,7 @@ const Section = (props: Props) => {
|
||||
const { title, routes } = props
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col gap-6 border-b border-dashed border-white-20 p-6 lg:border-0 lg:pb-12 lg:pt-0 lg:[&:nth-child(1)]:col-start-2">
|
||||
<div className="relative flex flex-col gap-6 border-b border-dashed border-white-20 p-6 lg:pb-12 lg:pt-0 [&:nth-child(2)]:pt-0">
|
||||
<p className="text-12 uppercase text-white-80">{title}</p>
|
||||
<ul className="grid gap-2">
|
||||
{routes.map(route => {
|
||||
@@ -26,7 +26,7 @@ const Section = (props: Props) => {
|
||||
<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 className="text-white-60" />}
|
||||
{external && <External />}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { CloseIcon, LoadingIcon } from '@status-im/icons/20'
|
||||
import { cx } from 'cva'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useController, useForm } from 'react-hook-form'
|
||||
import type { SubmitHandler } from 'react-hook-form'
|
||||
import { Close, Confetti } from '../_icons'
|
||||
import { Loading } from '../_icons/loading'
|
||||
import { signUpSchema, type SignUp } from '../_lib/google/validation'
|
||||
import { handleSignUp } from '../actions'
|
||||
import { Button } from './button'
|
||||
@@ -57,11 +58,12 @@ export const GetNotifiedDialog = (props: Props) => {
|
||||
{showSuccess ? (
|
||||
<Dialog.Success
|
||||
title="We'll notify you!"
|
||||
description="You have successfully signed up to be notified."
|
||||
description="You have successfully signed up to be notified when Keycard Shell becomes available to buy next year."
|
||||
image="/assets/sign-up-success.png"
|
||||
onClose={() => setOpen(false)}
|
||||
footer={{
|
||||
title: "Don't want to wait?",
|
||||
description: 'Get started with Keycard today',
|
||||
description: 'Get started with Keycard',
|
||||
Dialog: BuyKeycardDialog,
|
||||
buttonText: 'Buy Keycard',
|
||||
}}
|
||||
@@ -69,7 +71,7 @@ export const GetNotifiedDialog = (props: Props) => {
|
||||
) : (
|
||||
<div
|
||||
className={cx(
|
||||
'fixed inset-0 z-50 h-full overflow-y-auto focus:outline-none',
|
||||
'fixed inset-0 z-[70] h-full overflow-y-auto focus:outline-none',
|
||||
'md:left-1/2 md:top-1/2 md:h-fit md:w-[570px] md:-translate-x-1/2 md:-translate-y-1/2',
|
||||
)}
|
||||
>
|
||||
@@ -77,16 +79,16 @@ export const GetNotifiedDialog = (props: Props) => {
|
||||
<div className="absolute right-5 top-5 z-50 md:right-6 md:top-7">
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="rounded-12 border border-white-12 bg-white-4 p-[10px] text-white-100 hover:bg-white-8"
|
||||
className="rounded-12 border border-white-12 bg-white-3 p-[10px] text-white-100 hover:bg-white-8"
|
||||
>
|
||||
<CloseIcon />
|
||||
<Close />
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Close>
|
||||
<div
|
||||
className={cx(
|
||||
'grid max-w-[570px] overflow-hidden border-0 border-white-12 bg-[transparent]',
|
||||
'md:rounded-28 md:border md:bg-white-4',
|
||||
'md:rounded-28 md:border md:bg-white-3',
|
||||
)}
|
||||
>
|
||||
<div className="p-5 pb-6 md:px-6">
|
||||
@@ -97,16 +99,16 @@ export const GetNotifiedDialog = (props: Props) => {
|
||||
</Dialog.Title>
|
||||
<SignUpForm onSubmit={onSubmit} error={error} />
|
||||
</div>
|
||||
{/* <div
|
||||
<div
|
||||
className={cx(
|
||||
'mx-5 flex items-center justify-center gap-2 bg-white-4 px-4 py-[14px] text-14 font-300 text-white-60',
|
||||
'mx-5 flex items-center justify-center gap-2 bg-white-3 px-4 py-[14px] text-14 font-300 text-white-60',
|
||||
'rounded-16 border border-dashed border-white-12',
|
||||
'md:mx-0 md:justify-start md:rounded-0 md:border-x-0 md:border-b-0 md:px-6',
|
||||
)}
|
||||
>
|
||||
<ConfettiColorfulIcon className="size-5" /> Keycard Shell is
|
||||
launching in 2025
|
||||
</div> */}
|
||||
<Confetti className="size-5" /> Keycard Shell is launching in
|
||||
2025
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -158,7 +160,7 @@ const SignUpForm = (props: SignUpFormProps) => {
|
||||
className={cx(
|
||||
// input has to have at least 16px font size to prevent from "zooming in" on mobile
|
||||
'h-12 w-full border border-solid px-4 py-1 text-16 font-300 text-white-95 placeholder-white-40 caret-orange max-sm:text-[1rem]',
|
||||
'rounded-16 border-white-12 bg-white-4 hover:bg-white-8 focus:border-white-20',
|
||||
'rounded-16 border-white-12 bg-white-3 hover:bg-white-8 focus:border-white-20',
|
||||
'disabled:cursor-not-allowed disabled:border-0 disabled:bg-white-8 disabled:text-white-40',
|
||||
)}
|
||||
/>
|
||||
@@ -175,7 +177,7 @@ const SignUpForm = (props: SignUpFormProps) => {
|
||||
className="absolute right-1 top-8 h-10 min-w-[101px] justify-center border border-white-20"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<LoadingIcon className="my-px animate-spin text-white-100" />
|
||||
<Loading className="my-px animate-spin text-white-100" />
|
||||
) : (
|
||||
'Notify me'
|
||||
)}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import NextImage, { ImageProps } from 'next/image'
|
||||
|
||||
export const Image = (props: ImageProps) => {
|
||||
return <NextImage quality={100} {...props} />
|
||||
}
|
||||
@@ -1,22 +1,24 @@
|
||||
import React from 'react'
|
||||
|
||||
const Logo = (props: React.SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="95"
|
||||
width="23"
|
||||
height="40"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<g fill="#fff" clipPath="url(#a)">
|
||||
<path d="M25.771 34.206c-1.415 0-2.627-.302-3.638-.908a6.487 6.487 0 0 1-2.425-2.522c-.607-1.11-.81-2.321-.81-3.835 0-1.412.304-2.724.81-3.834a5.878 5.878 0 0 1 2.324-2.623 6.989 6.989 0 0 1 3.638-1.01c1.213 0 2.325.303 3.234.808 1.01.504 1.718 1.312 2.325 2.32.606 1.01.808 2.321.808 3.835v1.513h-9.6c.1 1.01.504 1.918 1.01 2.523.606.605 1.314.908 2.425.908.809 0 1.516-.202 1.92-.605.405-.404.708-.808.91-1.413l3.234.908c-.202.605-.505 1.312-1.01 1.917a5.317 5.317 0 0 1-2.022 1.514c-.91.303-1.92.504-3.133.504Zm-.202-11.704c-.808 0-1.516.302-2.021.807-.505.504-.91 1.21-1.112 2.22h6.165c0-.807-.303-1.514-.808-2.12-.607-.605-1.314-.907-2.224-.907ZM36.989 34.61l.303-.707-5.154-14.127h3.74l3.132 9.384h.202l3.133-9.384h3.74L40.02 36.123c-.203.504-.506 1.11-.91 1.614-.404.505-.91 1.01-1.516 1.312-.606.303-1.415.504-2.324.504-.405 0-.708 0-1.112-.1-.404-.102-.606-.202-.91-.303l.809-2.825c.91.201 1.516.201 1.92-.102.303-.201.708-.807 1.01-1.614ZM58.717 24.317l-3.335.505c-.202-.707-.505-1.211-.91-1.615-.404-.404-1.01-.605-1.819-.605-1.111 0-1.819.403-2.425 1.21-.505.808-.809 1.817-.809 3.128 0 1.312.304 2.422.81 3.23.504.807 1.313 1.21 2.425 1.21.707 0 1.313-.202 1.718-.605.404-.404.808-.908.91-1.514l3.334.505c-.303 1.312-1.01 2.422-2.021 3.33a6.68 6.68 0 0 1-4.042 1.311c-1.415 0-2.527-.302-3.538-.908-1.01-.605-1.718-1.513-2.324-2.623a9.284 9.284 0 0 1-.809-3.835c0-1.412.303-2.724.809-3.834.505-1.11 1.314-1.917 2.324-2.523 1.01-.605 2.123-.908 3.538-.908 1.617 0 2.93.404 4.042 1.312 1.213.606 1.92 1.816 2.122 3.229ZM68.118 34.004v-1.615h-.203c-.202.404-.606.908-1.213 1.312-.606.404-1.414.605-2.425.605-.809 0-1.617-.201-2.325-.504-.707-.303-1.212-.807-1.718-1.413-.404-.605-.606-1.412-.606-2.32 0-1.413.505-2.422 1.516-3.128 1.01-.707 2.425-1.11 4.245-1.312.404 0 .808-.101 1.212-.101.404 0 .708-.101 1.01-.202.203-.1.405-.303.405-.505v-.201c0-.606-.202-1.11-.606-1.514a2.484 2.484 0 0 0-1.516-.504c-.708 0-1.213.201-1.718.504-.506.303-.708.706-.91 1.211l-3.335-.505c.303-1.311 1.01-2.32 2.122-3.027 1.113-.706 2.426-1.009 3.942-1.009.91 0 1.82.202 2.729.505.808.303 1.516.807 2.122 1.513.505.707.808 1.716.808 2.826v9.585h-3.536v-.201Zm-.203-4.743v-1.917c-.1.101-.303.202-.505.303-.303.1-.607.1-.91.202-.303.1-.606.1-.808.1-.809.101-1.516.303-1.92.606-.404.303-.606.807-.606 1.413 0 .504.202.908.504 1.21.405.303.81.404 1.415.404.809 0 1.415-.202 2.022-.706.606-.505.808-1.01.808-1.615ZM82.569 20.08l-1.112 3.128c-.101-.101-.303-.101-.505-.202-.203-.101-.506-.101-.81-.101-.606 0-1.11.1-1.515.403-.404.303-.808.707-1.01 1.11-.203.505-.405 1.01-.405 1.514v8.072h-3.638V19.777h3.537v2.018h.202c.202-.706.606-1.312 1.213-1.614a3.647 3.647 0 0 1 2.02-.606c.405 0 .709 0 1.113.101s.707.202.91.404ZM95 34.004h-3.537v-1.715h-.202c-.202.504-.606.908-1.213 1.311-.606.404-1.314.606-2.325.606a5.18 5.18 0 0 1-2.83-.808 6.398 6.398 0 0 1-2.02-2.522c-.506-1.11-.708-2.422-.708-4.036 0-1.615.303-2.926.808-4.036.506-1.11 1.213-1.917 2.022-2.422.91-.505 1.819-.807 2.83-.807.909 0 1.718.202 2.324.605.607.404 1.01.808 1.213 1.312h.1v-6.458H95v18.97Zm-9.5-7.164c0 1.312.303 2.422.809 3.229.505.807 1.313 1.21 2.425 1.21.708 0 1.314-.201 1.82-.605.404-.404.808-.908 1.01-1.614a7.859 7.859 0 0 0 .303-2.22c0-1.211-.202-2.321-.707-3.128-.505-.807-1.213-1.211-2.325-1.211s-1.819.404-2.425 1.21c-.606.808-.91 1.817-.91 3.129Z" />
|
||||
<path d="m15.564 34.005-4.043-5.853-1.212 1.413v4.44H6.67V13.52h3.639v11.503h.202l4.447-5.247h4.244l-5.255 6.055 5.559 8.173h-3.942Z" />
|
||||
<path d="M8.388 16.85C3.74 16.85 0 13.017 0 8.376A8.361 8.361 0 0 1 8.388 0a8.361 8.361 0 0 1 8.389 8.375c.1 4.642-3.74 8.476-8.389 8.476Zm0-13.52c-2.83 0-5.053 2.32-5.053 5.045 0 2.825 2.325 5.045 5.053 5.045 2.83 0 5.053-2.32 5.053-5.045.101-2.724-2.223-5.045-5.053-5.045Z" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h95v40H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ const NavBarMobile = () => {
|
||||
<motion.nav
|
||||
key="nav-bar-mobile"
|
||||
className={cx([
|
||||
'fixed inset-0 z-40 block w-full transition-all lg:hidden',
|
||||
'fixed inset-0 z-[60] block w-full transition-all lg:hidden',
|
||||
])}
|
||||
animate={{
|
||||
height: isOpen ? '100%' : '80px',
|
||||
@@ -109,7 +109,6 @@ const NavBarMobile = () => {
|
||||
key={isOpen ? 'menu-open' : 'menu-closed'}
|
||||
className="rounded-12 border border-white-12 bg-white-8 text-white-95 transition-colors hover:bg-white-12"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
aria-label="Menu"
|
||||
>
|
||||
<MenuIcon isOpen={isOpen} />
|
||||
</button>
|
||||
@@ -123,7 +122,7 @@ const NavBarMobile = () => {
|
||||
animate={{ opacity: 1, height: 'calc(100% - 80px)' }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
||||
className="fixed inset-x-0 top-20 z-30 overflow-hidden"
|
||||
className="fixed inset-x-0 top-20 z-50 overflow-hidden"
|
||||
>
|
||||
<motion.div
|
||||
key="menu-content"
|
||||
|
||||
@@ -45,18 +45,10 @@ const MenuIcon = ({ isOpen }: { isOpen: boolean }) => (
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
initial={{
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: isOpen ? 0 : 1,
|
||||
x: isOpen ? 20 : 0,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 1,
|
||||
x: 20,
|
||||
}}
|
||||
transition={{ duration: 0.3, ease: 'easeInOut', delay: 0.1 }}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ExternalIcon } from '@status-im/icons/20'
|
||||
import { Routes } from '~/config/routes'
|
||||
import { Link } from '~components/link'
|
||||
import { External } from '~icons'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
@@ -24,7 +24,7 @@ const Section = (props: Props) => {
|
||||
<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 />}
|
||||
{external && <External />}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Logo } from './logo'
|
||||
const NAV_BAR_HEIGHT = 92
|
||||
|
||||
const internalLinkStyles = cva({
|
||||
base: 'rounded-12 border border-[transparent] px-[14px] py-[7px] transition-colors hover:bg-white-8',
|
||||
base: 'rounded-12 border border-[transparent] px-[14px] py-[7px] transition-colors hover:border-white-8 hover:bg-white-8',
|
||||
variants: {
|
||||
isActive: {
|
||||
true: 'bg-white-12',
|
||||
@@ -81,7 +81,6 @@ const NavBar = () => {
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<BuyKeycardDialog>
|
||||
<Button variant={variant}>Buy Keycard</Button>
|
||||
</BuyKeycardDialog>
|
||||
|
||||