"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 [isFetching, setIsFetching] = useState(false) const { me } = useAccount() const form = useForm({ resolver: zodResolver(createLinkSchema), defaultValues: { ...DEFAULT_FORM_VALUES, isLink: true } }) const selectedLink = useCoState(PersonalLink, personalLink?.id) const title = form.watch("title") const [inputValue, setInputValue] = useState("") const [originalLink, setOriginalLink] = useState("") const [linkValidation, setLinkValidation] = useState(null) const [invalidLink, setInvalidLink] = useState(false) const [showLink, setShowLink] = 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]) const changeInput = (e: React.ChangeEvent) => { const value = e.target.value setInputValue(value) form.setValue("title", value) } const pressEnter = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !showLink) { e.preventDefault() const trimmedValue = inputValue.trim().toLowerCase() if (LibIsUrl(trimmedValue)) { setShowLink(true) setInvalidLink(false) setLinkValidation(trimmedValue) setInputValue(trimmedValue) form.setValue("title", trimmedValue) } else { setInvalidLink(true) setShowLink(true) setLinkValidation(null) } } } useEffect(() => { const fetchMetadata = async (url: string) => { setIsFetching(true) try { const res = await fetch(`/api/metadata?url=${encodeURIComponent(url)}`, { cache: "force-cache" }) 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) } } if (showLink && !invalidLink && LibIsUrl(form.getValues("title").toLowerCase())) { fetchMetadata(ensureUrlProtocol(form.getValues("title").toLowerCase())) } }, [showLink, invalidLink, form]) 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 (values.isLink && values.meta) { linkMetadata = LinkMetadata.create(values.meta, { owner: me._owner }) } } 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 undoEditing: () => void = () => { form.reset(DEFAULT_FORM_VALUES) onCancel?.() } return (
{/* */} ( Text )} /> {showLink && ( {invalidLink ? "Only links are allowed" : linkValidation || originalLink || ""} )}
Actions Delete
{showStatusOptions && (
{statusOptions.map(option => ( ))}
)}
( Description