mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
Move to TanStack Start from Next.js (#184)
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import * as React from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface LinkEditorProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
defaultUrl?: string
|
||||
defaultText?: string
|
||||
defaultIsNewTab?: boolean
|
||||
onSave: (url: string, text?: string, isNewTab?: boolean) => void
|
||||
}
|
||||
|
||||
export const LinkEditBlock = React.forwardRef<HTMLDivElement, LinkEditorProps>(
|
||||
({ onSave, defaultIsNewTab, defaultUrl, defaultText, className }, ref) => {
|
||||
const formRef = React.useRef<HTMLDivElement>(null)
|
||||
const [url, setUrl] = React.useState(defaultUrl || "")
|
||||
const [text, setText] = React.useState(defaultText || "")
|
||||
const [isNewTab, setIsNewTab] = React.useState(defaultIsNewTab || false)
|
||||
|
||||
const handleSave = React.useCallback(
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (formRef.current) {
|
||||
const isValid = Array.from(
|
||||
formRef.current.querySelectorAll("input"),
|
||||
).every((input) => input.checkValidity())
|
||||
|
||||
if (isValid) {
|
||||
onSave(url, text, isNewTab)
|
||||
} else {
|
||||
formRef.current.querySelectorAll("input").forEach((input) => {
|
||||
if (!input.checkValidity()) {
|
||||
input.reportValidity()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
[onSave, url, text, isNewTab],
|
||||
)
|
||||
|
||||
React.useImperativeHandle(ref, () => formRef.current as HTMLDivElement)
|
||||
|
||||
return (
|
||||
<div ref={formRef}>
|
||||
<div className={cn("space-y-4", className)}>
|
||||
<div className="space-y-1">
|
||||
<Label>URL</Label>
|
||||
<Input
|
||||
type="url"
|
||||
required
|
||||
placeholder="Enter URL"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label>Display Text (optional)</Label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter display text"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label>Open in New Tab</Label>
|
||||
<Switch checked={isNewTab} onCheckedChange={setIsNewTab} />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button type="button" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
LinkEditBlock.displayName = "LinkEditBlock"
|
||||
|
||||
export default LinkEditBlock
|
||||
@@ -0,0 +1,72 @@
|
||||
import * as React from "react"
|
||||
import type { Editor } from "@tiptap/react"
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
import type { toggleVariants } from "@/components/ui/toggle"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
import { Link2Icon } from "@radix-ui/react-icons"
|
||||
import { ToolbarButton } from "../toolbar-button"
|
||||
import { LinkEditBlock } from "./link-edit-block"
|
||||
|
||||
interface LinkEditPopoverProps extends VariantProps<typeof toggleVariants> {
|
||||
editor: Editor
|
||||
}
|
||||
|
||||
const LinkEditPopover = ({ editor, size, variant }: LinkEditPopoverProps) => {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
const { from, to } = editor.state.selection
|
||||
const text = editor.state.doc.textBetween(from, to, " ")
|
||||
|
||||
const onSetLink = React.useCallback(
|
||||
(url: string, text?: string, openInNewTab?: boolean) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.extendMarkRange("link")
|
||||
.insertContent({
|
||||
type: "text",
|
||||
text: text || url,
|
||||
marks: [
|
||||
{
|
||||
type: "link",
|
||||
attrs: {
|
||||
href: url,
|
||||
target: openInNewTab ? "_blank" : "",
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.setLink({ href: url })
|
||||
.run()
|
||||
|
||||
editor.commands.enter()
|
||||
},
|
||||
[editor],
|
||||
)
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<ToolbarButton
|
||||
isActive={editor.isActive("link")}
|
||||
tooltip="Link"
|
||||
aria-label="Insert link"
|
||||
disabled={editor.isActive("codeBlock")}
|
||||
size={size}
|
||||
variant={variant}
|
||||
>
|
||||
<Link2Icon className="size-5" />
|
||||
</ToolbarButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full min-w-80" align="start" side="bottom">
|
||||
<LinkEditBlock onSave={onSetLink} defaultText={text} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export { LinkEditPopover }
|
||||
@@ -0,0 +1,77 @@
|
||||
import * as React from "react"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { ToolbarButton } from "../toolbar-button"
|
||||
import {
|
||||
CopyIcon,
|
||||
ExternalLinkIcon,
|
||||
LinkBreak2Icon,
|
||||
} from "@radix-ui/react-icons"
|
||||
|
||||
interface LinkPopoverBlockProps {
|
||||
url: string
|
||||
onClear: () => void
|
||||
onEdit: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||
}
|
||||
|
||||
export const LinkPopoverBlock: React.FC<LinkPopoverBlockProps> = ({
|
||||
url,
|
||||
onClear,
|
||||
onEdit,
|
||||
}) => {
|
||||
const [copyTitle, setCopyTitle] = React.useState<string>("Copy")
|
||||
|
||||
const handleCopy = React.useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault()
|
||||
navigator.clipboard
|
||||
.writeText(url)
|
||||
.then(() => {
|
||||
setCopyTitle("Copied!")
|
||||
setTimeout(() => setCopyTitle("Copy"), 1000)
|
||||
})
|
||||
.catch(console.error)
|
||||
},
|
||||
[url],
|
||||
)
|
||||
|
||||
const handleOpenLink = React.useCallback(() => {
|
||||
window.open(url, "_blank", "noopener,noreferrer")
|
||||
}, [url])
|
||||
|
||||
return (
|
||||
<div className="flex h-10 overflow-hidden rounded bg-background p-2 shadow-lg">
|
||||
<div className="inline-flex items-center gap-1">
|
||||
<ToolbarButton
|
||||
tooltip="Edit link"
|
||||
onClick={onEdit}
|
||||
className="w-auto px-2"
|
||||
>
|
||||
Edit link
|
||||
</ToolbarButton>
|
||||
<Separator orientation="vertical" />
|
||||
<ToolbarButton
|
||||
tooltip="Open link in a new tab"
|
||||
onClick={handleOpenLink}
|
||||
>
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
</ToolbarButton>
|
||||
<Separator orientation="vertical" />
|
||||
<ToolbarButton tooltip="Clear link" onClick={onClear}>
|
||||
<LinkBreak2Icon className="size-4" />
|
||||
</ToolbarButton>
|
||||
<Separator orientation="vertical" />
|
||||
<ToolbarButton
|
||||
tooltip={copyTitle}
|
||||
onClick={handleCopy}
|
||||
tooltipOptions={{
|
||||
onPointerDownOutside: (e) => {
|
||||
if (e.target === e.currentTarget) e.preventDefault()
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="size-4" />
|
||||
</ToolbarButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user