Skip to content

Commit

Permalink
scaffold react
Browse files Browse the repository at this point in the history
  • Loading branch information
deodad committed Dec 19, 2023
1 parent 5ec3e60 commit d431c06
Show file tree
Hide file tree
Showing 21 changed files with 1,625 additions and 20 deletions.
18 changes: 18 additions & 0 deletions packages/react/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
],
ignorePatterns: ["dist", ".eslintrc.cjs"],
parser: "@typescript-eslint/parser",
plugins: ["react-refresh"],
rules: {
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
},
};
21 changes: 21 additions & 0 deletions packages/react/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Logs
logs
*.log
yarn-debug.log*
yarn-error.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
1 change: 1 addition & 0 deletions packages/react/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Farcaster Connect React
12 changes: 12 additions & 0 deletions packages/react/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Farcaster Connect</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/demo.tsx"></script>
</body>
</html>
43 changes: 43 additions & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "connect-react",
"version": "0.0.0",
"type": "module",
"main": "./dist/connect-react.umd.cjs",
"module": "./dist/connect-react.js",
"exports": {
".": {
"import": "./dist/my-lib.js",
"require": "./dist/my-lib.umd.cjs"
}
},
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@vanilla-extract/css": "^1.14.0",
"qrcode": "^1.5.3",
"react-remove-scroll": "^2.5.7"
},
"devDependencies": {
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vanilla-extract/vite-plugin": "^3.9.3",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.55.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.2.2",
"vite": "^4.4.11"
},
"peerDependencies": {
"react": ">= 17",
"react-dom": "^18.2.0"
}
}
37 changes: 37 additions & 0 deletions packages/react/src/components/Dialog/Dialog.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { keyframes, style } from "@vanilla-extract/css";

const slideUp = keyframes({
"0%": { transform: "translateY(100%)" },
"100%": { transform: "translateY(0)" },
});

const fadeIn = keyframes({
"0%": { opacity: 0 },
"100%": { opacity: 1 },
});

const bleed = 200;

export const overlay = style({
backdropFilter: "modalOverlay",
background: "rgba(0, 0, 0, 0.3)",
display: "flex",
justifyContent: "center",
position: "fixed",
animation: `${fadeIn} 150ms ease`,
bottom: -bleed,
left: -bleed,
padding: bleed,
right: -bleed,
top: -bleed,
transform: "translateZ(0)", // This is required for content to render under the URL bar on iOS
zIndex: 999999999,
});

export const content = style({
display: "flex",
flexDirection: "column",
position: "relative",
animation: `${slideUp} 350ms cubic-bezier(.15,1.15,0.6,1.00), ${fadeIn} 150ms ease`,
maxWidth: "100vw",
});
74 changes: 74 additions & 0 deletions packages/react/src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
MouseEventHandler,
ReactNode,
useCallback,
useEffect,
useState,
} from "react";
import { createPortal } from "react-dom";
import { RemoveScroll } from "react-remove-scroll";
import * as styles from "./Dialog.css";
import { FocusTrap } from "./FocusTrap";
import { isMobile } from "../../utils";

const stopPropagation: MouseEventHandler<unknown> = (event) =>
event.stopPropagation();

interface DialogProps {
open: boolean;
onClose: () => void;
titleId: string;
onMountAutoFocus?: (event: Event) => void;
children: ReactNode;
}

export function Dialog({ children, onClose, open, titleId }: DialogProps) {
useEffect(() => {
const handleEscape = (event: KeyboardEvent) =>
open && event.key === "Escape" && onClose();

document.addEventListener("keydown", handleEscape);

return () => document.removeEventListener("keydown", handleEscape);
}, [open, onClose]);

const [bodyScrollable, setBodyScrollable] = useState(true);
useEffect(() => {
setBodyScrollable(
getComputedStyle(window.document.body).overflow !== "hidden"
);
}, []);

const handleBackdropClick = useCallback(() => onClose(), [onClose]);

return (
<>
{open
? createPortal(
<RemoveScroll enabled={bodyScrollable}>
<div
style={{
alignItems: isMobile() ? "flex-end" : "center",
position: "fixed",
}}
aria-labelledby={titleId}
aria-modal
className={styles.overlay}
onClick={handleBackdropClick}
role="dialog"
>
<FocusTrap
className={styles.content}
onClick={stopPropagation}
role="document"
>
{children}
</FocusTrap>
</div>
</RemoveScroll>,
document.body
)
: null}
</>
);
}
66 changes: 66 additions & 0 deletions packages/react/src/components/Dialog/FocusTrap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useCallback, useEffect, useRef } from "react";

const moveFocusWithin = (element: HTMLElement, position: "start" | "end") => {
const focusableElements = element.querySelectorAll(
"button:not(:disabled), a[href]"
) as NodeListOf<HTMLButtonElement | HTMLAnchorElement>;

if (focusableElements.length === 0) return;

focusableElements[
position === "end" ? focusableElements.length - 1 : 0
].focus();
};

