diff --git a/bun.lockb b/bun.lockb index dce45b0b..7a133c97 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/web/components/routes/link/form/manage.tsx b/web/components/routes/link/form/manage.tsx index 1dc01fb9..7c549971 100644 --- a/web/components/routes/link/form/manage.tsx +++ b/web/components/routes/link/form/manage.tsx @@ -121,20 +121,24 @@ interface LinkFormProps extends React.ComponentPropsWithoutRef<"form"> { } 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 + 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 [linkEntered, setLinkEntered] = useState(false) + 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) @@ -166,11 +170,35 @@ const LinkForm = React.forwardRef(({ onSuccess, } }, [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: "no-store" }) + 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) @@ -188,19 +216,10 @@ const LinkForm = React.forwardRef(({ onSuccess, setIsFetching(false) } } - - const lowerText = debouncedText.toLowerCase() - if (linkEntered && LibIsUrl(lowerText)) { - fetchMetadata(ensureUrlProtocol(lowerText)) + if (showLink && !invalidLink && LibIsUrl(form.getValues("title").toLowerCase())) { + fetchMetadata(ensureUrlProtocol(form.getValues("title").toLowerCase())) } - }, [debouncedText, form, linkEntered]) - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && LibIsUrl(e.currentTarget.value.toLowerCase())) { - e.preventDefault() - setLinkEntered(true) - } - } + }, [showLink, invalidLink, form]) const onSubmit = (values: LinkFormValues) => { if (isFetching) return @@ -221,11 +240,9 @@ const LinkForm = React.forwardRef(({ onSuccess, selectedLink.description = values.description ?? "" selectedLink.isLink = values.isLink - if (selectedLink.meta) { - Object.assign(selectedLink.meta, values.meta) + if (values.isLink && values.meta) { + linkMetadata = LinkMetadata.create(values.meta, { owner: me._owner }) } - - // toast.success("Todo updated") } else { const newPersonalLink = PersonalLink.create( { @@ -252,26 +269,26 @@ const LinkForm = React.forwardRef(({ onSuccess, } } - const handleCancel: () => void = () => { + const undoEditing: () => void = () => { form.reset(DEFAULT_FORM_VALUES) onCancel?.() } return (
-
+
- + */} (({ onSuccess, )} /> - - {linkEntered - ? originalLink - : LibIsUrl(form.watch("title").toLowerCase()) - ? 'Press "Enter" to confirm URL' - : ""} - + {showLink && ( + + {invalidLink ? "Only links are allowed" : linkValidation || originalLink || ""} + + )}
- - {/* */} - + Actions - + Delete @@ -393,7 +401,7 @@ const LinkForm = React.forwardRef(({ onSuccess, {...field} autoComplete="off" placeholder="Description (optional)" - className="placeholder:text-primary/40 min-h-[24px] resize-none overflow-y-auto border-none p-1.5 text-xs font-medium shadow-none focus-visible:outline-none focus-visible:ring-0" + className="min-h-[24px] resize-none overflow-y-auto border-none p-1.5 text-xs font-medium shadow-none placeholder:text-primary/40 focus-visible:outline-none focus-visible:ring-0" onInput={e => { const target = e.target as HTMLTextAreaElement target.style.height = "auto" @@ -416,10 +424,10 @@ const LinkForm = React.forwardRef(({ onSuccess,
- -
diff --git a/web/components/routes/link/form/schema.ts b/web/components/routes/link/form/schema.ts index ace8a90d..1ed6b98b 100644 --- a/web/components/routes/link/form/schema.ts +++ b/web/components/routes/link/form/schema.ts @@ -1,16 +1,12 @@ import { z } from "zod" +import { isUrl } from "@/lib/utils" export const createLinkSchema = z.object({ - title: z - .string({ - message: "Please enter a valid title" - }) - .min(1, { - message: "Please enter a valid title" - }), + title: z.string().min(1, { message: "Title can't be empty" }), + originalUrl: z.string().refine(isUrl, { message: "Only links are allowed" }), description: z.string().optional(), topic: z.string().optional(), - isLink: z.boolean().default(false), + isLink: z.boolean().default(true), meta: z .object({ url: z.string(), diff --git a/web/components/routes/link/header.tsx b/web/components/routes/link/header.tsx index 879ee29a..339376ef 100644 --- a/web/components/routes/link/header.tsx +++ b/web/components/routes/link/header.tsx @@ -25,7 +25,6 @@ export const LinkHeader = () => { return ( <> - {/* Toggle and Title */}
@@ -50,18 +49,25 @@ export const LinkHeader = () => { } const Tabs = () => { + const [activeTab, setActiveTab] = React.useState(TABS[0]) + return (
{TABS.map(tab => ( - + setActiveTab(tab)} /> ))}
) } -const TabItem = ({ url, label }: TabItemProps) => { - const [isActive, setIsActive] = React.useState(false) +interface TabItemProps { + url: string + label: string + isActive: boolean + onClick: () => void +} +const TabItem = ({ url, label, isActive, onClick }: TabItemProps) => { return (
@@ -72,8 +78,7 @@ const TabItem = ({ url, label }: TabItemProps) => { type="button" variant="ghost" className={`gap-x-2 truncate text-sm ${isActive ? "bg-accent text-accent-foreground" : ""}`} - onClick={() => setIsActive(true)} - onBlur={() => setIsActive(false)} + onClick={onClick} > {label} diff --git a/web/components/routes/link/list-item.tsx b/web/components/routes/link/list-item.tsx index 551c92fa..419fb520 100644 --- a/web/components/routes/link/list-item.tsx +++ b/web/components/routes/link/list-item.tsx @@ -41,6 +41,7 @@ export const ListItem: React.FC = ({ }) => { const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: personalLink.id, disabled }) const formRef = React.useRef(null) + const [showDeleteIcon, setShowDeleteIcon] = React.useState(false) const style = { transform: CSS.Transform.toString(transform), @@ -77,8 +78,15 @@ export const ListItem: React.FC = ({ setEditId(null) } + // const handleRowClick = () => { + // console.log("Row clicked", personalLink.id) + // setEditId(personalLink.id) + // } const handleRowClick = () => { - console.log("Row clicked", personalLink.id) + setShowDeleteIcon(!showDeleteIcon) + } + + const handleDoubleClick = () => { setEditId(personalLink.id) } @@ -126,6 +134,7 @@ export const ListItem: React.FC = ({ "bg-muted/50": isFocused })} onClick={handleRowClick} + onDoubleClick={handleDoubleClick} >
@@ -177,13 +186,15 @@ export const ListItem: React.FC = ({
Topic Name - + {showDeleteIcon && ( + + )}
diff --git a/web/lib/schema/personal-link.ts b/web/lib/schema/personal-link.ts index 905a09e7..2d21a646 100644 --- a/web/lib/schema/personal-link.ts +++ b/web/lib/schema/personal-link.ts @@ -17,7 +17,7 @@ export class LinkMetadata extends CoMap { export class PersonalLink extends CoMap { title = co.string slug = co.string - description = nullable(co.string) + description = co.optional.string completed = co.boolean sequence = co.number isLink = co.boolean