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:
125
web/shared/minimal-tiptap/components/image/image-edit-block.tsx
Normal file
125
web/shared/minimal-tiptap/components/image/image-edit-block.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import * as React from "react"
|
||||
import type { Editor } from "@tiptap/react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { storeImageFn } from "@shared/actions"
|
||||
import { ZodError } from "zod"
|
||||
|
||||
interface ImageEditBlockProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
editor: Editor
|
||||
close: () => void
|
||||
}
|
||||
|
||||
const ImageEditBlock = ({
|
||||
editor,
|
||||
className,
|
||||
close,
|
||||
...props
|
||||
}: ImageEditBlockProps) => {
|
||||
const fileInputRef = React.useRef<HTMLInputElement>(null)
|
||||
const [link, setLink] = React.useState<string>("")
|
||||
const [isUploading, setIsUploading] = React.useState<boolean>(false)
|
||||
const [error, setError] = React.useState<string | null>(null)
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
fileInputRef.current?.click()
|
||||
}
|
||||
|
||||
const handleLink = () => {
|
||||
editor.chain().focus().setImage({ src: link }).run()
|
||||
close()
|
||||
}
|
||||
|
||||
const handleFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files
|
||||
if (!files || files.length === 0) return
|
||||
|
||||
setIsUploading(true)
|
||||
setError(null)
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append("file", files[0])
|
||||
|
||||
try {
|
||||
const response = await storeImageFn(formData)
|
||||
|
||||
editor
|
||||
.chain()
|
||||
.setImage({ src: response.fileModel.content.src })
|
||||
.focus()
|
||||
.run()
|
||||
close()
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
try {
|
||||
const errors = JSON.parse(error.message)
|
||||
if (errors.body.name === "ZodError") {
|
||||
setError(
|
||||
(errors.body as ZodError).issues
|
||||
.map((issue) => issue.message)
|
||||
.join(", "),
|
||||
)
|
||||
} else {
|
||||
setError(error.message)
|
||||
}
|
||||
} catch (parseError) {
|
||||
setError(error.message)
|
||||
}
|
||||
} else {
|
||||
setError("An unknown error occurred")
|
||||
}
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleLink()
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className={cn("space-y-6", className)} {...props}>
|
||||
<div className="space-y-1">
|
||||
<Label>Attach an image link</Label>
|
||||
<div className="flex">
|
||||
<Input
|
||||
type="url"
|
||||
required
|
||||
placeholder="https://example.com"
|
||||
value={link}
|
||||
className="grow"
|
||||
onChange={(e) => setLink(e.target.value)}
|
||||
/>
|
||||
<Button type="submit" className="ml-2 inline-block">
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="w-full" onClick={handleClick} disabled={isUploading}>
|
||||
{isUploading ? "Uploading..." : "Upload from your computer"}
|
||||
</Button>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/gif,image/webp"
|
||||
ref={fileInputRef}
|
||||
className="hidden"
|
||||
onChange={handleFile}
|
||||
/>
|
||||
{error && (
|
||||
<div className="text-destructive text-sm bg-destructive/10 p-2 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export { ImageEditBlock }
|
||||
@@ -0,0 +1,50 @@
|
||||
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 { ImageIcon } from "@radix-ui/react-icons"
|
||||
import { ToolbarButton } from "../toolbar-button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { ImageEditBlock } from "./image-edit-block"
|
||||
|
||||
interface ImageEditDialogProps extends VariantProps<typeof toggleVariants> {
|
||||
editor: Editor
|
||||
}
|
||||
|
||||
const ImageEditDialog = ({ editor, size, variant }: ImageEditDialogProps) => {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<ToolbarButton
|
||||
isActive={editor.isActive("image")}
|
||||
tooltip="Image"
|
||||
aria-label="Image"
|
||||
size={size}
|
||||
variant={variant}
|
||||
>
|
||||
<ImageIcon className="size-5" />
|
||||
</ToolbarButton>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Select image</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Upload an image from your computer
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ImageEditBlock editor={editor} close={() => setOpen(false)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export { ImageEditDialog }
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ToolbarButton } from "../toolbar-button"
|
||||
import { TrashIcon } from "@radix-ui/react-icons"
|
||||
|
||||
const ImagePopoverBlock = ({
|
||||
onRemove,
|
||||
}: {
|
||||
onRemove: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||
}) => {
|
||||
const handleRemove = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault()
|
||||
onRemove(e)
|
||||
}
|
||||
|
||||
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="Remove" onClick={handleRemove}>
|
||||
<TrashIcon className="size-4" />
|
||||
</ToolbarButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { ImagePopoverBlock }
|
||||
Reference in New Issue
Block a user