export function FocusTrap(props: JSX.IntrinsicElements["div"]) {
const contentRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const previouslyActiveElement = document.activeElement;

return () => {
(previouslyActiveElement as HTMLElement).focus?.();
};
}, []);

useEffect(() => {
if (contentRef.current) {
const elementToFocus =
contentRef.current.querySelector("[data-auto-focus]");
if (elementToFocus) {
(elementToFocus as HTMLElement).focus();
} else {
contentRef.current.focus();
}
}
}, []);

return (
<>
<div
onFocus={useCallback(
() =>
contentRef.current && moveFocusWithin(contentRef.current, "end"),
[]
)}
// biome-ignore lint/a11y/noNoninteractiveTabindex: incorrect
tabIndex={0}
/>
<div
ref={contentRef}
style={{ outline: "none" }}
tabIndex={-1}
{...props}
/>
<div
onFocus={useCallback(
() =>
contentRef.current && moveFocusWithin(contentRef.current, "start"),
[]
)}
// biome-ignore lint/a11y/noNoninteractiveTabindex: incorrect
tabIndex={0}
/>
</>
);
}
1 change: 1 addition & 0 deletions packages/react/src/components/Dialog/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./Dialog";
27 changes: 27 additions & 0 deletions packages/react/src/components/SignInButton/SignInButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { button } from "../styles.css.ts";

export function SignInButton() {
const handleClick = () => {
console.log("clicked");
};

return (
<button className={button} onClick={handleClick}>
<svg width="20" height="20" viewBox="0 0 1000 1000" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M257.778 155.556H742.222V844.445H671.111V528.889H670.414C662.554 441.677 589.258 373.333 500 373.333C410.742 373.333 337.446 441.677 329.586 528.889H328.889V844.445H257.778V155.556Z"
fill="white"
/>
<path
d="M128.889 253.333L157.778 351.111H182.222V746.667C169.949 746.667 160 756.616 160 768.889V795.556H155.556C143.283 795.556 133.333 805.505 133.333 817.778V844.445H382.222V817.778C382.222 805.505 372.273 795.556 360 795.556H355.556V768.889C355.556 756.616 345.606 746.667 333.333 746.667H306.667V253.333H128.889Z"
fill="white"
/>
<path
d="M675.556 746.667C663.282 746.667 653.333 756.616 653.333 768.889V795.556H648.889C636.616 795.556 626.667 805.505 626.667 817.778V844.445H875.556V817.778C875.556 805.505 865.606 795.556 853.333 795.556H848.889V768.889C848.889 756.616 838.94 746.667 826.667 746.667V351.111H851.111L880 253.333H702.222V746.667H675.556Z"
fill="white"
/>
</svg>
<span style={{ marginLeft: 9 }}>Sign in with Farcaster</span>
</button>
);
}
1 change: 1 addition & 0 deletions packages/react/src/components/SignInButton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./SignInButton";
1 change: 1 addition & 0 deletions packages/react/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./SignInButton";
38 changes: 38 additions & 0 deletions packages/react/src/components/styles.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { style } from "@vanilla-extract/css";

const resetBase = {
border: 0,
borderColor: "#f1f1f1",
borderStyle: "solid",
borderWidth: 0,
boxSizing: "border-box",
fontSize: "100%",
margin: 0,
padding: 0,
verticalAlign: "baseline",
WebkitFontSmoothing: "antialiased",
WebkitTapHighlightColor: "transparent",
} as const;

const reset = {
button: {
...resetBase,
appearance: "none",
background: "none",
border: "none",
cursor: "pointer",
textAlign: "left",
},
} as const;

export const button = style({
...reset.button,
paddingLeft: 12,
paddingRight: 12,
paddingTop: 9,
paddingBottom: 9,
backgroundColor: "#855DCD",
color: "white",
display: "flex",
alignItems: "center",
});
6 changes: 6 additions & 0 deletions packages/react/src/demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createRoot } from "react-dom/client";
import { SignInButton } from "./index";

const domNode = document.getElementById("root");
const root = createRoot(domNode!);
root.render(<SignInButton />);
Empty file.
2 changes: 2 additions & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./components";
export * from "./hooks";
22 changes: 22 additions & 0 deletions packages/react/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export function isAndroid(): boolean {
return typeof navigator !== "undefined" && /android/i.test(navigator.userAgent);
}

export function isSmallIOS(): boolean {
return typeof navigator !== "undefined" && /iPhone|iPod/.test(navigator.userAgent);
}

export function isLargeIOS(): boolean {
return (
typeof navigator !== "undefined" &&
(/iPad/.test(navigator.userAgent) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1))
);
}

export function isIOS(): boolean {
return isSmallIOS() || isLargeIOS();
}

export function isMobile(): boolean {
return isAndroid() || isIOS();
}
1 change: 1 addition & 0 deletions packages/react/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
Loading

0 comments on commit d431c06

Please sign in to comment.