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 7d38059 commit 088d2e4
Show file tree
Hide file tree
Showing 23 changed files with 1,736 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}
/>
</>
);
}
10 changes: 10 additions & 0 deletions packages/react/src/components/Provider/ConnectProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ReactNode } from "react";
import { ModalProvider } from "./ModalProvider";

export interface ConnectProviderProps {
children: ReactNode;
}

export function ConnectProvider({ children }: ConnectProviderProps) {
return <ModalProvider>{children}</ModalProvider>;
}
17 changes: 17 additions & 0 deletions packages/react/src/components/Provider/ModalContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createContext, useContext } from "react";

interface ModalContextValue {
isOpen: boolean;
open: () => void;
}

export const ModalContext = createContext<ModalContextValue>({
isOpen: false,
open: () => {
/* no-op */
},
});

export const useModal = () => {
return useContext(ModalContext);
};
28 changes: 28 additions & 0 deletions packages/react/src/components/Provider/ModalProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ReactNode, useCallback, useMemo, useState } from "react";
import { SignInModal } from "../SignInDialog/SignInDialog";
import { ModalContext } from "./ModalContext";

interface ModalProviderProps {
children: ReactNode;
}

export function ModalProvider({ children }: ModalProviderProps) {
const [isOpen, setIsOpen] = useState(false);
const open = useCallback(() => setIsOpen(true), [setIsOpen]);
const close = useCallback(() => setIsOpen(false), [setIsOpen]);

return (
<ModalContext.Provider
value={useMemo(
() => ({
isOpen,
open,
}),
[isOpen, open]
)}
>
{children}
<SignInModal onClose={close} open={isOpen} />
</ModalContext.Provider>
);
}
36 changes: 36 additions & 0 deletions packages/react/src/components/SignInButton/SignInButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useModal } from "../Provider/ModalContext.ts";
import { button } from "../styles.css.ts";

export function SignInButton() {
const { open } = useModal();

const handleClick = () => {
open();
};

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>
);
}
Loading

0 comments on commit 088d2e4

Please sign in to comment.