Hardwar page work

This commit is contained in:
Sacha Weatherstone 2022-04-09 18:54:54 +10:00
parent ebaa8951ff
commit f89b477cbb
23 changed files with 664 additions and 206 deletions

View file

@ -15,11 +15,13 @@
"dependencies": { "dependencies": {
"@algolia/client-search": "^4.13.0", "@algolia/client-search": "^4.13.0",
"@docusaurus/core": "^2.0.0-beta.18", "@docusaurus/core": "^2.0.0-beta.18",
"@docusaurus/plugin-content-docs": "^2.0.0-beta.18",
"@docusaurus/preset-classic": "^2.0.0-beta.18", "@docusaurus/preset-classic": "^2.0.0-beta.18",
"@headlessui/react": "^1.5.0", "@headlessui/react": "^1.5.0",
"@mdx-js/react": "^1.6.22", "@mdx-js/react": "^1.6.22",
"autoprefixer": "^10.4.4", "autoprefixer": "^10.4.4",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"framer-motion": "^6.2.8",
"postcss": "^8.4.12", "postcss": "^8.4.12",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
@ -29,6 +31,7 @@
"swr": "^1.2.2", "swr": "^1.2.2",
"tailwindcss": "^3.0.23", "tailwindcss": "^3.0.23",
"url-search-params-polyfill": "^8.1.1", "url-search-params-polyfill": "^8.1.1",
"use-breakpoint": "^3.0.2",
"victory": "^36.3.1" "victory": "^36.3.1"
}, },
"devDependencies": { "devDependencies": {

View file

@ -4,6 +4,7 @@ specifiers:
'@algolia/client-search': ^4.13.0 '@algolia/client-search': ^4.13.0
'@docusaurus/core': ^2.0.0-beta.18 '@docusaurus/core': ^2.0.0-beta.18
'@docusaurus/module-type-aliases': ^2.0.0-beta.18 '@docusaurus/module-type-aliases': ^2.0.0-beta.18
'@docusaurus/plugin-content-docs': ^2.0.0-beta.18
'@docusaurus/preset-classic': ^2.0.0-beta.18 '@docusaurus/preset-classic': ^2.0.0-beta.18
'@headlessui/react': ^1.5.0 '@headlessui/react': ^1.5.0
'@mdx-js/react': ^1.6.22 '@mdx-js/react': ^1.6.22
@ -16,6 +17,7 @@ specifiers:
'@types/w3c-web-serial': ^1.0.2 '@types/w3c-web-serial': ^1.0.2
autoprefixer: ^10.4.4 autoprefixer: ^10.4.4
dotenv: ^16.0.0 dotenv: ^16.0.0
framer-motion: ^6.2.8
postcss: ^8.4.12 postcss: ^8.4.12
prettier: ^2.6.1 prettier: ^2.6.1
react: ^17.0.2 react: ^17.0.2
@ -27,16 +29,19 @@ specifiers:
tailwindcss: ^3.0.23 tailwindcss: ^3.0.23
typescript: ^4.6.3 typescript: ^4.6.3
url-search-params-polyfill: ^8.1.1 url-search-params-polyfill: ^8.1.1
use-breakpoint: ^3.0.2
victory: ^36.3.1 victory: ^36.3.1
dependencies: dependencies:
'@algolia/client-search': 4.13.0 '@algolia/client-search': 4.13.0
'@docusaurus/core': 2.0.0-beta.18_28e7016540b0a32b5f8d7be755522ab7 '@docusaurus/core': 2.0.0-beta.18_28e7016540b0a32b5f8d7be755522ab7
'@docusaurus/plugin-content-docs': 2.0.0-beta.18_28e7016540b0a32b5f8d7be755522ab7
'@docusaurus/preset-classic': 2.0.0-beta.18_cd2ecee38e568e94b4890eebc445da58 '@docusaurus/preset-classic': 2.0.0-beta.18_cd2ecee38e568e94b4890eebc445da58
'@headlessui/react': 1.5.0_react-dom@17.0.2+react@17.0.2 '@headlessui/react': 1.5.0_react-dom@17.0.2+react@17.0.2
'@mdx-js/react': 1.6.22_react@17.0.2 '@mdx-js/react': 1.6.22_react@17.0.2
autoprefixer: 10.4.4_postcss@8.4.12 autoprefixer: 10.4.4_postcss@8.4.12
dotenv: 16.0.0 dotenv: 16.0.0
framer-motion: 6.2.8_react-dom@17.0.2+react@17.0.2
postcss: 8.4.12 postcss: 8.4.12
react: 17.0.2 react: 17.0.2
react-dom: 17.0.2_react@17.0.2 react-dom: 17.0.2_react@17.0.2
@ -46,6 +51,7 @@ dependencies:
swr: 1.2.2_react@17.0.2 swr: 1.2.2_react@17.0.2
tailwindcss: 3.0.23_autoprefixer@10.4.4 tailwindcss: 3.0.23_autoprefixer@10.4.4
url-search-params-polyfill: 8.1.1 url-search-params-polyfill: 8.1.1
use-breakpoint: 3.0.2_react-dom@17.0.2+react@17.0.2
victory: 36.3.1_react@17.0.2 victory: 36.3.1_react@17.0.2
devDependencies: devDependencies:
@ -638,7 +644,7 @@ packages:
'@babel/core': ^7.0.0-0 '@babel/core': ^7.0.0-0
dependencies: dependencies:
'@babel/core': 7.12.9 '@babel/core': 7.12.9
'@babel/helper-plugin-utils': 7.10.4 '@babel/helper-plugin-utils': 7.16.7
'@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.12.9 '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.12.9
'@babel/plugin-transform-parameters': 7.16.7_@babel+core@7.12.9 '@babel/plugin-transform-parameters': 7.16.7_@babel+core@7.12.9
dev: false dev: false
@ -2182,6 +2188,19 @@ packages:
- webpack-cli - webpack-cli
dev: false dev: false
/@emotion/is-prop-valid/0.8.8:
resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==}
requiresBuild: true
dependencies:
'@emotion/memoize': 0.7.4
dev: false
optional: true
/@emotion/memoize/0.7.4:
resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==}
dev: false
optional: true
/@eslint/eslintrc/1.2.1: /@eslint/eslintrc/1.2.1:
resolution: {integrity: sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==} resolution: {integrity: sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@ -5182,6 +5201,29 @@ packages:
resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
dev: false dev: false
/framer-motion/6.2.8_react-dom@17.0.2+react@17.0.2:
resolution: {integrity: sha512-4PtBWFJ6NqR350zYVt9AsFDtISTqsdqna79FvSYPfYDXuuqFmiKtZdkTnYPslnsOMedTW0pEvaQ7eqjD+sA+HA==}
peerDependencies:
react: '>=16.8 || ^17.0.0 || ^18.0.0'
react-dom: '>=16.8 || ^17.0.0 || ^18.0.0'
dependencies:
framesync: 6.0.1
hey-listen: 1.0.8
popmotion: 11.0.3
react: 17.0.2
react-dom: 17.0.2_react@17.0.2
style-value-types: 5.0.0
tslib: 2.3.1
optionalDependencies:
'@emotion/is-prop-valid': 0.8.8
dev: false
/framesync/6.0.1:
resolution: {integrity: sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==}
dependencies:
tslib: 2.3.1
dev: false
/fresh/0.5.2: /fresh/0.5.2:
resolution: {integrity: sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=} resolution: {integrity: sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@ -5519,6 +5561,10 @@ packages:
hasBin: true hasBin: true
dev: false dev: false
/hey-listen/1.0.8:
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
dev: false
/history/4.10.1: /history/4.10.1:
resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==} resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==}
dependencies: dependencies:
@ -6976,6 +7022,15 @@ packages:
find-up: 3.0.0 find-up: 3.0.0
dev: false dev: false
/popmotion/11.0.3:
resolution: {integrity: sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==}
dependencies:
framesync: 6.0.1
hey-listen: 1.0.8
style-value-types: 5.0.0
tslib: 2.3.1
dev: false
/portfinder/1.0.28: /portfinder/1.0.28:
resolution: {integrity: sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==} resolution: {integrity: sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==}
engines: {node: '>= 0.12.0'} engines: {node: '>= 0.12.0'}
@ -8567,6 +8622,13 @@ packages:
inline-style-parser: 0.1.1 inline-style-parser: 0.1.1
dev: false dev: false
/style-value-types/5.0.0:
resolution: {integrity: sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==}
dependencies:
hey-listen: 1.0.8
tslib: 2.3.1
dev: false
/stylehacks/5.1.0_postcss@8.4.12: /stylehacks/5.1.0_postcss@8.4.12:
resolution: {integrity: sha512-SzLmvHQTrIWfSgljkQCw2++C9+Ne91d/6Sp92I8c5uHTcy/PgeHamwITIbBW9wnFTY/3ZfSXR9HIL6Ikqmcu6Q==} resolution: {integrity: sha512-SzLmvHQTrIWfSgljkQCw2++C9+Ne91d/6Sp92I8c5uHTcy/PgeHamwITIbBW9wnFTY/3ZfSXR9HIL6Ikqmcu6Q==}
engines: {node: ^10 || ^12 || >=14.0} engines: {node: ^10 || ^12 || >=14.0}
@ -9005,6 +9067,17 @@ packages:
resolution: {integrity: sha512-KmkCs6SjE6t4ihrfW9JelAPQIIIFbJweaaSLTh/4AO+c58JlDcb+GbdPt8yr5lRcFg4rPswRFRRhBGpWwh0K/Q==} resolution: {integrity: sha512-KmkCs6SjE6t4ihrfW9JelAPQIIIFbJweaaSLTh/4AO+c58JlDcb+GbdPt8yr5lRcFg4rPswRFRRhBGpWwh0K/Q==}
dev: false dev: false
/use-breakpoint/3.0.2_react-dom@17.0.2+react@17.0.2:
resolution: {integrity: sha512-O7qfp6QOOQI3CqLnJg6e5TgV2lSuLv3jQT48RAtScYTPbYjuGogPDQUS+Bz/MC0ZCYdZge262rKBi+jgZgCSHw==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
peerDependencies:
react: '>=16.8'
react-dom: '>=16.8'
dependencies:
react: 17.0.2
react-dom: 17.0.2_react@17.0.2
dev: false
/use-composed-ref/1.2.1_react@17.0.2: /use-composed-ref/1.2.1_react@17.0.2:
resolution: {integrity: sha512-6+X1FLlIcjvFMAeAD/hcxDT8tmyrWnbSPMU0EnxQuDLIxokuFzWliXBiYZuGIx+mrAMLBw0WFfCkaPw8ebzAhw==} resolution: {integrity: sha512-6+X1FLlIcjvFMAeAD/hcxDT8tmyrWnbSPMU0EnxQuDLIxokuFzWliXBiYZuGIx+mrAMLBw0WFfCkaPw8ebzAhw==}
peerDependencies: peerDependencies:

16
src/components/Button.tsx Normal file
View file

@ -0,0 +1,16 @@
import React from 'react';
import { HTMLMotionProps, motion } from 'framer-motion';
export const Button = ({ children, ...props }: HTMLMotionProps<'div'>) => {
return (
<motion.div
{...props}
whileHover={{ scale: 1.1, backgroundColor: 'var(--tertiary)' }}
whileTap={{ scale: 1.0 }}
className="m-auto flex cursor-pointer rounded-full bg-secondary p-3 shadow-md"
>
<div className="m-auto">{children}</div>
</motion.div>
);
};

View file

@ -1,6 +1,8 @@
import React, { Fragment } from 'react'; import React from 'react';
import { Dialog, Transition } from '@headlessui/react'; import { AnimatePresence, motion } from 'framer-motion';
import { Dialog } from '@headlessui/react';
export interface ModalProps { export interface ModalProps {
open: boolean; open: boolean;
@ -10,45 +12,38 @@ export interface ModalProps {
export const Modal = ({ open, onClose, children }: ModalProps): JSX.Element => { export const Modal = ({ open, onClose, children }: ModalProps): JSX.Element => {
return ( return (
<Transition appear show={open} as={Fragment}> <AnimatePresence initial={false} exitBeforeEnter={true}>
<Dialog <Dialog
as="div" as="div"
className="fixed inset-0 z-10 overflow-y-auto" className="fixed inset-0 z-10 overflow-y-auto"
open={open}
onClose={onClose} onClose={onClose}
> >
<div className="min-h-screen px-4 text-center"> <div className="min-h-screen px-4 text-center">
<Transition.Child <motion.div
as={Fragment} initial={{ opacity: 0 }}
enter="ease-out duration-100" animate={{ opacity: 1 }}
enterFrom="opacity-0" exit={{ opacity: 0 }}
enterTo="opacity-100"
leave="ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
> >
<Dialog.Overlay className="fixed inset-0 backdrop-blur-md" /> <Dialog.Overlay className="fixed inset-0 backdrop-blur-md" />
</Transition.Child> </motion.div>
{/* This element is to trick the browser into centering the modal contents. */}
<span <span
className="inline-block h-screen align-middle" className="inline-block h-screen align-middle"
aria-hidden="true" aria-hidden="true"
> >
&#8203; &#8203;
</span> </span>
<Transition.Child <div className="inline-block w-full transform text-left align-middle transition-all 2xl:max-w-7xl">
as={Fragment} <div className="group relative">
enter="ease-out duration-100" <div className="animate-tilt absolute -inset-0.5 rotate-2 rounded-lg bg-accent shadow-md transition duration-1000 group-hover:opacity-100 group-hover:duration-200"></div>
enterFrom="opacity-0 scale-95" <div className="relative flex aspect-[3/2] flex-col overflow-hidden rounded-2xl bg-base shadow-md md:aspect-[2/1] md:flex-row md:bg-primary">
enterTo="opacity-100 scale-100" {children}
leave="ease-in duration-200" </div>
leaveFrom="opacity-100 scale-100" </div>
leaveTo="opacity-0 scale-95" </div>
>
{children}
</Transition.Child>
</div> </div>
</Dialog> </Dialog>
</Transition> </AnimatePresence>
); );
}; };

View file

@ -1,67 +1,58 @@
import React, { useState } from 'react'; import React from 'react';
import { IDevice, Stability } from '@site/src/data/device'; import { IDevice, Stability } from '@site/src/data/device';
import { HardwareModal } from './HardwareModal';
export interface HardwareCardProps { export interface HardwareCardProps {
device: IDevice; device: IDevice;
setDevice: () => void;
} }
export const HardwareCard = ({ device }: HardwareCardProps): JSX.Element => { export const HardwareCard = ({
const [open, setOpen] = useState(false); device,
setDevice,
}: HardwareCardProps): JSX.Element => {
return ( return (
<> <li
<li className="group relative"
className="group relative" onClick={() => {
onClick={() => { setDevice();
setOpen(true); }}
}} >
> <div className="overflow-hidden rounded-lg">
<div className="overflow-hidden rounded-lg"> <div
<div className={`flex aspect-[4/3] overflow-hidden ${device.misc.Gradient}`}
className={`flex aspect-[4/3] overflow-hidden ${device.misc.Gradient}`} >
> <img
<img src={device.images.Front}
src={device.misc.ImagePath} alt=""
alt="" className="pointer-events-none m-auto max-h-full max-w-full object-cover p-2 group-hover:opacity-75"
className="pointer-events-none m-auto max-h-full max-w-full object-cover p-2 group-hover:opacity-75" />
</div>
<button type="button" className="absolute inset-0 focus:outline-none">
<span className="sr-only">View details for {device.name}</span>
</button>
</div>
<div className="flex">
<div>
<p className="pointer-events-none mt-2 block truncate text-sm font-medium text-primaryInv">
{device.name}
</p>
<p className="pointer-events-none flex gap-1 text-sm font-medium text-mute">
<div
className={`my-auto h-3 w-3 rounded-full ${
device.misc.Stability === Stability.Broken
? 'bg-red-500'
: device.misc.Stability === Stability.Unstable
? 'bg-orange-500'
: device.misc.Stability === Stability.Semi
? 'bg-cyan-500'
: 'bg-green-500'
}`}
/> />
</div> <div className="my-auto">{Stability[device.misc.Stability]}</div>
<button type="button" className="absolute inset-0 focus:outline-none"> </p>
<span className="sr-only">View details for {device.name}</span>
</button>
</div> </div>
<div className="flex"> </div>
<div> </li>
<p className="pointer-events-none mt-2 block truncate text-sm font-medium text-primaryInv">
{device.name}
</p>
<p className="pointer-events-none flex gap-1 text-sm font-medium text-mute">
<div
className={`my-auto h-3 w-3 rounded-full ${
device.misc.Stability === Stability.Broken
? 'bg-red-500'
: device.misc.Stability === Stability.Unstable
? 'bg-orange-500'
: device.misc.Stability === Stability.Semi
? 'bg-cyan-500'
: 'bg-green-500'
}`}
/>
<div className="my-auto">{Stability[device.misc.Stability]}</div>
</p>
</div>
</div>
</li>
<HardwareModal
open={open}
close={() => {
setOpen(false);
}}
device={device}
/>
</>
); );
}; };

View file

@ -1,10 +1,14 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { FiBluetooth, FiChevronRight, FiWifi, FiX } from 'react-icons/fi'; import { FiBluetooth, FiChevronRight, FiWifi, FiX } from 'react-icons/fi';
import { useBreakpoint } from 'use-breakpoint';
import { Tab, Transition } from '@headlessui/react'; import { Tab } from '@headlessui/react';
import type { IDevice } from '@site/src/data/device'; import type { IDevice } from '@site/src/data/device';
import { Button } from '../../components/Button';
import { BREAKPOINTS } from '../../utils/breakpoints';
import { Modal } from '../Modal'; import { Modal } from '../Modal';
import { Badge } from './Badge'; import { Badge } from './Badge';
import { CardTab } from './CardTab'; import { CardTab } from './CardTab';
@ -24,102 +28,120 @@ export const HardwareModal = ({
open, open,
close, close,
}: HardwareModal): JSX.Element => { }: HardwareModal): JSX.Element => {
const colors = ['#428517', '#77D200', '#D6D305', '#EC8E19', '#C92B05'];
const [hideDetails, setHideDetails] = useState(false); const [hideDetails, setHideDetails] = useState(false);
const { breakpoint } = useBreakpoint(BREAKPOINTS);
return ( return (
<Modal open={open} onClose={close}> <Modal open={open} onClose={close}>
<div className="inline-block w-full max-w-md transform overflow-hidden rounded-2xl bg-base text-left align-middle transition-all md:max-w-2xl md:bg-primary lg:max-w-4xl xl:max-w-6xl"> <div className="absolute right-0 z-20 m-2 md:flex">
<div className="flex aspect-[3/2] flex-col md:aspect-[2/1] md:flex-row"> <Button onClick={close}>
<div <FiX />
className={`relative flex h-full rounded-t-2xl md:rounded-l-2xl md:rounded-tr-none ${ </Button>
device.misc.Gradient </div>
} ${hideDetails ? 'w-full' : ''}`} <motion.div
> layout
<img animate={hideDetails ? 'hidden' : 'visible'}
src={device.misc.ImagePath} variants={{
alt="" hidden: breakpoint === 'sm' ? { height: '100%' } : { width: '100%' },
className="pointer-events-none m-auto object-cover p-2 group-hover:opacity-75" visible: breakpoint === 'sm' ? { height: '25%' } : { width: '20%' },
/> }}
<div className="absolute -bottom-4 flex w-full md:bottom-auto md:-right-4 md:h-full md:w-auto "> transition={{
<div type: 'just',
onClick={() => { }}
setHideDetails(!hideDetails); className="absolute inset-0 flex flex-col md:h-full md:flex-row"
}} >
className="m-auto flex cursor-pointer rounded-full bg-secondary p-2 shadow-md hover:bg-tertiary" <motion.div
> layout
<FiChevronRight className={`relative z-10 flex h-full w-full rounded-t-2xl md:rounded-l-2xl md:rounded-tr-none ${device.misc.Gradient}`}
className={`m-auto ${ >
hideDetails <motion.img
? 'rotate-90 md:rotate-180' layout
: '-rotate-90 md:rotate-0' src={device.images.Front}
}`} alt=""
/> className="pointer-events-none m-auto max-h-full max-w-full object-cover p-2"
</div> />
</div> <div className="absolute -bottom-5 z-20 flex w-full md:bottom-auto md:-right-5 md:h-full md:w-auto">
{!hideDetails && ( <Button
<div className="absolute -bottom-3 right-0 m-auto mr-2 ml-auto flex gap-2 md:bottom-2 md:mr-14 md:mt-2"> animate={hideDetails ? 'hidden' : 'visible'}
{device.features.BLE && ( variants={{
<Badge hidden: breakpoint === 'sm' ? { rotate: -90 } : { rotate: 180 },
name="Bluetooth" visible: breakpoint === 'sm' ? { rotate: 90 } : { rotate: 0 },
color="bg-blue-500" }}
icon={<FiBluetooth />} onClick={() => {
/> setHideDetails(!hideDetails);
)} }}
{device.features.WiFi && (
<Badge name="WiFi" color="bg-orange-500" icon={<FiWifi />} />
)}
</div>
)}
</div>
<div
className="absolute right-0 mr-2 flex cursor-pointer rounded-b-full bg-secondary p-3 shadow-md hover:bg-tertiary md:mt-2 md:rounded-full"
onClick={close}
>
<FiX className="m-auto" />
</div>
<div
className={`transition-[all] duration-100 ease-linear ${
hideDetails ? 'h-7 bg-base md:h-auto md:w-7' : 'w-full'
}`}
>
<Transition
appear
as={'div'}
className="flex h-full flex-col"
show={!hideDetails}
enter="ease-out duration-100 delay-100"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-100 delay-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
> >
<> <FiChevronRight />
<div className="flex shadow-md md:pb-2"> </Button>
<VariantSelectButton options={device.variants} />
</div>
<div className="flex h-full bg-base p-2 md:p-4">
<Tab.Group
as="div"
className="flex-grow rounded-2xl bg-primary p-2"
>
<Tab.List className="flex gap-2">
<CardTab title="Info" />
<CardTab title="Power" />
<CardTab title="Pinout" />
</Tab.List>
<Tab.Panels as="div" className="">
<InfoTab device={device} />
<PowerTab device={device} />
<PinoutTab device={device} />
</Tab.Panels>
</Tab.Group>
</div>
</>
</Transition>
</div> </div>
<AnimatePresence>
{!hideDetails && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={`absolute -bottom-5 z-20 flex md:mt-0 md:hidden md:pb-2 ${
hideDetails ? 'opacity-0' : 'opacity-100'
}`}
>
<VariantSelectButton options={device.variants} />
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute -bottom-3 right-0 m-auto mr-2 ml-auto flex gap-2 md:bottom-2 md:mr-14 md:mt-2"
>
{device.features.BLE && (
<Badge
name="Bluetooth"
color="bg-blue-500"
icon={<FiBluetooth />}
/>
)}
{device.features.WiFi && (
<Badge
name="WiFi"
color="bg-orange-500"
icon={<FiWifi />}
/>
)}
</motion.div>
</>
)}
</AnimatePresence>
</motion.div>
<div
className={`h-7 bg-base opacity-0 md:h-auto md:w-7 ${
hideDetails ? 'flex' : 'hidden'
}`}
/>
</motion.div>
<div className="mt-[25%] flex h-full flex-col md:ml-[20%] md:mt-0 md:w-4/5">
<div className="z-0 hidden pb-2 md:flex">
<VariantSelectButton options={device.variants} />
</div>
<div
className={`mt-1 flex flex-grow rounded-2xl bg-base p-2 shadow-inner transition-opacity duration-100 ease-linear md:mt-0 md:rounded-l-none md:rounded-r-2xl md:p-4 ${
hideDetails ? 'opacity-0' : 'opacity-100'
}`}
>
<Tab.Group
as="div"
className="flex flex-grow flex-col rounded-2xl bg-primary p-2"
>
<Tab.List className="flex gap-2">
<CardTab title="Info" />
<CardTab title="Power" />
<CardTab title="Pinout" />
</Tab.List>
<Tab.Panels as="div" className="flex-grow overflow-y-auto">
<InfoTab device={device} />
<PowerTab device={device} />
<PinoutTab device={device} />
</Tab.Panels>
</Tab.Group>
</div> </div>
</div> </div>
</Modal> </Modal>

View file

@ -9,5 +9,40 @@ export interface InfoTabProps {
} }
export const InfoTab = ({ device }: InfoTabProps): JSX.Element => { export const InfoTab = ({ device }: InfoTabProps): JSX.Element => {
return <Tab.Panel className="h-32">Content 1</Tab.Panel>; return (
<Tab.Panel>
<div className="px-4 py-5 sm:p-0">
<dl className="sm:divide-y sm:divide-gray-200">
<div className="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6">
<dt className="text-sm font-medium text-secondaryInv">
BLE/WiFi Version
</dt>
<dd className="mt-1 flex gap-1 text-sm text-tertiaryInv sm:col-span-2 sm:mt-0">
<span className="rounded-md bg-secondary px-0.5">
{device.specifications.BLEVersion}
</span>
/
<span className="rounded-md bg-secondary px-0.5">
{device.specifications.WiFiVersion}
</span>
</dd>
</div>
<div className="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6">
<dt className="text-sm font-medium text-secondaryInv">
BLE/WiFi Antenna
</dt>
<dd className="mt-1 flex gap-1 text-sm text-tertiaryInv sm:col-span-2 sm:mt-0">
<span className="rounded-md bg-secondary px-0.5">
{device.specifications.BLEAntenna}
</span>
/
<span className="rounded-md bg-secondary px-0.5">
{device.specifications.WiFiAntenna}
</span>
</dd>
</div>
</dl>
</div>
</Tab.Panel>
);
}; };

View file

@ -1,5 +1,6 @@
import React, { Fragment, useState } from 'react'; import React, { Fragment, useState } from 'react';
import { motion } from 'framer-motion';
import { FiCheck } from 'react-icons/fi'; import { FiCheck } from 'react-icons/fi';
import { HiSelector } from 'react-icons/hi'; import { HiSelector } from 'react-icons/hi';
@ -19,15 +20,21 @@ export const VariantSelectButton = ({
<Listbox value={selected} onChange={setSelected}> <Listbox value={selected} onChange={setSelected}>
{({ open }) => ( {({ open }) => (
<> <>
<div className="relative"> <div className="relative select-none">
<Listbox.Button className="relative -mt-5 ml-2 flex w-fit gap-1 rounded-lg bg-secondary p-2 py-2 pl-3 pr-10 text-lg font-medium leading-6 shadow-md hover:bg-tertiary md:mt-2"> <Listbox.Button as={Fragment}>
<span className="block truncate">{selected.name}</span> <motion.button
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> whileHover={{ backgroundColor: 'var(--tertiary)' }}
<HiSelector whileTap={{ scale: 0.99 }}
className="h-5 w-5 text-gray-400" className="relative -mt-5 ml-2 flex w-fit gap-1 rounded-lg bg-secondary p-2 py-2 pl-3 pr-10 text-lg font-medium leading-6 shadow-md md:mt-2"
aria-hidden="true" >
/> <span className="block truncate">{selected.name}</span>
</span> <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<HiSelector
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</motion.button>
</Listbox.Button> </Listbox.Button>
<Transition <Transition
@ -37,7 +44,7 @@ export const VariantSelectButton = ({
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-primary py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"> <Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-primary py-1 shadow-md ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{options.map((variant, index) => ( {options.map((variant, index) => (
<Listbox.Option <Listbox.Option
key={index} key={index}

View file

@ -19,8 +19,9 @@ export const SocialCard = ({
> >
{children} {children}
<a <a
className="absolute top-0 left-0 right-0 bottom-0 hidden rounded-xl border border-accent bg-secondaryDark bg-opacity-95 text-2xl shadow-xl group-hover:flex" className="absolute top-0 left-0 right-0 bottom-0 hidden rounded-xl border border-accent bg-secondary bg-opacity-95 text-2xl shadow-xl group-hover:flex"
href={link} href={link}
rel="noreferrer"
target="_blank" target="_blank"
> >
<FiExternalLink className="m-auto" /> <FiExternalLink className="m-auto" />

View file

@ -48,9 +48,12 @@ export interface IDevice {
misc: { misc: {
SuggestedUse: UseCase[]; SuggestedUse: UseCase[];
Stability: Stability; Stability: Stability;
ImagePath: string;
Gradient: string; Gradient: string;
}; };
images: {
Front: string;
Back: string;
};
features: { features: {
BLE: boolean; BLE: boolean;
WiFi: boolean; WiFi: boolean;

View file

@ -5,8 +5,11 @@ export const heltec: IDevice = {
misc: { misc: {
Stability: Stability.Unstable, Stability: Stability.Unstable,
SuggestedUse: [UseCase.Portable], SuggestedUse: [UseCase.Portable],
ImagePath: '/img/hardware/heltec-v2.png', Gradient: 'bg-gradient-to-r from-pink-300 via-purple-300 to-indigo-400',
Gradient: 'from-pink-300 via-purple-300 to-indigo-400', },
images: {
Front: '/img/hardware/heltec-v2.png',
Back: '',
}, },
features: { features: {
BLE: true, BLE: true,

View file

@ -5,9 +5,12 @@ export const hydra: IDevice = {
misc: { misc: {
Stability: Stability.Stable, Stability: Stability.Stable,
SuggestedUse: [UseCase.Portable], SuggestedUse: [UseCase.Portable],
ImagePath: '/img/hardware/Hydra-PCB.2.1.svg',
Gradient: 'bg-gradient-to-r from-indigo-200 via-red-200 to-yellow-100', Gradient: 'bg-gradient-to-r from-indigo-200 via-red-200 to-yellow-100',
}, },
images: {
Front: '/img/hardware/Hydra-PCB.2.1.svg',
Back: '',
},
features: { features: {
BLE: true, BLE: true,
WiFi: true, WiFi: true,

View file

@ -0,0 +1,66 @@
import { IDevice, Stability, UseCase } from '../device';
export const nano_g1: IDevice = {
name: 'Nano G1',
misc: {
Stability: Stability.Unstable,
SuggestedUse: [UseCase.Portable],
Gradient: 'bg-gradient-to-r from-green-200 to-green-500',
},
images: {
Front: '/img/hardware/nano_g1_front.svg',
Back: '/img/hardware/nano_g1_back.svg',
},
features: {
BLE: true,
WiFi: true,
Modules: [
'cannedMessage',
'externalNotification',
'rangeTest',
'rotaryEncoder',
'storeAndForward',
'telemetry',
],
},
specifications: {
BLEVersion: '4.2',
BLEAntenna: 'Integrated',
WiFiVersion: '2.4GHz 802.11 b/g/n WPA/WPA2/WPA2-Enterprise/SPS',
WiFiAntenna: 'Integrated',
Chipset: 'ESP32',
Driver: 'CH9102',
GNSS: 'NEO-6M',
FlashSize: 4,
Frequencies: [433, 868, 915, 923],
LoRa: 'SX1262',
PSRAM: 8,
RAM: undefined,
},
variants: [
{
name: 'TBeam 0.7',
misc: {
Stability: Stability.Unstable,
},
specifications: {
Driver: 'CP210X',
GNSS: 'NEO-6M',
Frequencies: [868, 915],
},
},
{
name: 'TBeam 1.0',
specifications: {
Frequencies: [868, 915],
},
},
{
name: 'TBeam 1.1',
specifications: {
Driver: 'CP210X',
GNSS: 'NEO-6M',
},
},
],
};

View file

@ -0,0 +1,66 @@
import { IDevice, Stability, UseCase } from '../device';
export const rak19001: IDevice = {
name: 'WisBlock 19001',
misc: {
Stability: Stability.Stable,
SuggestedUse: [UseCase.Portable],
Gradient: 'bg-gradient-to-r from-indigo-300 to-purple-400',
},
images: {
Front: '/img/hardware/rak/RAK19001.png',
Back: '',
},
features: {
BLE: true,
WiFi: true,
Modules: [
'cannedMessage',
'externalNotification',
'rangeTest',
'rotaryEncoder',
'storeAndForward',
'telemetry',
],
},
specifications: {
BLEVersion: '4.2',
BLEAntenna: 'Integrated',
WiFiVersion: '2.4GHz 802.11 b/g/n WPA/WPA2/WPA2-Enterprise/SPS',
WiFiAntenna: 'Integrated',
Chipset: 'ESP32',
Driver: 'CH9102',
GNSS: 'NEO-6M',
FlashSize: 4,
Frequencies: [433, 868, 915, 923],
LoRa: 'SX1262',
PSRAM: 8,
RAM: undefined,
},
variants: [
{
name: 'TBeam 0.7',
misc: {
Stability: Stability.Unstable,
},
specifications: {
Driver: 'CP210X',
GNSS: 'NEO-6M',
Frequencies: [868, 915],
},
},
{
name: 'TBeam 1.0',
specifications: {
Frequencies: [868, 915],
},
},
{
name: 'TBeam 1.1',
specifications: {
Driver: 'CP210X',
GNSS: 'NEO-6M',
},
},
],
};

View file

@ -5,9 +5,12 @@ export const rak19003: IDevice = {
misc: { misc: {
Stability: Stability.Stable, Stability: Stability.Stable,
SuggestedUse: [UseCase.Portable], SuggestedUse: [UseCase.Portable],
ImagePath: '/img/hardware/rak/RAK19003.png',
Gradient: 'bg-gradient-to-b from-orange-500 to-yellow-300', Gradient: 'bg-gradient-to-b from-orange-500 to-yellow-300',
}, },
images: {
Front: '/img/hardware/rak/RAK19003.png',
Back: '',
},
features: { features: {
BLE: true, BLE: true,
WiFi: true, WiFi: true,

View file

@ -5,9 +5,12 @@ export const tbeam: IDevice = {
misc: { misc: {
Stability: Stability.Stable, Stability: Stability.Stable,
SuggestedUse: [UseCase.Portable], SuggestedUse: [UseCase.Portable],
ImagePath: '/img/hardware/tbeam-v1.1.svg',
Gradient: 'bg-gradient-to-r from-pink-500 via-red-500 to-yellow-500', Gradient: 'bg-gradient-to-r from-pink-500 via-red-500 to-yellow-500',
}, },
images: {
Front: '/img/hardware/tbeam-v1.1.svg',
Back: '',
},
features: { features: {
BLE: true, BLE: true,
WiFi: true, WiFi: true,

View file

@ -5,8 +5,11 @@ export const techo: IDevice = {
misc: { misc: {
Stability: Stability.Semi, Stability: Stability.Semi,
SuggestedUse: [UseCase.Portable], SuggestedUse: [UseCase.Portable],
ImagePath: '/img/hardware/t-echo-lilygo.jpg', Gradient: 'bg-gradient-to-r from-gray-700 via-gray-900 to-black',
Gradient: 'from-gray-700 via-gray-900 to-black', },
images: {
Front: '/img/hardware/t-echo-lilygo.jpg',
Back: '',
}, },
features: { features: {
BLE: true, BLE: true,

View file

@ -1,28 +1,23 @@
import React from 'react'; import React, { useState } from 'react';
import { FiPlus } from 'react-icons/fi'; import { FiPlus } from 'react-icons/fi';
import { HardwareModal } from '@site/src/components/hardware/HardwareModal';
import { IDevice } from '@site/src/data/device';
import { HardwareCard } from '../../components/hardware/HardwareCard'; import { HardwareCard } from '../../components/hardware/HardwareCard';
import { PageLayout } from '../../components/PageLayout'; import { PageLayout } from '../../components/PageLayout';
import { heltec } from '../../data/devices/heltec'; import { heltec } from '../../data/devices/heltec';
import { hydra } from '../../data/devices/hydra'; import { hydra } from '../../data/devices/hydra';
import { nano_g1 } from '../../data/devices/nano_g1';
import { rak19001 } from '../../data/devices/rak19001';
import { rak19003 } from '../../data/devices/rak19003'; import { rak19003 } from '../../data/devices/rak19003';
import { tbeam } from '../../data/devices/tbeam'; import { tbeam } from '../../data/devices/tbeam';
import { techo } from '../../data/devices/techo'; import { techo } from '../../data/devices/techo';
const Hardware = (): JSX.Element => { const Hardware = (): JSX.Element => {
const hardware = [ const hardware = [tbeam, hydra, rak19003, rak19001, nano_g1, heltec, techo];
tbeam, const [modalData, setModalData] = useState<IDevice>();
hydra,
rak19003,
heltec,
techo,
rak19003,
rak19003,
rak19003,
rak19003,
rak19003,
];
return ( return (
<PageLayout title="Hardware" description="Supported hardware"> <PageLayout title="Hardware" description="Supported hardware">
@ -57,7 +52,13 @@ const Hardware = (): JSX.Element => {
className="grid grid-cols-2 gap-x-2 gap-y-4 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:grid-cols-5 xl:gap-x-4" className="grid grid-cols-2 gap-x-2 gap-y-4 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:grid-cols-5 xl:gap-x-4"
> >
{hardware.map((device, index) => ( {hardware.map((device, index) => (
<HardwareCard key={index} device={device} /> <HardwareCard
key={index}
device={device}
setDevice={(): void => {
setModalData(device);
}}
/>
))} ))}
<li className="group relative"> <li className="group relative">
<a <a
@ -77,6 +78,15 @@ const Hardware = (): JSX.Element => {
</li> </li>
</ul> </ul>
</div> </div>
{modalData && (
<HardwareModal
open={!!modalData}
close={() => {
setModalData(undefined);
}}
device={modalData}
/>
)}
</PageLayout> </PageLayout>
); );
}; };

1
src/utils/breakpoints.ts Normal file
View file

@ -0,0 +1 @@
export const BREAKPOINTS = { sm: 640, md: 768, lg: 1024, xl: 1280 };

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB