"use client" import React, { useState, useEffect, useRef } from "react" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { useDebounce } from "react-use" import { toast } from "sonner" import Image from "next/image" import { z } from "zod" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Form, FormField, FormItem, FormLabel, FormControl } from "@/components/ui/form" import { BoxIcon, PlusIcon, Trash2Icon, PieChartIcon, Bookmark, GraduationCap, Check } from "lucide-react" import { cn, ensureUrlProtocol, generateUniqueSlug, isUrl as LibIsUrl } from "@/lib/utils" import { useAccount, useCoState } from "@/lib/providers/jazz-provider" import { LinkMetadata, PersonalLink } from "@/lib/schema/personal-link" import { createLinkSchema } from "./schema" import { TopicSelector } from "./partial/topic-section" import { useAtom } from "jotai" import { linkEditIdAtom, linkShowCreateAtom } from "@/store/link" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { useKey } from "react-use" export type LinkFormValues = z.infer const DEFAULT_FORM_VALUES: Partial = { title: "", description: "", topic: "", isLink: false, meta: null } const LinkManage: React.FC = () => { const [showCreate, setShowCreate] = useAtom(linkShowCreateAtom) const [, setEditId] = useAtom(linkEditIdAtom) const formRef = useRef(null) const buttonRef = useRef(null) const toggleForm = (event: React.MouseEvent) => { event.stopPropagation() setShowCreate(prev => !prev) } useEffect(() => { if (!showCreate) { formRef.current?.reset() setEditId(null) } }, [showCreate, setEditId]) useEffect(() => { const handleOutsideClick = (event: MouseEvent) => { if ( formRef.current && !formRef.current.contains(event.target as Node) && buttonRef.current && !buttonRef.current.contains(event.target as Node) ) { setShowCreate(false) } } if (showCreate) { document.addEventListener("mousedown", handleOutsideClick) } return () => { document.removeEventListener("mousedown", handleOutsideClick) } }, [showCreate, setShowCreate]) useKey("Escape", () => { setShowCreate(false) }) return ( <> {showCreate && (
setShowCreate(false)} onCancel={() => setShowCreate(false)} />
)} ) } const CreateButton = React.forwardRef< HTMLButtonElement, { onClick: (event: React.MouseEvent) => void isOpen: boolean } >(({ onClick, isOpen }, ref) => ( )) CreateButton.displayName = "CreateButton" interface LinkFormProps extends React.ComponentPropsWithoutRef<"form"> { onSuccess?: () => void onCancel?: () => void personalLink?: PersonalLink } const LinkForm = React.forwardRef(({ onSuccess, onCancel, personalLink }, ref) => { const selectedLink = useCoState(PersonalLink, personalLink?.id) const [isFetching, setIsFetching] = useState(false) const { me } = useAccount() const form = useForm({ resolver: zodResolver(createLinkSchema), defaultValues: DEFAULT_FORM_VALUES }) const title = form.watch("title") const [originalLink, setOriginalLink] = useState("") const [linkEntered, setLinkEntered] = useState(false) const [debouncedText, setDebouncedText] = useState("") useDebounce(() => setDebouncedText(title), 300, [title]) const [showStatusOptions, setShowStatusOptions] = useState(false) const [selectedStatus, setSelectedStatus] = useState(null) const statusOptions = [ { text: "To Learn", icon: , color: "text-white/70" }, { text: "Learning", icon: , color: "text-[#D29752]" }, { text: "Learned", icon: , color: "text-[#708F51]" } ] const statusSelect = (status: string) => { setSelectedStatus(status === selectedStatus ? null : status) setShowStatusOptions(false) } useEffect(() => { if (selectedLink) { form.setValue("title", selectedLink.title) form.setValue("description", selectedLink.description ?? "") form.setValue("isLink", selectedLink.isLink) form.setValue("meta", selectedLink.meta) } }, [selectedLink, form]) useEffect(() => { const fetchMetadata = async (url: string) => { setIsFetching(true) try { const res = await fetch(`/api/metadata?url=${encodeURIComponent(url)}`, { cache: "no-store" }) if (!res.ok) throw new Error("Failed to fetch metadata") const data = await res.json() form.setValue("isLink", true) form.setValue("meta", data) form.setValue("title", data.title) form.setValue("description", data.description) setOriginalLink(url) } catch (err) { form.setValue("isLink", false) form.setValue("meta", null) form.setValue("title", debouncedText) form.setValue("description", "") setOriginalLink("") } finally { setIsFetching(false) } } const lowerText = debouncedText.toLowerCase() if (linkEntered && LibIsUrl(lowerText)) { fetchMetadata(ensureUrlProtocol(lowerText)) } }, [debouncedText, form, linkEntered]) const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && LibIsUrl(e.currentTarget.value.toLowerCase())) { e.preventDefault() setLinkEntered(true) } } const onSubmit = (values: LinkFormValues) => { if (isFetching) return try { let linkMetadata: LinkMetadata | undefined const personalLinks = me.root?.personalLinks?.toJSON() || [] const slug = generateUniqueSlug(personalLinks, values.title) if (values.isLink && values.meta) { linkMetadata = LinkMetadata.create(values.meta, { owner: me._owner }) } if (selectedLink) { selectedLink.title = values.title selectedLink.slug = slug selectedLink.description = values.description ?? "" selectedLink.isLink = values.isLink if (selectedLink.meta) { Object.assign(selectedLink.meta, values.meta) } // toast.success("Todo updated") } else { const newPersonalLink = PersonalLink.create( { title: values.title, slug, description: values.description, sequence: me.root?.personalLinks?.length || 1, completed: false, isLink: values.isLink, meta: linkMetadata // topic: values.topic }, { owner: me._owner } ) me.root?.personalLinks?.push(newPersonalLink) } form.reset(DEFAULT_FORM_VALUES) onSuccess?.() } catch (error) { console.error("Failed to create/update link", error) toast.error(personalLink ? "Failed to update link" : "Failed to create link") } } const handleCancel: () => void = () => { form.reset(DEFAULT_FORM_VALUES) onCancel?.() } return (
( Text )} /> {linkEntered ? originalLink : LibIsUrl(form.watch("title").toLowerCase()) ? 'Press "Enter" to confirm URL' : ""}
{/* */} Actions Delete
{showStatusOptions && (
{statusOptions.map(option => ( ))}
)}
( Description