Merge branch 'main' of github.com:learn-anything/learn-anything.xyz

This commit is contained in:
Aslam H
2024-08-11 05:40:46 +07:00
6 changed files with 96 additions and 76 deletions
BIN
View File
Binary file not shown.
+61 -53
View File
@@ -121,20 +121,24 @@ interface LinkFormProps extends React.ComponentPropsWithoutRef<"form"> {
} }
const LinkForm = React.forwardRef<HTMLFormElement, LinkFormProps>(({ onSuccess, onCancel, personalLink }, ref) => { const LinkForm = React.forwardRef<HTMLFormElement, LinkFormProps>(({ onSuccess, onCancel, personalLink }, ref) => {
const selectedLink = useCoState(PersonalLink, personalLink?.id)
const [isFetching, setIsFetching] = useState(false) const [isFetching, setIsFetching] = useState(false)
const { me } = useAccount() const { me } = useAccount()
const form = useForm<LinkFormValues>({ const form = useForm<LinkFormValues>({
resolver: zodResolver(createLinkSchema), 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 title = form.watch("title")
const [inputValue, setInputValue] = useState("")
const [originalLink, setOriginalLink] = useState<string>("") const [originalLink, setOriginalLink] = useState<string>("")
const [linkEntered, setLinkEntered] = useState(false) const [linkValidation, setLinkValidation] = useState<string | null>(null)
const [invalidLink, setInvalidLink] = useState(false)
const [showLink, setShowLink] = useState(false)
const [debouncedText, setDebouncedText] = useState<string>("") const [debouncedText, setDebouncedText] = useState<string>("")
useDebounce(() => setDebouncedText(title), 300, [title]) useDebounce(() => setDebouncedText(title), 300, [title])
const [showStatusOptions, setShowStatusOptions] = useState(false) const [showStatusOptions, setShowStatusOptions] = useState(false)
const [selectedStatus, setSelectedStatus] = useState<string | null>(null) const [selectedStatus, setSelectedStatus] = useState<string | null>(null)
@@ -166,11 +170,35 @@ const LinkForm = React.forwardRef<HTMLFormElement, LinkFormProps>(({ onSuccess,
} }
}, [selectedLink, form]) }, [selectedLink, form])
const changeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setInputValue(value)
form.setValue("title", value)
}
const pressEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
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(() => { useEffect(() => {
const fetchMetadata = async (url: string) => { const fetchMetadata = async (url: string) => {
setIsFetching(true) setIsFetching(true)
try { 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") if (!res.ok) throw new Error("Failed to fetch metadata")
const data = await res.json() const data = await res.json()
form.setValue("isLink", true) form.setValue("isLink", true)
@@ -188,19 +216,10 @@ const LinkForm = React.forwardRef<HTMLFormElement, LinkFormProps>(({ onSuccess,
setIsFetching(false) setIsFetching(false)
} }
} }
if (showLink && !invalidLink && LibIsUrl(form.getValues("title").toLowerCase())) {
const lowerText = debouncedText.toLowerCase() fetchMetadata(ensureUrlProtocol(form.getValues("title").toLowerCase()))
if (linkEntered && LibIsUrl(lowerText)) {
fetchMetadata(ensureUrlProtocol(lowerText))
} }
}, [debouncedText, form, linkEntered]) }, [showLink, invalidLink, form])
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && LibIsUrl(e.currentTarget.value.toLowerCase())) {
e.preventDefault()
setLinkEntered(true)
}
}
const onSubmit = (values: LinkFormValues) => { const onSubmit = (values: LinkFormValues) => {
if (isFetching) return if (isFetching) return
@@ -221,11 +240,9 @@ const LinkForm = React.forwardRef<HTMLFormElement, LinkFormProps>(({ onSuccess,
selectedLink.description = values.description ?? "" selectedLink.description = values.description ?? ""
selectedLink.isLink = values.isLink selectedLink.isLink = values.isLink
if (selectedLink.meta) { if (values.isLink && values.meta) {
Object.assign(selectedLink.meta, values.meta) linkMetadata = LinkMetadata.create(values.meta, { owner: me._owner })
} }
// toast.success("Todo updated")
} else { } else {
const newPersonalLink = PersonalLink.create( const newPersonalLink = PersonalLink.create(
{ {
@@ -252,26 +269,26 @@ const LinkForm = React.forwardRef<HTMLFormElement, LinkFormProps>(({ onSuccess,
} }
} }
const handleCancel: () => void = () => { const undoEditing: () => void = () => {
form.reset(DEFAULT_FORM_VALUES) form.reset(DEFAULT_FORM_VALUES)
onCancel?.() onCancel?.()
} }
return ( return (
<div className="p-3 transition-all"> <div className="p-3 transition-all">
<div className="bg-muted/50 rounded-md border"> <div className="rounded-md border bg-muted/50">
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="relative min-w-0 flex-1" ref={ref}> <form onSubmit={form.handleSubmit(onSubmit)} className="relative min-w-0 flex-1" ref={ref}>
<div className="flex flex-row p-3"> <div className="flex flex-row p-3">
<div className="flex flex-auto flex-col gap-1.5"> <div className="flex flex-auto flex-col gap-1.5">
<div className="flex flex-row items-start justify-between"> <div className="flex flex-row items-start justify-between">
<div className="flex grow flex-row items-center gap-1.5"> <div className="flex grow flex-row items-center gap-1.5">
<Button {/* <Button
type="button" type="button"
variant="secondary" variant="secondary"
size="icon" size="icon"
aria-label="Choose icon" aria-label="Choose icon"
className="text-primary/60 size-7" className="size-7 text-primary/60"
> >
{form.watch("isLink") ? ( {form.watch("isLink") ? (
<Image <Image
@@ -284,7 +301,7 @@ const LinkForm = React.forwardRef<HTMLFormElement, LinkFormProps>(({ onSuccess,
) : ( ) : (
<BoxIcon size={16} /> <BoxIcon size={16} />
)} )}
</Button> </Button> */}
<FormField <FormField
control={form.control} control={form.control}
@@ -295,45 +312,36 @@ const LinkForm = React.forwardRef<HTMLFormElement, LinkFormProps>(({ onSuccess,
<FormControl> <FormControl>
<Input <Input
{...field} {...field}
value={inputValue}
autoComplete="off" autoComplete="off"
maxLength={100} maxLength={100}
autoFocus autoFocus
placeholder="Paste a link or write a link" placeholder="Paste a link or write a link"
className="placeholder:text-primary/40 h-6 border-none p-1.5 font-medium focus-visible:outline-none focus-visible:ring-0" className={cn(
onKeyDown={handleKeyDown} "h-6 border-none p-1.5 font-medium placeholder:text-primary/40 focus-visible:outline-none focus-visible:ring-0",
invalidLink ? "text-red-500" : ""
)}
onKeyDown={pressEnter}
onChange={changeInput}
/> />
</FormControl> </FormControl>
</FormItem> </FormItem>
)} )}
/> />
<span className="mr-5 max-w-[200px] truncate text-xs text-white/60"> {showLink && (
{linkEntered <span className={cn("mr-5 max-w-[200px] truncate text-xs", invalidLink ? "text-red-500" : "")}>
? originalLink {invalidLink ? "Only links are allowed" : linkValidation || originalLink || ""}
: LibIsUrl(form.watch("title").toLowerCase()) </span>
? 'Press "Enter" to confirm URL' )}
: ""}
</span>
</div> </div>
<div className="flex min-w-0 shrink-0 cursor-pointer select-none flex-row"> <div className="flex min-w-0 shrink-0 cursor-pointer select-none flex-row">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild></DropdownMenuTrigger>
{/* <Button
size="icon"
type="button"
variant="ghost"
className="size-7 gap-x-2 text-sm"
>
<EllipsisIcon
size={16}
className="text-primary/60"
/>
</Button> */}
</DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel> <DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem className="group"> <DropdownMenuItem className="group">
<Trash2Icon size={16} className="text-destructive mr-2 group-hover:text-red-500" /> <Trash2Icon size={16} className="mr-2 text-destructive group-hover:text-red-500" />
<span>Delete</span> <span>Delete</span>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
@@ -393,7 +401,7 @@ const LinkForm = React.forwardRef<HTMLFormElement, LinkFormProps>(({ onSuccess,
{...field} {...field}
autoComplete="off" autoComplete="off"
placeholder="Description (optional)" 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 => { onInput={e => {
const target = e.target as HTMLTextAreaElement const target = e.target as HTMLTextAreaElement
target.style.height = "auto" target.style.height = "auto"
@@ -416,10 +424,10 @@ const LinkForm = React.forwardRef<HTMLFormElement, LinkFormProps>(({ onSuccess,
</div> </div>
<div className="flex w-auto items-center justify-end"> <div className="flex w-auto items-center justify-end">
<div className="flex min-w-0 shrink-0 cursor-pointer select-none flex-row gap-x-2"> <div className="flex min-w-0 shrink-0 cursor-pointer select-none flex-row gap-x-2">
<Button size="sm" type="button" variant="ghost" onClick={handleCancel}> <Button size="sm" type="button" variant="ghost" onClick={undoEditing}>
Cancel Cancel
</Button> </Button>
<Button size="sm" disabled={isFetching}> <Button size="sm" type="submit" disabled={isFetching}>
Save Save
</Button> </Button>
</div> </div>
+4 -8
View File
@@ -1,16 +1,12 @@
import { z } from "zod" import { z } from "zod"
import { isUrl } from "@/lib/utils"
export const createLinkSchema = z.object({ export const createLinkSchema = z.object({
title: z title: z.string().min(1, { message: "Title can't be empty" }),
.string({ originalUrl: z.string().refine(isUrl, { message: "Only links are allowed" }),
message: "Please enter a valid title"
})
.min(1, {
message: "Please enter a valid title"
}),
description: z.string().optional(), description: z.string().optional(),
topic: z.string().optional(), topic: z.string().optional(),
isLink: z.boolean().default(false), isLink: z.boolean().default(true),
meta: z meta: z
.object({ .object({
url: z.string(), url: z.string(),
+11 -6
View File
@@ -25,7 +25,6 @@ export const LinkHeader = () => {
return ( return (
<> <>
<ContentHeader className="p-4"> <ContentHeader className="p-4">
{/* Toggle and Title */}
<div className="flex min-w-0 shrink-0 items-center gap-1.5"> <div className="flex min-w-0 shrink-0 items-center gap-1.5">
<SidebarToggleButton /> <SidebarToggleButton />
<div className="flex min-h-0 items-center"> <div className="flex min-h-0 items-center">
@@ -50,18 +49,25 @@ export const LinkHeader = () => {
} }
const Tabs = () => { const Tabs = () => {
const [activeTab, setActiveTab] = React.useState(TABS[0])
return ( return (
<div className="bg-secondary/50 flex items-baseline overflow-x-hidden rounded-md"> <div className="bg-secondary/50 flex items-baseline overflow-x-hidden rounded-md">
{TABS.map(tab => ( {TABS.map(tab => (
<TabItem key={tab} url="#" label={tab} /> <TabItem key={tab} url="#" label={tab} isActive={activeTab === tab} onClick={() => setActiveTab(tab)} />
))} ))}
</div> </div>
) )
} }
const TabItem = ({ url, label }: TabItemProps) => { interface TabItemProps {
const [isActive, setIsActive] = React.useState(false) url: string
label: string
isActive: boolean
onClick: () => void
}
const TabItem = ({ url, label, isActive, onClick }: TabItemProps) => {
return ( return (
<div tabIndex={-1} className="rounded-md"> <div tabIndex={-1} className="rounded-md">
<div className="flex flex-row"> <div className="flex flex-row">
@@ -72,8 +78,7 @@ const TabItem = ({ url, label }: TabItemProps) => {
type="button" type="button"
variant="ghost" variant="ghost"
className={`gap-x-2 truncate text-sm ${isActive ? "bg-accent text-accent-foreground" : ""}`} className={`gap-x-2 truncate text-sm ${isActive ? "bg-accent text-accent-foreground" : ""}`}
onClick={() => setIsActive(true)} onClick={onClick}
onBlur={() => setIsActive(false)}
> >
{label} {label}
</Button> </Button>
+19 -8
View File
@@ -41,6 +41,7 @@ export const ListItem: React.FC<ListItemProps> = ({
}) => { }) => {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: personalLink.id, disabled }) const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: personalLink.id, disabled })
const formRef = React.useRef<HTMLFormElement>(null) const formRef = React.useRef<HTMLFormElement>(null)
const [showDeleteIcon, setShowDeleteIcon] = React.useState(false)
const style = { const style = {
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
@@ -77,8 +78,15 @@ export const ListItem: React.FC<ListItemProps> = ({
setEditId(null) setEditId(null)
} }
// const handleRowClick = () => {
// console.log("Row clicked", personalLink.id)
// setEditId(personalLink.id)
// }
const handleRowClick = () => { const handleRowClick = () => {
console.log("Row clicked", personalLink.id) setShowDeleteIcon(!showDeleteIcon)
}
const handleDoubleClick = () => {
setEditId(personalLink.id) setEditId(personalLink.id)
} }
@@ -126,6 +134,7 @@ export const ListItem: React.FC<ListItemProps> = ({
"bg-muted/50": isFocused "bg-muted/50": isFocused
})} })}
onClick={handleRowClick} onClick={handleRowClick}
onDoubleClick={handleDoubleClick}
> >
<div className="flex grow justify-between gap-x-6 px-6 max-lg:px-4"> <div className="flex grow justify-between gap-x-6 px-6 max-lg:px-4">
<div className="flex min-w-0 items-center gap-x-4"> <div className="flex min-w-0 items-center gap-x-4">
@@ -177,13 +186,15 @@ export const ListItem: React.FC<ListItemProps> = ({
<div className="flex shrink-0 items-center gap-x-4"> <div className="flex shrink-0 items-center gap-x-4">
<Badge variant="secondary">Topic Name</Badge> <Badge variant="secondary">Topic Name</Badge>
<Button {showDeleteIcon && (
size="icon" <Button
className="text-destructive h-auto w-auto bg-transparent hover:bg-transparent hover:text-red-500" size="icon"
onClick={e => handleDelete(e, personalLink)} className="text-destructive h-auto w-auto bg-transparent hover:bg-transparent hover:text-red-500"
> onClick={e => handleDelete(e, personalLink)}
<Trash2Icon size={16} /> >
</Button> <Trash2Icon size={16} />
</Button>
)}
</div> </div>
</div> </div>
</li> </li>
+1 -1
View File
@@ -17,7 +17,7 @@ export class LinkMetadata extends CoMap {
export class PersonalLink extends CoMap { export class PersonalLink extends CoMap {
title = co.string title = co.string
slug = co.string slug = co.string
description = nullable(co.string) description = co.optional.string
completed = co.boolean completed = co.boolean
sequence = co.number sequence = co.number
isLink = co.boolean isLink = co.boolean