From 3977391740c90101cc6d28c998f4b6f1211fc26c Mon Sep 17 00:00:00 2001 From: zmeyer44 Date: Fri, 13 Oct 2023 09:23:11 -0400 Subject: [PATCH] boilerplate --- .eslintrc.json | 3 - .eslintrc.ts | 32 +++ app/_providers/index.tsx | 29 +++ app/_providers/modal/index.tsx | 101 ++++++++++ app/_providers/modal/leaflet.tsx | 81 ++++++++ app/_providers/modal/provider.tsx | 64 ++++++ app/_providers/ndk/context/Users.ts | 69 +++++++ app/_providers/ndk/context/index.tsx | 143 ++++++++++++++ app/_providers/ndk/context/instance.ts | 106 ++++++++++ app/_providers/ndk/context/signers.ts | 72 +++++++ app/_providers/ndk/index.ts | 2 + app/_providers/ndk/utils/index.ts | 1 + app/_providers/ndk/utils/notes.ts | 238 +++++++++++++++++++++++ app/api/metadata/route.ts | 87 +++++++++ app/api/metadata/types.ts | 37 ++++ app/api/well-known/nostr/[name]/route.ts | 12 ++ app/globals.css | 75 +++++-- app/layout.tsx | 64 ++++-- app/page.tsx | 2 +- bun.lockb | Bin 133980 -> 191554 bytes components.json | 16 ++ components/ui/avatar.tsx | 50 +++++ components/ui/badge.tsx | 36 ++++ components/ui/button.tsx | 57 ++++++ components/ui/card.tsx | 76 ++++++++ components/ui/dialog.tsx | 119 ++++++++++++ components/ui/dropdown-menu.tsx | 205 +++++++++++++++++++ components/ui/form.tsx | 176 +++++++++++++++++ components/ui/input.tsx | 25 +++ components/ui/label.tsx | 26 +++ components/ui/select.tsx | 120 ++++++++++++ components/ui/switch.tsx | 29 +++ components/ui/textarea.tsx | 24 +++ constants/index.ts | 1 + constants/relays.ts | 7 + lib/hooks/useAutoSizeTextArea.ts | 21 ++ lib/hooks/useCacheFetch.ts | 124 ++++++++++++ lib/hooks/useElementOnScreen.ts | 40 ++++ lib/hooks/useLocalStorage.ts | 45 +++++ lib/hooks/useQueryParams.ts | 62 ++++++ lib/hooks/useRouteChange.ts | 18 ++ lib/hooks/useWindowSize.ts | 38 ++++ lib/utils/dates.ts | 73 +++++++ lib/utils/index.ts | 68 +++++++ next.config.js | 19 +- package.json | 44 ++++- prettier.config.js | 3 + tailwind.config.ts | 109 +++++++++-- tsconfig.json | 13 +- types/global.ts | 0 50 files changed, 2805 insertions(+), 57 deletions(-) delete mode 100644 .eslintrc.json create mode 100644 .eslintrc.ts create mode 100644 app/_providers/index.tsx create mode 100644 app/_providers/modal/index.tsx create mode 100644 app/_providers/modal/leaflet.tsx create mode 100644 app/_providers/modal/provider.tsx create mode 100644 app/_providers/ndk/context/Users.ts create mode 100644 app/_providers/ndk/context/index.tsx create mode 100644 app/_providers/ndk/context/instance.ts create mode 100644 app/_providers/ndk/context/signers.ts create mode 100644 app/_providers/ndk/index.ts create mode 100644 app/_providers/ndk/utils/index.ts create mode 100644 app/_providers/ndk/utils/notes.ts create mode 100644 app/api/metadata/route.ts create mode 100644 app/api/metadata/types.ts create mode 100644 app/api/well-known/nostr/[name]/route.ts create mode 100644 components.json create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 constants/index.ts create mode 100644 constants/relays.ts create mode 100644 lib/hooks/useAutoSizeTextArea.ts create mode 100644 lib/hooks/useCacheFetch.ts create mode 100644 lib/hooks/useElementOnScreen.ts create mode 100644 lib/hooks/useLocalStorage.ts create mode 100644 lib/hooks/useQueryParams.ts create mode 100644 lib/hooks/useRouteChange.ts create mode 100644 lib/hooks/useWindowSize.ts create mode 100644 lib/utils/dates.ts create mode 100644 lib/utils/index.ts create mode 100644 prettier.config.js create mode 100644 types/global.ts diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index bffb357..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/.eslintrc.ts b/.eslintrc.ts new file mode 100644 index 0000000..d30ea24 --- /dev/null +++ b/.eslintrc.ts @@ -0,0 +1,32 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + overrides: [ + { + extends: [ + "plugin:@typescript-eslint/recommended-requiring-type-checking", + ], + files: ["*.ts", "*.tsx"], + parserOptions: { + project: "tsconfig.json", + }, + }, + ], + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + }, + plugins: ["@typescript-eslint"], + extends: ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"], + rules: { + "import/no-anonymous-default-export": ["off"], + "@typescript-eslint/no-unsafe-assignment": ["error"], + "@typescript-eslint/no-empty-interface": ["warn"], + "@typescript-eslint/consistent-type-imports": [ + "warn", + { + prefer: "type-imports", + fixStyle: "inline-type-imports", + }, + ], + }, +}; diff --git a/app/_providers/index.tsx b/app/_providers/index.tsx new file mode 100644 index 0000000..4d09648 --- /dev/null +++ b/app/_providers/index.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { Toaster } from "sonner"; +import { ModalProvider } from "./modal/provider"; +import useRouteChange from "@/lib/hooks/useRouteChange"; +import { NDKProvider } from "./ndk"; +import { RELAYS } from "@/constants"; + +export function Providers({ children }: { children: React.ReactNode }) { + const handleRouteChange = (url: string) => { + const RichHistory = sessionStorage.getItem("RichHistory"); + if (!RichHistory) { + sessionStorage.setItem("RichHistory", "true"); + } + }; + useRouteChange(handleRouteChange); + return ( + <> + + + + + {children} + + + ); +} diff --git a/app/_providers/modal/index.tsx b/app/_providers/modal/index.tsx new file mode 100644 index 0000000..511015b --- /dev/null +++ b/app/_providers/modal/index.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useRef, +} from "react"; +import FocusTrap from "focus-trap-react"; +import { AnimatePresence, motion } from "framer-motion"; +import Leaflet from "./leaflet"; +import useWindowSize from "@/lib/hooks/useWindowSize"; + +export default function Modal({ + children, + showModal, + setShowModal, +}: { + children: React.ReactNode; + showModal: boolean; + setShowModal: Dispatch>; +}) { + const desktopModalRef = useRef(null); + + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === "Escape") { + setShowModal(false); + } + }, + [setShowModal] + ); + + useEffect(() => { + document.addEventListener("keydown", onKeyDown); + return () => document.removeEventListener("keydown", onKeyDown); + }, [onKeyDown]); + + const { isMobile, isDesktop } = useWindowSize(); + + useEffect(() => { + if (showModal) { + document.body.style.top = `-${window.scrollY}px`; + document.body.style.position = "fixed"; + } else { + const scrollY = document.body.style.top; + document.body.style.position = ""; + document.body.style.top = ""; + window.scrollTo(0, parseInt(scrollY || "0") * -1); + } + return () => { + const scrollY = document.body.style.top; + document.body.style.position = ""; + document.body.style.top = ""; + window.scrollTo(0, parseInt(scrollY || "0") * -1); + }; + }, [showModal]); + + return ( + + {showModal && ( + <> + {isMobile && {children}} + {isDesktop && ( + <> + + { + if (desktopModalRef.current === e.target) { + setShowModal(false); + } + }} + > +
{children}
+
+
+ setShowModal(false)} + /> + + )} + + )} +
+ ); +} diff --git a/app/_providers/modal/leaflet.tsx b/app/_providers/modal/leaflet.tsx new file mode 100644 index 0000000..65ba8de --- /dev/null +++ b/app/_providers/modal/leaflet.tsx @@ -0,0 +1,81 @@ +import { useEffect, useRef, ReactNode, Dispatch, SetStateAction } from "react"; +import { AnimatePresence, motion, useAnimation } from "framer-motion"; +import { cn } from "@/lib/utils"; + +export default function Leaflet({ + setShow, + children, +}: { + setShow: Dispatch>; + children: ReactNode; +}) { + const leafletRef = useRef(null); + const controls = useAnimation(); + const transitionProps = { type: "spring", stiffness: 500, damping: 30 }; + useEffect(() => { + void controls.start({ + y: 20, + transition: transitionProps, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + async function handleDragEnd( + _: any, + info: { + delta: { x: number; y: number }; + offset: { x: number; y: number }; + point: { x: number; y: number }; + velocity: { x: number; y: number }; + } + ) { + const offset = info.offset.y; + const velocity = info.velocity.y; + const height = leafletRef.current?.getBoundingClientRect().height || 0; + if (offset > height / 2 || velocity > 800) { + await controls.start({ y: "100%", transition: transitionProps }); + setShow(false); + } else { + void controls.start({ y: 0, transition: transitionProps }); + } + } + + return ( + + void handleDragEnd(_, i)} + dragElastic={{ top: 0, bottom: 1 }} + dragConstraints={{ top: 0, bottom: 0 }} + > +
+
+
+
+
+ {children} +
+ + setShow(false)} + /> + + ); +} diff --git a/app/_providers/modal/provider.tsx b/app/_providers/modal/provider.tsx new file mode 100644 index 0000000..07407bd --- /dev/null +++ b/app/_providers/modal/provider.tsx @@ -0,0 +1,64 @@ +"use client"; + +import Modal from "."; +import { + ReactElement, + ReactNode, + createContext, + useContext, + useState, +} from "react"; + +export enum Modals {} + +type ModalsType = { + [key in Modals]: (props: any) => JSX.Element; +}; + +const ModalOptions: ModalsType = {}; + +type ModalProps = ReactElement | Modals; + +type ModalContextProps = { + show: (content: ModalProps) => void; + hide: () => void; +}; + +const ModalContext = createContext(undefined); + +export function ModalProvider({ children }: { children: ReactNode }) { + const [modalContent, setModalContent] = useState(null); + const [showModal, setShowModal] = useState(false); + + const show = (content: ModalProps) => { + if (typeof content === "string" && ModalOptions[content]) { + setModalContent(ModalOptions[content]); + } else { + setModalContent(content); + } + + setShowModal(true); + }; + + const hide = () => { + setShowModal(false); + setTimeout(() => { + setModalContent(null); + }, 300); // Adjust this timeout as per your transition duration + }; + + return ( + + {children} + {showModal && ( + + {modalContent} + + )} + + ); +} + +export function useModal() { + return useContext(ModalContext); +} diff --git a/app/_providers/ndk/context/Users.ts b/app/_providers/ndk/context/Users.ts new file mode 100644 index 0000000..7d2a6e7 --- /dev/null +++ b/app/_providers/ndk/context/Users.ts @@ -0,0 +1,69 @@ +"use client"; + +import { useRef, useState } from "react"; +import NDK, { NDKUser } from "@nostr-dev-kit/ndk"; + +export const Users = (ndk: NDK | undefined) => { + const [users, setUsers] = useState<{ [id: string]: NDKUser }>({}); + const refUsers = useRef<{ [id: string]: NDKUser }>({}); + + async function fetchUser(id: string) { + if (ndk == undefined) { + return; + } + + if (refUsers.current[id]) { + return; + } + + refUsers.current = { + ...refUsers.current, + [id]: NDKUser.prototype, + }; + + let user; + + if (id.startsWith("npub")) { + user = ndk.getUser({ + npub: id, + }); + } else { + user = ndk.getUser({ + hexpubkey: id, + }); + } + + await user.fetchProfile(); + + if (user.profile) { + refUsers.current = { + ...refUsers.current, + [id]: user, + }; + setUsers(refUsers.current); + } + } + + function getUser(id: string) { + if (users[id]) { + return users[id]; + } else { + fetchUser(id); + } + return NDKUser.prototype; + } + + function getProfile(id: string) { + if (users[id]) { + return users[id].profile!; + } else { + fetchUser(id); + } + return {}; + } + + return { + getUser, + getProfile, + }; +}; diff --git a/app/_providers/ndk/context/index.tsx b/app/_providers/ndk/context/index.tsx new file mode 100644 index 0000000..b900ba4 --- /dev/null +++ b/app/_providers/ndk/context/index.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { PropsWithChildren, createContext, useContext } from "react"; +import NDK, { + NDKEvent, + NDKFilter, + NDKNip07Signer, + NDKNip46Signer, + NDKPrivateKeySigner, + NDKUser, + NDKUserProfile, +} from "@nostr-dev-kit/ndk"; +import NDKInstance from "./instance"; +import { _loginWithNip07, _loginWithNip46, _loginWithSecret } from "./signers"; +import { Users } from "./Users"; + +interface NDKContext { + ndk: NDK | undefined; + signer: NDKPrivateKeySigner | NDKNip46Signer | NDKNip07Signer | undefined; + fetchEvents: (filter: NDKFilter) => Promise; + loginWithNip46: ( + npub: string, + sk?: string, + ) => Promise< + | undefined + | { + npub: string; + sk: string | undefined; + token: string; + remoteSigner: NDKNip46Signer; + localSigner: NDKPrivateKeySigner; + } + >; + loginWithSecret: (skOrNsec: string) => Promise< + | undefined + | { + npub: string; + sk: string; + signer: NDKPrivateKeySigner; + } + >; + loginWithNip07: () => Promise< + | undefined + | { + npub: string; + signer: NDKNip07Signer; + user: NDKUser; + } + >; + signPublishEvent: ( + event: NDKEvent, + params?: + | { + repost: boolean; + publish: boolean; + } + | undefined, + ) => Promise; + getUser: (_: string) => NDKUser; + getProfile: (_: string) => NDKUserProfile; +} + +const NDKContext = createContext({ + ndk: undefined, + signer: undefined, + fetchEvents: (_: NDKFilter) => Promise.resolve([]), + loginWithNip46: (_: string, __?: string) => Promise.resolve(undefined), + loginWithSecret: (_: string) => Promise.resolve(undefined), + loginWithNip07: () => Promise.resolve(undefined), + signPublishEvent: (_: NDKEvent, __?: {}) => Promise.resolve(undefined), + getUser: (_: string) => { + return NDKUser.prototype; + }, + getProfile: (_: string) => { + return {}; + }, +}); + +const NDKProvider = ({ + children, + relayUrls, +}: PropsWithChildren<{ + relayUrls: string[]; +}>) => { + const { ndk, signer, setSigner, fetchEvents, signPublishEvent } = + NDKInstance(relayUrls); + const { getUser, getProfile } = Users(ndk); + + async function loginWithNip46(npub: string, sk?: string) { + if (ndk === undefined) return undefined; + const res = await _loginWithNip46(ndk, npub, sk); + if (res) { + await setSigner(res.remoteSigner); + return res; + } + } + + async function loginWithSecret(skOrNsec: string) { + const res = await _loginWithSecret(skOrNsec); + if (res) { + const { signer } = res; + await setSigner(signer); + return res; + } + } + + async function loginWithNip07() { + const res = await _loginWithNip07(); + if (res) { + const { signer } = res; + await setSigner(signer); + return res; + } + } + + return ( + + {children} + + ); +}; + +const useNDK = () => { + const context = useContext(NDKContext); + if (context === undefined) { + throw new Error("import NDKProvider to use useNDK"); + } + return context; +}; + +export { NDKProvider, useNDK }; diff --git a/app/_providers/ndk/context/instance.ts b/app/_providers/ndk/context/instance.ts new file mode 100644 index 0000000..84f1660 --- /dev/null +++ b/app/_providers/ndk/context/instance.ts @@ -0,0 +1,106 @@ +"use client"; +import { useEffect, useRef, useState } from "react"; +import NDK, { + NDKEvent, + NDKFilter, + NDKNip07Signer, + NDKNip46Signer, + NDKPrivateKeySigner, +} from "@nostr-dev-kit/ndk"; + +export default function NDKInstance(explicitRelayUrls: string[]) { + const loaded = useRef(false); + + const [ndk, _setNDK] = useState(undefined); + const [signer, _setSigner] = useState< + NDKPrivateKeySigner | NDKNip46Signer | NDKNip07Signer | undefined + >(undefined); + + useEffect(() => { + async function load() { + if (ndk === undefined && loaded.current === false) { + loaded.current = true; + await loadNdk(explicitRelayUrls); + } + } + load(); + }, []); + + async function loadNdk( + explicitRelayUrls: string[], + signer?: NDKPrivateKeySigner | NDKNip46Signer | NDKNip07Signer, + ) { + const ndkInstance = new NDK({ explicitRelayUrls, signer }); + if (process.env.NODE_ENV === "development") { + ndkInstance.pool.on("connect", () => console.log("✅ connected")); + ndkInstance.pool.on("disconnect", () => console.log("❌ disconnected")); + } + + if (signer) { + _setSigner(signer); + } + + try { + await ndkInstance.connect(); + _setNDK(ndkInstance); + } catch (error) { + console.error("ERROR loading NDK NDKInstance", error); + } + } + + async function setSigner( + signer: NDKPrivateKeySigner | NDKNip46Signer | NDKNip07Signer, + ) { + loadNdk(explicitRelayUrls, signer); + } + + async function fetchEvents(filter: NDKFilter): Promise { + if (ndk === undefined) return []; + + return new Promise((resolve) => { + const events: Map = new Map(); + + const relaySetSubscription = ndk.subscribe(filter, { + closeOnEose: true, + }); + + relaySetSubscription.on("event", (event: NDKEvent) => { + event.ndk = ndk; + events.set(event.tagId(), event); + }); + + relaySetSubscription.on("eose", () => { + setTimeout(() => resolve(Array.from(new Set(events.values()))), 3000); + }); + }); + } + + async function signPublishEvent( + event: NDKEvent, + params: { repost: boolean; publish: boolean } | undefined = { + repost: false, + publish: true, + }, + ) { + if (ndk === undefined) return; + event.ndk = ndk; + if (params.repost) { + await event.repost(); + } else { + await event.sign(); + } + if (params.publish) { + await event.publish(); + } + return event; + } + + return { + ndk, + signer, + loadNdk, + setSigner, + fetchEvents, + signPublishEvent, + }; +} diff --git a/app/_providers/ndk/context/signers.ts b/app/_providers/ndk/context/signers.ts new file mode 100644 index 0000000..2532740 --- /dev/null +++ b/app/_providers/ndk/context/signers.ts @@ -0,0 +1,72 @@ +"use client"; + +import { nip19 } from "nostr-tools"; +import NDK, { + NDKNip07Signer, + NDKNip46Signer, + NDKPrivateKeySigner, +} from "@nostr-dev-kit/ndk"; + +export async function _loginWithSecret(skOrNsec: string) { + try { + let privkey = skOrNsec; + + if (privkey.substring(0, 4) === "nsec") { + privkey = nip19.decode(privkey).data as string; + } + + const signer = new NDKPrivateKeySigner(privkey); + return signer.user().then(async (user) => { + if (user.npub) { + return { + user: user, + npub: user.npub, + sk: privkey, + signer: signer, + }; + } + }); + } catch (e) { + throw e; + } +} + +export async function _loginWithNip46(ndk: NDK, token: string, sk?: string) { + try { + let localSigner = NDKPrivateKeySigner.generate(); + if (sk) { + localSigner = new NDKPrivateKeySigner(sk); + } + + const remoteSigner = new NDKNip46Signer(ndk, token, localSigner); + + return remoteSigner.user().then(async (user) => { + if (user.npub) { + await remoteSigner.blockUntilReady(); + return { + user: user, + npub: (await remoteSigner.user()).npub, + sk: localSigner.privateKey, + token: token, + remoteSigner: remoteSigner, + localSigner: localSigner, + }; + } + }); + } catch (e) { + throw e; + } +} + +export async function _loginWithNip07() { + try { + const signer = new NDKNip07Signer(); + return signer.user().then(async (user) => { + if (user.npub) { + return { user: user, npub: user.npub, signer: signer }; + } + }); + } catch (e) { + throw e; + } +} diff --git a/app/_providers/ndk/index.ts b/app/_providers/ndk/index.ts new file mode 100644 index 0000000..3f3bd57 --- /dev/null +++ b/app/_providers/ndk/index.ts @@ -0,0 +1,2 @@ +export * from "./context"; +export * from "./utils"; diff --git a/app/_providers/ndk/utils/index.ts b/app/_providers/ndk/utils/index.ts new file mode 100644 index 0000000..b9f45ef --- /dev/null +++ b/app/_providers/ndk/utils/index.ts @@ -0,0 +1 @@ +export * from "./notes"; diff --git a/app/_providers/ndk/utils/notes.ts b/app/_providers/ndk/utils/notes.ts new file mode 100644 index 0000000..b43d7cc --- /dev/null +++ b/app/_providers/ndk/utils/notes.ts @@ -0,0 +1,238 @@ +import { last, pluck, identity } from "ramda"; +import { nip19 } from "nostr-tools"; + +export const NEWLINE = "newline"; +export const TEXT = "text"; +export const TOPIC = "topic"; +export const LINK = "link"; +export const INVOICE = "invoice"; +export const NOSTR_NOTE = "nostr:note"; +export const NOSTR_NEVENT = "nostr:nevent"; +export const NOSTR_NPUB = "nostr:npub"; +export const NOSTR_NPROFILE = "nostr:nprofile"; +export const NOSTR_NADDR = "nostr:naddr"; + +function first(list: any) { + return list ? list[0] : undefined; +} + +export const fromNostrURI = (s: string) => s.replace(/^[\w\+]+:\/?\/?/, ""); + +export const urlIsMedia = (url: string) => + !url.match(/\.(apk|docx|xlsx|csv|dmg)/) && + last(url.split("://"))?.includes("/"); + +export const parseContent = ({ + content, + tags = [], +}: { + content: string; + tags: any[]; +}): { type: string; value: string }[] => { + const result: { type: string; value: string }[] = []; + + let text = content.trim(); + let buffer = ""; + + const parseNewline = () => { + const newline = first(text.match(/^\n+/)); + + if (newline) { + return [NEWLINE, newline, newline]; + } + }; + + const parseMention = () => { + // Convert legacy mentions to bech32 entities + const mentionMatch = text.match(/^#\[(\d+)\]/i); + + if (mentionMatch) { + const i = parseInt(mentionMatch[1]); + + if (tags[i]) { + const [tag, value, url] = tags[i]; + const relays = [url].filter(identity); + + let type, data, entity; + if (tag === "p") { + type = "nprofile"; + data = { pubkey: value, relays }; + entity = nip19.nprofileEncode(data); + } else { + type = "nevent"; + data = { id: value, relays }; + entity = nip19.neventEncode(data); + } + + return [`nostr:${type}`, mentionMatch[0], { ...data, entity }]; + } + } + }; + + const parseTopic = () => { + const topic = first(text.match(/^#\w+/i)); + + // Skip numeric topics + if (topic && !topic.match(/^#\d+$/)) { + return [TOPIC, topic, topic.slice(1)]; + } + }; + + const parseBech32 = () => { + const bech32 = first( + text.match( + /^(web\+)?(nostr:)?\/?\/?n(event|ote|profile|pub|addr)1[\d\w]+/i, + ), + ); + + if (bech32) { + try { + const entity = fromNostrURI(bech32); + const { type, data } = nip19.decode(entity) as { + type: string; + data: object; + }; + + let value = data; + if (type === "note") { + value = { id: data }; + } else if (type === "npub") { + value = { pubkey: data }; + } + + return [`nostr:${type}`, bech32, { ...value, entity }]; + } catch (e) { + console.log(e); + // pass + } + } + }; + + const parseInvoice = () => { + const invoice = first(text.match(/^ln(bc|url)[\d\w]{50,1000}/i)); + + if (invoice) { + return [INVOICE, invoice, invoice]; + } + }; + + const parseUrl = () => { + const raw = first( + text.match( + /^([a-z\+:]{2,30}:\/\/)?[^\s]+\.[a-z]{2,6}[^\s]*[^\.!?,:\s]/gi, + ), + ); + + // Skip url if it's just the end of a filepath + if (raw) { + const prev: any = last(result); + + if (prev?.type === "text" && prev.value.endsWith("/")) { + return; + } + + let url = raw; + + // Skip ellipses and very short non-urls + if (url.match(/\.\./)) { + return; + } + + if (!url.match("://")) { + url = "https://" + url; + } + + return [LINK, raw, { url, isMedia: urlIsMedia(url) }]; + } + }; + + while (text) { + const part = + parseNewline() || + parseMention() || + parseTopic() || + parseBech32() || + parseUrl() || + parseInvoice(); + + if (part) { + if (buffer) { + result.push({ type: "text", value: buffer }); + buffer = ""; + } + + const [type, raw, value] = part; + + result.push({ type, value }); + text = text.slice(raw.length); + } else { + // Instead of going character by character and re-running all the above regular expressions + // a million times, try to match the next word and add it to the buffer + const match = first(text.match(/^[\w\d]+ ?/i)) || text[0]; + + buffer += match; + text = text.slice(match.length); + } + } + + if (buffer) { + result.push({ type: TEXT, value: buffer }); + } + + return result; +}; + +export const truncateContent = ( + content: { value: string; type: string; isMedia: boolean }[], + { + showEntire, + maxLength, + showMedia = false, + }: { showEntire: boolean; maxLength: number; showMedia: boolean }, +) => { + if (showEntire) { + return content; + } + + let length = 0; + const result: any[] = []; + const truncateAt = maxLength * 0.6; + + content.every((part, i) => { + const isText = + [TOPIC, TEXT].includes(part.type) || + (part.type === LINK && !part.isMedia); + const isMedia = + part.type === INVOICE || part.type.startsWith("nostr:") || part.isMedia; + + if (isText) { + length += part.value.length; + } + + if (isMedia) { + length += showMedia ? maxLength / 3 : part.value.length; + } + + result.push(part); + + if (length > truncateAt && i < content.length - 1) { + if (isText || (isMedia && !showMedia)) { + result.push({ type: TEXT, value: "..." }); + } + + return false; + } + + return true; + }); + + return result; +}; + +export const getLinks = ( + parts: { value: string; type: string; isMedia: boolean }[], +) => + pluck( + "value", + parts.filter((x) => x.type === LINK && x.isMedia), + ); diff --git a/app/api/metadata/route.ts b/app/api/metadata/route.ts new file mode 100644 index 0000000..0da7aa2 --- /dev/null +++ b/app/api/metadata/route.ts @@ -0,0 +1,87 @@ +import { NextRequest, NextResponse } from "next/server"; +import { parse } from "node-html-parser"; +import { z } from "zod"; +import { validateUrl } from "@/lib/utils"; + + +type MetaData = { + title: string; + description: string; + image: string; + creator: string; + "theme-color": string; + type: string; +}; +const keys = [ + "title", + "description", + "image", + "creator", + "theme-color", + "type", +] as const; +const bodySchema = z.object({ + url: z.string(), +}); +async function handler(req: NextRequest) { + const body = await req.json(); + let { url } = bodySchema.parse(body); + + if (url.includes("open.spotify")) { + url = url.split("?")[0] as string; + } else if (url.startsWith("https://t.co/")) { + // const data = await getMetaData({ + // url: url, + // ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + // }); + // if (data.title) { + // url = data.title; + // } + console.log("Twitter"); + } + + const decode = decodeURIComponent(url); + try { + const retreivedHtml = await fetch(decode); + const html = await retreivedHtml.text(); + const root = parse(html); + + const metadata: Partial = {}; + const titleElement = root.querySelector("title"); + const title = titleElement?.text; + metadata.title = title; + const metaTags = root.querySelectorAll("meta"); + + for (const metaTag of metaTags) { + const name = + metaTag.getAttribute("name") ?? metaTag.getAttribute("property"); + + const content = metaTag.getAttribute("content"); + if (!name || !content) continue; + for (const key of keys) { + if (name.includes(key)) { + if (key === "image" && !validateUrl(content)) { + continue; + } + const current = metadata[key]; + if (!current || content.length > current.length) { + metadata[key] = content; + } + } + } + console.log(`Name: ${name}, Content: ${content}`); + } + + const data = { + ...metadata, + url: decode, + }; + return NextResponse.json({ + data, + }); + } catch (err) { + console.log("Error", err); + } +} + +export { handler as GET, handler as POST }; diff --git a/app/api/metadata/types.ts b/app/api/metadata/types.ts new file mode 100644 index 0000000..fa46220 --- /dev/null +++ b/app/api/metadata/types.ts @@ -0,0 +1,37 @@ +export interface MetaData { + title?: string; + description?: string; + icon?: string; + image?: string; + keywords?: string[]; + language?: string; + type?: string; + url?: string; + provider?: string; + [x: string]: string | string[] | undefined; +} + +export type MetadataRule = [string, (el: Element) => string | null]; + +export interface Context { + url: string; + options: Options; +} + +export interface RuleSet { + rules: MetadataRule[]; + defaultValue?: (context: Context) => string | string[]; + scorer?: (el: Element, score: any) => any; + processor?: (input: any, context: Context) => any; +} + +export interface Options { + maxRedirects?: number; + ua?: string; + lang?: string; + timeout?: number; + forceImageHttps?: boolean; + html?: string; + url?: string; + customRules?: Record; +} diff --git a/app/api/well-known/nostr/[name]/route.ts b/app/api/well-known/nostr/[name]/route.ts new file mode 100644 index 0000000..636d146 --- /dev/null +++ b/app/api/well-known/nostr/[name]/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from "next/server"; + +function handler() { + return NextResponse.json({ + names: { + zach: "17717ad4d20e2a425cda0a2195624a0a4a73c4f6975f16b1593fc87fa46f2d58", + _: "17717ad4d20e2a425cda0a2195624a0a4a73c4f6975f16b1593fc87fa46f2d58", + }, + }); +} + +export { handler as GET, handler as POST }; diff --git a/app/globals.css b/app/globals.css index fd81e88..b5c2a0d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,27 +1,66 @@ @tailwind base; @tailwind components; @tailwind utilities; + -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { +@layer base { :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; + --background: 0 0% 100%; + --foreground: 20 14.3% 4.1%; + --card: 0 0% 100%; + --card-foreground: 20 14.3% 4.1%; + --popover: 0 0% 100%; + --popover-foreground: 20 14.3% 4.1%; + --primary: 24.6 95% 53.1%; + --primary-foreground: 60 9.1% 97.8%; + --secondary: 60 4.8% 95.9%; + --secondary-foreground: 24 9.8% 10%; + --muted: 60 4.8% 95.9%; + --muted-foreground: 25 5.3% 44.7%; + --accent: 60 4.8% 95.9%; + --accent-foreground: 24 9.8% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 60 9.1% 97.8%; + --border: 20 5.9% 90%; + --input: 20 5.9% 90%; + --ring: 24.6 95% 53.1%; + --radius: 0.75rem; + } + + .dark { + --background: 20 14.3% 4.1%; + --foreground: 60 9.1% 97.8%; + --card: 20 14.3% 4.1%; + --card-foreground: 60 9.1% 97.8%; + --popover: 20 14.3% 4.1%; + --popover-foreground: 60 9.1% 97.8%; + --primary: 20.5 90.2% 48.2%; + --primary-foreground: 60 9.1% 97.8%; + --secondary: 12 6.5% 15.1%; + --secondary-foreground: 60 9.1% 97.8%; + --muted: 12 6.5% 15.1%; + --muted-foreground: 24 5.4% 63.9%; + --accent: 12 6.5% 15.1%; + --accent-foreground: 60 9.1% 97.8%; + --destructive: 0 72.2% 50.6%; + --destructive-foreground: 60 9.1% 97.8%; + --border: 12 6.5% 15.1%; + --input: 12 6.5% 15.1%; + --ring: 20.5 90.2% 48.2%; } } -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } } +@layer components { + .center { + @apply flex items-center justify-center; + } +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index ae84562..1ec0f7d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,22 +1,64 @@ -import './globals.css' -import type { Metadata } from 'next' -import { Inter } from 'next/font/google' +import "./globals.css"; +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import { cn } from "@/lib/utils"; +import { Providers } from "./_providers"; -const inter = Inter({ subsets: ['latin'] }) +const inter = Inter({ subsets: ["latin"] }); +const title = "Flockstr"; +const description = "Own your flock"; +const image = + "https://o-0-o-image-storage.s3.amazonaws.com/zachmeyer_a_cartoon_image_of_an_ostrich_wearing_sunglasses_at_a_e68ac83e-a3b8-4d81-9550-a1fb7ee1ee62.png"; export const metadata: Metadata = { - title: 'Create Next App', - description: 'Generated by create next app', -} + title, + description, + icons: [ + { rel: "apple-touch-icon", url: "/apple-touch-icon.png" }, + { rel: "shortcut icon", url: "/favicon.ico" }, + ], + openGraph: { + title, + description, + images: [image], + }, + twitter: { + card: "summary_large_image", + title, + description, + images: [image], + creator: "@zachmeyer_", + }, + metadataBase: new URL("https://flockstr.com"), + viewport: + "minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, viewport-fit=cover", + manifest: "/manifest.json", + appleWebApp: { + capable: true, + title: title, + statusBarStyle: "default", + }, + applicationName: "Flockstr", + formatDetection: { + telephone: false, + }, + themeColor: { + color: "#000000", + }, +}; export default function RootLayout({ children, }: { - children: React.ReactNode + children: React.ReactNode; }) { return ( - - {children} + + + {children} + - ) + ); } diff --git a/app/page.tsx b/app/page.tsx index 7a8286b..941ad8c 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,6 @@ import Image from 'next/image' -export default function Home() { +export default function LandingPage() { return (
diff --git a/bun.lockb b/bun.lockb index 8090c726108362941073fc0b1dabdfc4bf7a487b..0923e1bce65d780aff80c189e775f2ad9ba1cdf3 100755 GIT binary patch delta 63509 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 J&M-+!! diff --git a/components.json b/components.json new file mode 100644 index 0000000..48ade7d --- /dev/null +++ b/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "zinc", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..51e507b --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..e87d62b --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..4ecf369 --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,57 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..77e9fb7 --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,76 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 0000000..cf284e9 --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,119 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { Cross2Icon } from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..242b07a --- /dev/null +++ b/components/ui/dropdown-menu.tsx @@ -0,0 +1,205 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { + CheckIcon, + ChevronRightIcon, + DotFilledIcon, +} from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/components/ui/form.tsx b/components/ui/form.tsx new file mode 100644 index 0000000..f6afdaf --- /dev/null +++ b/components/ui/form.tsx @@ -0,0 +1,176 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +