import * as React from "react" import { useAccount, useCoState } from "@/lib/providers/jazz-provider" import { PersonalLink, Topic } from "@/lib/schema" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { toast } from "sonner" import { createLinkSchema, LinkFormValues } from "./schema" import { cn, generateUniqueSlug } from "@/lib/utils" import { Form } from "@/components/ui/form" import { Button } from "@/components/ui/button" import { UrlInput } from "./partial/url-input" import { UrlBadge } from "./partial/url-badge" import { TitleInput } from "./partial/title-input" import { NotesSection } from "./partial/notes-section" import { TopicSelector } from "./partial/topic-selector" import { DescriptionInput } from "./partial/description-input" import { LearningStateSelector } from "./partial/learning-state-selector" import { atom, useAtom } from "jotai" import { linkLearningStateSelectorAtom, linkTopicSelectorAtom } from "@/store/link" export const globalLinkFormExceptionRefsAtom = atom[]>([]) interface LinkFormProps extends React.ComponentPropsWithoutRef<"form"> { onClose?: () => void onSuccess?: () => void onFail?: () => void personalLink?: PersonalLink exceptionsRefs?: React.RefObject[] } const defaultValues: Partial = { url: "", icon: "", title: "", description: "", completed: false, notes: "", learningState: "wantToLearn", topic: null } export const LinkForm: React.FC = ({ onSuccess, onFail, personalLink, onClose, exceptionsRefs = [] }) => { const [selectedTopic, setSelectedTopic] = React.useState(null) const [istopicSelectorOpen] = useAtom(linkTopicSelectorAtom) const [islearningStateSelectorOpen] = useAtom(linkLearningStateSelectorAtom) const [globalExceptionRefs] = useAtom(globalLinkFormExceptionRefsAtom) const formRef = React.useRef(null) const [isFetching, setIsFetching] = React.useState(false) const [urlFetched, setUrlFetched] = React.useState(null) const { me } = useAccount() const selectedLink = useCoState(PersonalLink, personalLink?.id) const form = useForm({ resolver: zodResolver(createLinkSchema), defaultValues, mode: "all" }) const allExceptionRefs = React.useMemo( () => [...exceptionsRefs, ...globalExceptionRefs], [exceptionsRefs, globalExceptionRefs] ) React.useEffect(() => { const handleClickOutside = (event: MouseEvent) => { const isClickInsideForm = formRef.current && formRef.current.contains(event.target as Node) const isClickInsideExceptions = allExceptionRefs.some((ref, index) => { const isInside = ref.current && ref.current.contains(event.target as Node) return isInside }) if (!isClickInsideForm && !istopicSelectorOpen && !islearningStateSelectorOpen && !isClickInsideExceptions) { onClose?.() } } document.addEventListener("mousedown", handleClickOutside) return () => { document.removeEventListener("mousedown", handleClickOutside) } }, [islearningStateSelectorOpen, istopicSelectorOpen, allExceptionRefs, onClose]) React.useEffect(() => { if (selectedLink) { setUrlFetched(selectedLink.url) form.reset({ url: selectedLink.url, icon: selectedLink.icon, title: selectedLink.title, description: selectedLink.description, completed: selectedLink.completed, notes: selectedLink.notes, learningState: selectedLink.learningState }) } }, [selectedLink, form]) const fetchMetadata = async (url: string) => { setIsFetching(true) try { const res = await fetch(`/api/metadata?url=${encodeURIComponent(url)}`, { cache: "no-cache" }) const data = await res.json() setUrlFetched(data.url) form.setValue("url", data.url, { shouldValidate: true }) form.setValue("icon", data.icon ?? "", { shouldValidate: true }) form.setValue("title", data.title, { shouldValidate: true }) if (!form.getValues("description")) form.setValue("description", data.description, { shouldValidate: true }) form.setFocus("title") console.log(form.formState.isValid, "form state after....") } catch (err) { console.error("Failed to fetch metadata", err) } finally { setIsFetching(false) } } const onSubmit = (values: LinkFormValues) => { if (isFetching) return try { const personalLinks = me.root?.personalLinks?.toJSON() || [] const slug = generateUniqueSlug(personalLinks, values.title) if (selectedLink) { selectedLink.applyDiff({ ...values, slug, topic: selectedTopic }) } else { const newPersonalLink = PersonalLink.create( { ...values, slug, topic: selectedTopic, sequence: me.root?.personalLinks?.length || 1, createdAt: new Date(), updatedAt: new Date() }, { owner: me._owner } ) me.root?.personalLinks?.push(newPersonalLink) } form.reset(defaultValues) onSuccess?.() } catch (error) { onFail?.() console.error("Failed to create/update link", error) toast.error(personalLink ? "Failed to update link" : "Failed to create link") } } const handleCancel = () => { form.reset(defaultValues) onClose?.() } const handleResetUrl = () => { setUrlFetched(null) form.setFocus("url") form.reset({ url: "", title: "", icon: "", description: "" }) } const canSubmit = form.formState.isValid && !form.formState.isSubmitting return (
{isFetching &&
) } LinkForm.displayName = "LinkForm"