mirror of
https://github.com/linsa-io/linsa.git
synced 2026-04-24 01:08:36 +02:00
chore(start): update to alpha and fix editor image removed (#187)
* chore(start): update to alpha and fix editor image removed * chore: fix image editor
This commit is contained in:
@@ -1,36 +1,38 @@
|
|||||||
import { defineConfig } from "@tanstack/start/config"
|
import { defineConfig } from "@tanstack/start/config"
|
||||||
import tsConfigPaths from "vite-tsconfig-paths"
|
import tsConfigPaths from "vite-tsconfig-paths"
|
||||||
|
|
||||||
const is_tauri = process.env.TAURI_ENV_TARGET_TRIPLE !== undefined;
|
const is_tauri = process.env.TAURI_ENV_TARGET_TRIPLE !== undefined
|
||||||
|
|
||||||
let config = is_tauri ? defineConfig({
|
const config = is_tauri
|
||||||
vite: {
|
? defineConfig({
|
||||||
envPrefix: ['VITE_', 'TAURI_ENV_*'],
|
vite: {
|
||||||
build: {
|
envPrefix: ["VITE_", "TAURI_ENV_*"],
|
||||||
target:
|
build: {
|
||||||
process.env.TAURI_ENV_PLATFORM == 'windows'
|
target:
|
||||||
? 'chrome105'
|
process.env.TAURI_ENV_PLATFORM == "windows"
|
||||||
: 'safari13',
|
? "chrome105"
|
||||||
minify: !process.env.TAURI_ENV_DEBUG ? 'esbuild' : false,
|
: "safari13",
|
||||||
sourcemap: !!process.env.TAURI_ENV_DEBUG,
|
minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false,
|
||||||
},
|
sourcemap: !!process.env.TAURI_ENV_DEBUG,
|
||||||
plugins: [
|
},
|
||||||
tsConfigPaths({
|
plugins: [
|
||||||
projects: ["./tsconfig.json"],
|
tsConfigPaths({
|
||||||
}),
|
projects: ["./tsconfig.json"],
|
||||||
],
|
}),
|
||||||
},
|
],
|
||||||
server: {
|
},
|
||||||
preset: "static"
|
server: {
|
||||||
}
|
preset: "static",
|
||||||
}) : defineConfig({
|
},
|
||||||
vite: {
|
})
|
||||||
plugins: [
|
: defineConfig({
|
||||||
tsConfigPaths({
|
vite: {
|
||||||
projects: ["./tsconfig.json"],
|
plugins: [
|
||||||
}),
|
tsConfigPaths({
|
||||||
],
|
projects: ["./tsconfig.json"],
|
||||||
},
|
}),
|
||||||
})
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export default config;
|
export default config
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { createServerFn } from "@tanstack/start"
|
import { createServerFn } from "@tanstack/start"
|
||||||
import { get } from "ronin"
|
import { get } from "ronin"
|
||||||
|
|
||||||
export const getFeatureFlag = createServerFn(
|
export const getFeatureFlag = createServerFn({ method: "GET" })
|
||||||
"GET",
|
.validator((input: string) => input)
|
||||||
async (data: { name: string }) => {
|
.handler(async ({ data }) => {
|
||||||
const response = await get.featureFlag.with({
|
const response = await get.featureFlag.with({
|
||||||
name: data.name,
|
name: data,
|
||||||
})
|
})
|
||||||
|
|
||||||
return response
|
return response
|
||||||
},
|
})
|
||||||
)
|
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ import { createRouter } from "./router"
|
|||||||
|
|
||||||
const router = createRouter()
|
const router = createRouter()
|
||||||
|
|
||||||
hydrateRoot(document.getElementById("root")!, <StartClient router={router} />)
|
hydrateRoot(document!, <StartClient router={router} />)
|
||||||
|
|||||||
@@ -32,11 +32,12 @@ import { Editor } from "@tiptap/react"
|
|||||||
import { createServerFn } from "@tanstack/start"
|
import { createServerFn } from "@tanstack/start"
|
||||||
import { clerkClient, getAuth } from "@clerk/tanstack-start/server"
|
import { clerkClient, getAuth } from "@clerk/tanstack-start/server"
|
||||||
import { create } from "ronin"
|
import { create } from "ronin"
|
||||||
|
import { getWebRequest } from "vinxi/http"
|
||||||
|
|
||||||
export const sendFeedbackFn = createServerFn(
|
export const sendFeedbackFn = createServerFn({ method: "POST" })
|
||||||
"POST",
|
.validator((content: string) => content)
|
||||||
async (data: { content: string }, { request }) => {
|
.handler(async ({ data }) => {
|
||||||
const auth = await getAuth(request)
|
const auth = await getAuth(getWebRequest())
|
||||||
|
|
||||||
if (!auth.userId) {
|
if (!auth.userId) {
|
||||||
throw new Error("User not authenticated")
|
throw new Error("User not authenticated")
|
||||||
@@ -46,11 +47,10 @@ export const sendFeedbackFn = createServerFn(
|
|||||||
telemetry: { disabled: true },
|
telemetry: { disabled: true },
|
||||||
}).users.getUser(auth.userId)
|
}).users.getUser(auth.userId)
|
||||||
await create.feedback.with({
|
await create.feedback.with({
|
||||||
message: data.content,
|
message: data,
|
||||||
emailFrom: user.emailAddresses[0].emailAddress,
|
emailFrom: user.emailAddresses[0].emailAddress,
|
||||||
})
|
})
|
||||||
},
|
})
|
||||||
)
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
content: z.string().min(1, {
|
content: z.string().min(1, {
|
||||||
@@ -83,7 +83,7 @@ export function Feedback() {
|
|||||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
try {
|
try {
|
||||||
setIsPending(true)
|
setIsPending(true)
|
||||||
await sendFeedbackFn(values)
|
await sendFeedbackFn({ data: values.content })
|
||||||
|
|
||||||
form.reset({ content: "" })
|
form.reset({ content: "" })
|
||||||
editorRef.current?.commands.clearContent()
|
editorRef.current?.commands.clearContent()
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const JournalSection: React.FC = () => {
|
|||||||
setIsFetching(true)
|
setIsFetching(true)
|
||||||
|
|
||||||
if (isLoaded && isSignedIn) {
|
if (isLoaded && isSignedIn) {
|
||||||
const response = await getFeatureFlag({ name: "JOURNAL" })
|
const response = await getFeatureFlag({ data: "JOURNAL" })
|
||||||
|
|
||||||
if (
|
if (
|
||||||
user?.emailAddresses.some((email) =>
|
user?.emailAddresses.some((email) =>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const TaskSection: React.FC = () => {
|
|||||||
setIsFetching(true)
|
setIsFetching(true)
|
||||||
|
|
||||||
if (isLoaded && isSignedIn) {
|
if (isLoaded && isSignedIn) {
|
||||||
const response = await getFeatureFlag({ name: "TASK" })
|
const response = await getFeatureFlag({ data: "TASK" })
|
||||||
|
|
||||||
if (
|
if (
|
||||||
user?.emailAddresses.some((email) =>
|
user?.emailAddresses.some((email) =>
|
||||||
|
|||||||
@@ -5,15 +5,9 @@ import {
|
|||||||
ScrollRestoration,
|
ScrollRestoration,
|
||||||
createRootRouteWithContext,
|
createRootRouteWithContext,
|
||||||
} from "@tanstack/react-router"
|
} from "@tanstack/react-router"
|
||||||
import {
|
import { createServerFn, Meta, Scripts } from "@tanstack/start"
|
||||||
Body,
|
|
||||||
createServerFn,
|
|
||||||
Head,
|
|
||||||
Html,
|
|
||||||
Meta,
|
|
||||||
Scripts,
|
|
||||||
} from "@tanstack/start"
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import { getWebRequest } from "vinxi/http"
|
||||||
import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary.js"
|
import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary.js"
|
||||||
import { NotFound } from "~/components/NotFound.js"
|
import { NotFound } from "~/components/NotFound.js"
|
||||||
import { seo } from "~/lib/utils/seo"
|
import { seo } from "~/lib/utils/seo"
|
||||||
@@ -28,8 +22,8 @@ export const TanStackRouterDevtools =
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const fetchClerkAuth = createServerFn("GET", async (_, ctx) => {
|
export const fetchClerkAuth = createServerFn().handler(async () => {
|
||||||
const auth = await getAuth(ctx.request)
|
const auth = await getAuth(getWebRequest())
|
||||||
|
|
||||||
return auth
|
return auth
|
||||||
})
|
})
|
||||||
@@ -37,44 +31,49 @@ export const fetchClerkAuth = createServerFn("GET", async (_, ctx) => {
|
|||||||
export const Route = createRootRouteWithContext<{
|
export const Route = createRootRouteWithContext<{
|
||||||
auth?: ReturnType<typeof getAuth> | null
|
auth?: ReturnType<typeof getAuth> | null
|
||||||
}>()({
|
}>()({
|
||||||
meta: () => [
|
head() {
|
||||||
{
|
return {
|
||||||
charSet: "utf-8",
|
meta: [
|
||||||
},
|
{
|
||||||
{
|
charSet: "utf-8",
|
||||||
name: "viewport",
|
},
|
||||||
content: "width=device-width, initial-scale=1",
|
{
|
||||||
},
|
name: "viewport",
|
||||||
...seo({
|
content: "width=device-width, initial-scale=1",
|
||||||
title: "Learn Anything",
|
},
|
||||||
description:
|
...seo({
|
||||||
"Discover and learn about any topic with Learn-Anything. Our free, comprehensive platform connects you to the best resources for every subject. Start learning today!",
|
title: "Learn Anything",
|
||||||
keywords:
|
description:
|
||||||
"learn anything, online learning, free education, educational resources, self-study, knowledge discovery, topic exploration, skill development, lifelong learning",
|
"Discover and learn about any topic with Learn-Anything. Our free, comprehensive platform connects you to the best resources for every subject. Start learning today!",
|
||||||
}),
|
keywords:
|
||||||
],
|
"learn anything, online learning, free education, educational resources, self-study, knowledge discovery, topic exploration, skill development, lifelong learning",
|
||||||
links: () => [
|
}),
|
||||||
{ rel: "stylesheet", href: appCss },
|
],
|
||||||
{
|
links: [
|
||||||
rel: "apple-touch-icon",
|
{ rel: "stylesheet", href: appCss },
|
||||||
sizes: "180x180",
|
{
|
||||||
href: "/apple-touch-icon.png",
|
rel: "apple-touch-icon",
|
||||||
},
|
sizes: "180x180",
|
||||||
{
|
href: "/apple-touch-icon.png",
|
||||||
rel: "icon",
|
},
|
||||||
type: "image/png",
|
{
|
||||||
sizes: "32x32",
|
rel: "icon",
|
||||||
href: "/favicon-32x32.png",
|
type: "image/png",
|
||||||
},
|
sizes: "32x32",
|
||||||
{
|
href: "/favicon-32x32.png",
|
||||||
rel: "icon",
|
},
|
||||||
type: "image/png",
|
{
|
||||||
sizes: "16x16",
|
rel: "icon",
|
||||||
href: "/favicon-16x16.png",
|
type: "image/png",
|
||||||
},
|
sizes: "16x16",
|
||||||
{ rel: "manifest", href: "/site.webmanifest", color: "#fffff" },
|
href: "/favicon-16x16.png",
|
||||||
{ rel: "icon", href: "/favicon.ico" },
|
},
|
||||||
],
|
{ rel: "manifest", href: "/site.webmanifest", color: "#fffff" },
|
||||||
|
{ rel: "icon", href: "/favicon.ico" },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
beforeLoad: async (ctx) => {
|
beforeLoad: async (ctx) => {
|
||||||
try {
|
try {
|
||||||
// Handle explicit null auth (logged out state)
|
// Handle explicit null auth (logged out state)
|
||||||
@@ -121,20 +120,17 @@ function RootComponent() {
|
|||||||
|
|
||||||
function RootDocument({ children }: { children: React.ReactNode }) {
|
function RootDocument({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<Html>
|
<html lang="en" suppressHydrationWarning={true}>
|
||||||
<Head>
|
<head>
|
||||||
<Meta />
|
<Meta />
|
||||||
</Head>
|
</head>
|
||||||
<Body>
|
<body>
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
<React.Suspense>
|
|
||||||
<TanStackRouterDevtools position="bottom-right" />
|
|
||||||
</React.Suspense>
|
|
||||||
|
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
|
<TanStackRouterDevtools position="bottom-right" />
|
||||||
<Scripts />
|
<Scripts />
|
||||||
</Body>
|
</body>
|
||||||
</Html>
|
</html>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router"
|
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router"
|
||||||
|
|
||||||
export const Route = createFileRoute("/_layout/(auth)/_auth")({
|
export const Route = createFileRoute("/_layout/(auth)/_auth")({
|
||||||
beforeLoad({ context }) {
|
beforeLoad: async ({ context }) => {
|
||||||
if (context.auth?.userId) {
|
const auth = await context.auth
|
||||||
|
if (auth?.userId) {
|
||||||
throw redirect({ to: "/links", replace: true })
|
throw redirect({ to: "/links", replace: true })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ export const LinkItem = React.forwardRef<HTMLDivElement, LinkItemProps>(
|
|||||||
<Link
|
<Link
|
||||||
to={ensureUrlProtocol(link.url)}
|
to={ensureUrlProtocol(link.url)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||||
className="text-xs text-muted-foreground hover:text-primary"
|
className="text-xs text-muted-foreground hover:text-primary"
|
||||||
>
|
>
|
||||||
<span className="line-clamp-1">{link.url}</span>
|
<span className="line-clamp-1">{link.url}</span>
|
||||||
|
|||||||
@@ -191,7 +191,9 @@ export const LinkItem = React.forwardRef<HTMLDivElement, LinkItemProps>(
|
|||||||
<Link
|
<Link
|
||||||
to={ensureUrlProtocol(personalLink.url)}
|
to={ensureUrlProtocol(personalLink.url)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e: React.MouseEvent<HTMLAnchorElement>) =>
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
className="mr-1 truncate text-xs hover:text-primary"
|
className="mr-1 truncate text-xs hover:text-primary"
|
||||||
>
|
>
|
||||||
{personalLink.url}
|
{personalLink.url}
|
||||||
|
|||||||
@@ -44,70 +44,72 @@ export const globalLinkFormExceptionRefsAtom = atom<
|
|||||||
React.RefObject<HTMLElement>[]
|
React.RefObject<HTMLElement>[]
|
||||||
>([])
|
>([])
|
||||||
|
|
||||||
export const getMetadata = createServerFn("GET", async (url: string) => {
|
export const getMetadata = createServerFn()
|
||||||
if (!url) {
|
.validator((url: string) => url)
|
||||||
return new Response('Missing "url" query parameter', {
|
.handler(async ({ data: url }) => {
|
||||||
status: 400,
|
if (!url) {
|
||||||
})
|
return new Response('Missing "url" query parameter', {
|
||||||
}
|
status: 400,
|
||||||
|
})
|
||||||
const result = urlSchema.safeParse(decodeURIComponent(url))
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(
|
|
||||||
result.error.issues.map((issue) => issue.message).join(", "),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
url = ensureUrlProtocol(decodeURIComponent(url))
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
headers: {
|
|
||||||
"User-Agent":
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.text()
|
const result = urlSchema.safeParse(decodeURIComponent(url))
|
||||||
const $ = cheerio.load(data)
|
|
||||||
|
|
||||||
const metadata: Metadata = {
|
if (!result.success) {
|
||||||
title:
|
throw new Error(
|
||||||
$("title").text() ||
|
result.error.issues.map((issue) => issue.message).join(", "),
|
||||||
$('meta[property="og:title"]').attr("content") ||
|
)
|
||||||
DEFAULT_VALUES.TITLE,
|
|
||||||
description:
|
|
||||||
$('meta[name="description"]').attr("content") ||
|
|
||||||
$('meta[property="og:description"]').attr("content") ||
|
|
||||||
DEFAULT_VALUES.DESCRIPTION,
|
|
||||||
icon:
|
|
||||||
$('link[rel="icon"]').attr("href") ||
|
|
||||||
$('link[rel="shortcut icon"]').attr("href") ||
|
|
||||||
DEFAULT_VALUES.FAVICON,
|
|
||||||
url: url,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metadata.icon && !metadata.icon.startsWith("http")) {
|
url = ensureUrlProtocol(decodeURIComponent(url))
|
||||||
metadata.icon = new URL(metadata.icon, url).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadata
|
try {
|
||||||
} catch (error) {
|
const response = await fetch(url, {
|
||||||
console.error("Error fetching metadata:", error)
|
headers: {
|
||||||
const defaultMetadata: Metadata = {
|
"User-Agent":
|
||||||
title: DEFAULT_VALUES.TITLE,
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||||
description: DEFAULT_VALUES.DESCRIPTION,
|
},
|
||||||
icon: DEFAULT_VALUES.FAVICON,
|
})
|
||||||
url: url,
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.text()
|
||||||
|
const $ = cheerio.load(data)
|
||||||
|
|
||||||
|
const metadata: Metadata = {
|
||||||
|
title:
|
||||||
|
$("title").text() ||
|
||||||
|
$('meta[property="og:title"]').attr("content") ||
|
||||||
|
DEFAULT_VALUES.TITLE,
|
||||||
|
description:
|
||||||
|
$('meta[name="description"]').attr("content") ||
|
||||||
|
$('meta[property="og:description"]').attr("content") ||
|
||||||
|
DEFAULT_VALUES.DESCRIPTION,
|
||||||
|
icon:
|
||||||
|
$('link[rel="icon"]').attr("href") ||
|
||||||
|
$('link[rel="shortcut icon"]').attr("href") ||
|
||||||
|
DEFAULT_VALUES.FAVICON,
|
||||||
|
url: url,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.icon && !metadata.icon.startsWith("http")) {
|
||||||
|
metadata.icon = new URL(metadata.icon, url).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching metadata:", error)
|
||||||
|
const defaultMetadata: Metadata = {
|
||||||
|
title: DEFAULT_VALUES.TITLE,
|
||||||
|
description: DEFAULT_VALUES.DESCRIPTION,
|
||||||
|
icon: DEFAULT_VALUES.FAVICON,
|
||||||
|
url: url,
|
||||||
|
}
|
||||||
|
return defaultMetadata
|
||||||
}
|
}
|
||||||
return defaultMetadata
|
})
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
interface LinkFormProps extends React.ComponentPropsWithoutRef<"form"> {
|
interface LinkFormProps extends React.ComponentPropsWithoutRef<"form"> {
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
@@ -196,7 +198,7 @@ export const LinkForm: React.FC<LinkFormProps> = ({
|
|||||||
const fetchMetadata = async (url: string) => {
|
const fetchMetadata = async (url: string) => {
|
||||||
setIsFetching(true)
|
setIsFetching(true)
|
||||||
try {
|
try {
|
||||||
const data = await getMetadata(encodeURIComponent(url))
|
const data = await getMetadata({ data: encodeURIComponent(url) })
|
||||||
setUrlFetched(data.url)
|
setUrlFetched(data.url)
|
||||||
form.setValue("url", data.url, {
|
form.setValue("url", data.url, {
|
||||||
shouldValidate: true,
|
shouldValidate: true,
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const Route = createFileRoute("/_layout/_pages/_protected/tasks/")({
|
|||||||
// throw new Error("Unauthorized")
|
// throw new Error("Unauthorized")
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// const flag = await getFeatureFlag({ name: "TASK" })
|
// const flag = await getFeatureFlag({ data: "TASK" })
|
||||||
// const canAccess = context.user?.emailAddresses.some((email) =>
|
// const canAccess = context.user?.emailAddresses.some((email) =>
|
||||||
// flag?.emails.includes(email.emailAddress),
|
// flag?.emails.includes(email.emailAddress),
|
||||||
// )
|
// )
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const SearchItem: React.FC<SearchItemProps> = ({
|
|||||||
<div className="group flex items-center justify-between">
|
<div className="group flex items-center justify-between">
|
||||||
<Link
|
<Link
|
||||||
to={href}
|
to={href}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||||
className="text-sm font-medium hover:text-primary hover:opacity-70"
|
className="text-sm font-medium hover:text-primary hover:opacity-70"
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
@@ -57,7 +57,7 @@ const SearchItem: React.FC<SearchItemProps> = ({
|
|||||||
{subtitle && (
|
{subtitle && (
|
||||||
<Link
|
<Link
|
||||||
to={href}
|
to={href}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||||
className="ml-2 truncate text-xs text-muted-foreground hover:underline"
|
className="ml-2 truncate text-xs text-muted-foreground hover:underline"
|
||||||
>
|
>
|
||||||
{subtitle}
|
{subtitle}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix"
|
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clerk/tanstack-start": "^0.4.26",
|
"@clerk/tanstack-start": "^0.4.28",
|
||||||
"@clerk/themes": "^2.1.43",
|
"@clerk/themes": "^2.1.45",
|
||||||
"@dnd-kit/core": "^6.1.0",
|
"@dnd-kit/core": "^6.1.0",
|
||||||
"@dnd-kit/modifiers": "^7.0.0",
|
"@dnd-kit/modifiers": "^7.0.0",
|
||||||
"@dnd-kit/sortable": "^8.0.0",
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"@radix-ui/react-dialog": "^1.1.2",
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-dismissable-layer": "^1.1.1",
|
"@radix-ui/react-dismissable-layer": "^1.1.1",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||||
"@radix-ui/react-icons": "^1.3.1",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-popover": "^1.1.2",
|
"@radix-ui/react-popover": "^1.1.2",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||||
@@ -39,10 +39,10 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.4",
|
"@radix-ui/react-tooltip": "^1.1.4",
|
||||||
"@tanstack/react-router": "^1.81.4",
|
"@tanstack/react-router": "^1.82.0",
|
||||||
"@tanstack/react-virtual": "^3.10.9",
|
"@tanstack/react-virtual": "^3.10.9",
|
||||||
"@tanstack/router-zod-adapter": "^1.81.4",
|
"@tanstack/router-zod-adapter": "^1.81.5",
|
||||||
"@tanstack/start": "^1.81.4",
|
"@tanstack/start": "^1.82.0",
|
||||||
"@tiptap/core": "^2.9.1",
|
"@tiptap/core": "^2.9.1",
|
||||||
"@tiptap/extension-code-block-lowlight": "^2.9.1",
|
"@tiptap/extension-code-block-lowlight": "^2.9.1",
|
||||||
"@tiptap/extension-color": "^2.9.1",
|
"@tiptap/extension-color": "^2.9.1",
|
||||||
@@ -65,11 +65,11 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "1.0.2",
|
"cmdk": "1.0.2",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"framer-motion": "^11.11.15",
|
"framer-motion": "^11.11.17",
|
||||||
"jazz-browser-media-images": "^0.8.20",
|
"jazz-browser-media-images": "^0.8.22",
|
||||||
"jazz-react": "^0.8.20",
|
"jazz-react": "^0.8.22",
|
||||||
"jazz-react-auth-clerk": "^0.8.20",
|
"jazz-react-auth-clerk": "^0.8.22",
|
||||||
"jazz-tools": "^0.8.19",
|
"jazz-tools": "^0.8.21",
|
||||||
"jotai": "^2.10.2",
|
"jotai": "^2.10.2",
|
||||||
"lowlight": "^3.1.0",
|
"lowlight": "^3.1.0",
|
||||||
"lucide-react": "^0.446.0",
|
"lucide-react": "^0.446.0",
|
||||||
@@ -92,9 +92,9 @@
|
|||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ronin/learn-anything": "^0.0.0-3461302210631",
|
"@ronin/learn-anything": "^0.0.0-3461795804931",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@tanstack/router-devtools": "^1.81.4",
|
"@tanstack/router-devtools": "^1.82.0",
|
||||||
"@types/node": "^22.9.0",
|
"@types/node": "^22.9.0",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.14",
|
"prettier-plugin-tailwindcss": "^0.5.14",
|
||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "^3.4.15",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"vite-tsconfig-paths": "^5.1.2"
|
"vite-tsconfig-paths": "^5.1.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createServerFn } from "@tanstack/start"
|
|||||||
import { create, drop } from "ronin"
|
import { create, drop } from "ronin"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { ALLOWED_FILE_TYPES, MAX_FILE_SIZE } from "./constants"
|
import { ALLOWED_FILE_TYPES, MAX_FILE_SIZE } from "./constants"
|
||||||
|
import { getWebRequest } from "vinxi/http"
|
||||||
|
|
||||||
const ImageRuleSchema = z.object({
|
const ImageRuleSchema = z.object({
|
||||||
file: z
|
file: z
|
||||||
@@ -16,10 +17,10 @@ const ImageRuleSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const storeImageFn = createServerFn(
|
export const storeImageFn = createServerFn({ method: "POST" })
|
||||||
"POST",
|
.validator((data: FormData) => data)
|
||||||
async (data: FormData, { request }) => {
|
.handler(async ({ data }) => {
|
||||||
const auth = await getAuth(request)
|
const auth = await getAuth(getWebRequest())
|
||||||
|
|
||||||
if (!auth.userId) {
|
if (!auth.userId) {
|
||||||
throw new Error("Unauthorized")
|
throw new Error("Unauthorized")
|
||||||
@@ -37,18 +38,16 @@ export const storeImageFn = createServerFn(
|
|||||||
})
|
})
|
||||||
|
|
||||||
return { fileModel }
|
return { fileModel }
|
||||||
},
|
})
|
||||||
)
|
|
||||||
|
|
||||||
export const deleteImageFn = createServerFn(
|
export const deleteImageFn = createServerFn({ method: "POST" })
|
||||||
"POST",
|
.validator((id: string) => id)
|
||||||
async (data: { id: string }, { request }) => {
|
.handler(async ({ data }) => {
|
||||||
const auth = await getAuth(request)
|
const auth = await getAuth(getWebRequest())
|
||||||
|
|
||||||
if (!auth.userId) {
|
if (!auth.userId) {
|
||||||
throw new Error("Unauthorized")
|
throw new Error("Unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
await drop.image.with.id(data.id)
|
await drop.image.with.id(data)
|
||||||
},
|
})
|
||||||
)
|
|
||||||
|
|||||||
@@ -202,7 +202,6 @@ export const ImageViewBlock: React.FC<NodeViewProps> = ({
|
|||||||
|
|
||||||
updateAttributes(normalizedData)
|
updateAttributes(normalizedData)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Image upload failed:", error)
|
|
||||||
setImageState((prev) => ({
|
setImageState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
error: true,
|
error: true,
|
||||||
@@ -240,7 +239,7 @@ export const ImageViewBlock: React.FC<NodeViewProps> = ({
|
|||||||
>
|
>
|
||||||
<div className="h-full contain-paint">
|
<div className="h-full contain-paint">
|
||||||
<div className="relative h-full">
|
<div className="relative h-full">
|
||||||
{!imageState.imageLoaded && !imageState.error && (
|
{imageState.isServerUploading && !imageState.error && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<Spinner className="size-7" />
|
<Spinner className="size-7" />
|
||||||
</div>
|
</div>
|
||||||
@@ -279,6 +278,8 @@ export const ImageViewBlock: React.FC<NodeViewProps> = ({
|
|||||||
onError={handleImageError}
|
onError={handleImageError}
|
||||||
onLoad={handleImageLoad}
|
onLoad={handleImageLoad}
|
||||||
alt={node.attrs.alt || ""}
|
alt={node.attrs.alt || ""}
|
||||||
|
title={node.attrs.title || ""}
|
||||||
|
id={node.attrs.id}
|
||||||
/>
|
/>
|
||||||
</ControlledZoom>
|
</ControlledZoom>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import type { ImageOptions } from "@tiptap/extension-image"
|
import type { ImageOptions } from "@tiptap/extension-image"
|
||||||
import { Image as TiptapImage } from "@tiptap/extension-image"
|
import { Image as TiptapImage } from "@tiptap/extension-image"
|
||||||
import type { Editor } from "@tiptap/react"
|
import type { Editor } from "@tiptap/react"
|
||||||
import type { Node } from "@tiptap/pm/model"
|
|
||||||
import { ReactNodeViewRenderer } from "@tiptap/react"
|
import { ReactNodeViewRenderer } from "@tiptap/react"
|
||||||
import { ImageViewBlock } from "./components/image-view-block"
|
import { ImageViewBlock } from "./components/image-view-block"
|
||||||
import {
|
import {
|
||||||
FileError,
|
|
||||||
FileValidationOptions,
|
|
||||||
filterFiles,
|
filterFiles,
|
||||||
isUrl,
|
|
||||||
randomId,
|
randomId,
|
||||||
|
type FileError,
|
||||||
|
type FileValidationOptions,
|
||||||
} from "@shared/editor/lib/utils"
|
} from "@shared/editor/lib/utils"
|
||||||
|
import { ReplaceStep } from "@tiptap/pm/transform"
|
||||||
|
import type { Attrs } from "@tiptap/pm/model"
|
||||||
|
|
||||||
type ImageAction = "download" | "copyImage" | "copyLink"
|
type ImageAction = "download" | "copyImage" | "copyLink"
|
||||||
|
|
||||||
@@ -23,11 +23,6 @@ interface ImageActionProps extends DownloadImageCommandProps {
|
|||||||
action: ImageAction
|
action: ImageAction
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageInfo = {
|
|
||||||
id?: string | number
|
|
||||||
src: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UploadReturnType =
|
export type UploadReturnType =
|
||||||
| string
|
| string
|
||||||
| {
|
| {
|
||||||
@@ -39,18 +34,18 @@ interface CustomImageOptions
|
|||||||
extends ImageOptions,
|
extends ImageOptions,
|
||||||
Omit<FileValidationOptions, "allowBase64"> {
|
Omit<FileValidationOptions, "allowBase64"> {
|
||||||
uploadFn?: (file: File, editor: Editor) => Promise<UploadReturnType>
|
uploadFn?: (file: File, editor: Editor) => Promise<UploadReturnType>
|
||||||
onImageRemoved?: (props: ImageInfo) => void
|
onImageRemoved?: (props: Attrs) => void
|
||||||
onActionSuccess?: (props: ImageActionProps) => void
|
onActionSuccess?: (props: ImageActionProps) => void
|
||||||
onActionError?: (error: Error, props: ImageActionProps) => void
|
onActionError?: (error: Error, props: ImageActionProps) => void
|
||||||
customDownloadImage?: (
|
downloadImage?: (
|
||||||
props: ImageActionProps,
|
props: ImageActionProps,
|
||||||
options: CustomImageOptions,
|
options: CustomImageOptions,
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
customCopyImage?: (
|
copyImage?: (
|
||||||
props: ImageActionProps,
|
props: ImageActionProps,
|
||||||
options: CustomImageOptions,
|
options: CustomImageOptions,
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
customCopyLink?: (
|
copyLink?: (
|
||||||
props: ImageActionProps,
|
props: ImageActionProps,
|
||||||
options: CustomImageOptions,
|
options: CustomImageOptions,
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
@@ -133,7 +128,7 @@ const saveImage = async (
|
|||||||
URL.revokeObjectURL(imageURL)
|
URL.revokeObjectURL(imageURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultDownloadImage = async (
|
const downloadImage = async (
|
||||||
props: ImageActionProps,
|
props: ImageActionProps,
|
||||||
options: CustomImageOptions,
|
options: CustomImageOptions,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
@@ -149,7 +144,7 @@ const defaultDownloadImage = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultCopyImage = async (
|
const copyImage = async (
|
||||||
props: ImageActionProps,
|
props: ImageActionProps,
|
||||||
options: CustomImageOptions,
|
options: CustomImageOptions,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
@@ -164,7 +159,7 @@ const defaultCopyImage = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultCopyLink = async (
|
const copyLink = async (
|
||||||
props: ImageActionProps,
|
props: ImageActionProps,
|
||||||
options: CustomImageOptions,
|
options: CustomImageOptions,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
@@ -187,23 +182,34 @@ export const Image = TiptapImage.extend<CustomImageOptions>({
|
|||||||
maxFileSize: 0,
|
maxFileSize: 0,
|
||||||
uploadFn: undefined,
|
uploadFn: undefined,
|
||||||
onToggle: undefined,
|
onToggle: undefined,
|
||||||
|
downloadImage: undefined,
|
||||||
|
copyImage: undefined,
|
||||||
|
copyLink: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
...this.parent?.(),
|
src: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
alt: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
id: {
|
id: {
|
||||||
default: undefined,
|
default: null,
|
||||||
},
|
},
|
||||||
width: {
|
width: {
|
||||||
default: undefined,
|
default: null,
|
||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
default: undefined,
|
default: null,
|
||||||
},
|
},
|
||||||
fileName: {
|
fileName: {
|
||||||
default: undefined,
|
default: null,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -228,10 +234,12 @@ export const Image = TiptapImage.extend<CustomImageOptions>({
|
|||||||
validImages.map((image) => {
|
validImages.map((image) => {
|
||||||
if (image.src instanceof File) {
|
if (image.src instanceof File) {
|
||||||
const blobUrl = URL.createObjectURL(image.src)
|
const blobUrl = URL.createObjectURL(image.src)
|
||||||
|
const id = randomId()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: this.type.name,
|
type: this.type.name,
|
||||||
attrs: {
|
attrs: {
|
||||||
id: randomId(),
|
id,
|
||||||
src: blobUrl,
|
src: blobUrl,
|
||||||
alt: image.alt,
|
alt: image.alt,
|
||||||
title: image.title,
|
title: image.title,
|
||||||
@@ -256,96 +264,81 @@ export const Image = TiptapImage.extend<CustomImageOptions>({
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
|
|
||||||
downloadImage: (attrs) => () => {
|
downloadImage: (attrs) => () => {
|
||||||
const downloadFunc =
|
const downloadFunc = this.options.downloadImage || downloadImage
|
||||||
this.options.customDownloadImage || defaultDownloadImage
|
|
||||||
void downloadFunc({ ...attrs, action: "download" }, this.options)
|
void downloadFunc({ ...attrs, action: "download" }, this.options)
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
|
||||||
copyImage: (attrs) => () => {
|
copyImage: (attrs) => () => {
|
||||||
const copyImageFunc = this.options.customCopyImage || defaultCopyImage
|
const copyImageFunc = this.options.copyImage || copyImage
|
||||||
void copyImageFunc({ ...attrs, action: "copyImage" }, this.options)
|
void copyImageFunc({ ...attrs, action: "copyImage" }, this.options)
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
|
||||||
copyLink: (attrs) => () => {
|
copyLink: (attrs) => () => {
|
||||||
const copyLinkFunc = this.options.customCopyLink || defaultCopyLink
|
const copyLinkFunc = this.options.copyLink || copyLink
|
||||||
void copyLinkFunc({ ...attrs, action: "copyLink" }, this.options)
|
void copyLinkFunc({ ...attrs, action: "copyLink" }, this.options)
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
toggleImage: () => (props) => {
|
|
||||||
const input = document.createElement("input")
|
|
||||||
input.type = "file"
|
|
||||||
input.accept = this.options.allowedMimeTypes.join(",")
|
|
||||||
input.onchange = () => {
|
|
||||||
const files = input.files
|
|
||||||
if (!files) return
|
|
||||||
|
|
||||||
const [validImages, errors] = filterFiles(Array.from(files), {
|
toggleImage:
|
||||||
allowedMimeTypes: this.options.allowedMimeTypes,
|
() =>
|
||||||
maxFileSize: this.options.maxFileSize,
|
({ editor }) => {
|
||||||
allowBase64: this.options.allowBase64,
|
const input = document.createElement("input")
|
||||||
})
|
input.type = "file"
|
||||||
|
input.accept = this.options.allowedMimeTypes.join(",")
|
||||||
|
input.onchange = () => {
|
||||||
|
const files = input.files
|
||||||
|
if (!files) return
|
||||||
|
|
||||||
|
const [validImages, errors] = filterFiles(Array.from(files), {
|
||||||
|
allowedMimeTypes: this.options.allowedMimeTypes,
|
||||||
|
maxFileSize: this.options.maxFileSize,
|
||||||
|
allowBase64: this.options.allowBase64,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (errors.length > 0 && this.options.onValidationError) {
|
||||||
|
this.options.onValidationError(errors)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validImages.length === 0) return false
|
||||||
|
|
||||||
|
if (this.options.onToggle) {
|
||||||
|
this.options.onToggle(
|
||||||
|
editor,
|
||||||
|
validImages,
|
||||||
|
editor.state.selection.from,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (errors.length > 0 && this.options.onValidationError) {
|
|
||||||
this.options.onValidationError(errors)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validImages.length === 0) return false
|
input.click()
|
||||||
|
return true
|
||||||
if (this.options.onToggle) {
|
},
|
||||||
this.options.onToggle(
|
|
||||||
props.editor,
|
|
||||||
validImages,
|
|
||||||
props.editor.state.selection.from,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input.click()
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onTransaction({ transaction }) {
|
onTransaction({ transaction }) {
|
||||||
if (!transaction.docChanged) return
|
transaction.steps.forEach((step) => {
|
||||||
|
if (step instanceof ReplaceStep && step.slice.size === 0) {
|
||||||
|
const deletedPages = transaction.before.content.cut(step.from, step.to)
|
||||||
|
|
||||||
const oldDoc = transaction.before
|
deletedPages.forEach((node) => {
|
||||||
const newDoc = transaction.doc
|
if (node.type.name === "image") {
|
||||||
|
const attrs = node.attrs
|
||||||
|
|
||||||
const oldImages = new Map<string, ImageInfo>()
|
if (attrs.src.startsWith("blob:")) {
|
||||||
const newImages = new Map<string, ImageInfo>()
|
URL.revokeObjectURL(attrs.src)
|
||||||
|
}
|
||||||
|
|
||||||
const addToMap = (node: Node, map: Map<string, ImageInfo>) => {
|
this.options.onImageRemoved?.(attrs)
|
||||||
if (node.type.name === "image") {
|
}
|
||||||
const attrs = node.attrs
|
})
|
||||||
if (attrs.src) {
|
|
||||||
const key = attrs.id || attrs.src
|
|
||||||
map.set(key, { id: attrs.id, src: attrs.src })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
oldDoc.descendants((node) => addToMap(node, oldImages))
|
|
||||||
newDoc.descendants((node) => addToMap(node, newImages))
|
|
||||||
|
|
||||||
oldImages.forEach((imageInfo, key) => {
|
|
||||||
if (!newImages.has(key)) {
|
|
||||||
if (imageInfo.src.startsWith("blob:")) {
|
|
||||||
URL.revokeObjectURL(imageInfo.src)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!imageInfo.src.startsWith("blob:") &&
|
|
||||||
!imageInfo.src.startsWith("data:") &&
|
|
||||||
isUrl(imageInfo.src)
|
|
||||||
) {
|
|
||||||
this.options.onImageRemoved?.({
|
|
||||||
id: imageInfo.id,
|
|
||||||
src: imageInfo.src,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { Content, EditorOptions, UseEditorOptions } from "@tiptap/react"
|
|||||||
import { useEditor } from "@tiptap/react"
|
import { useEditor } from "@tiptap/react"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useThrottleCallback } from "@shared/hooks/use-throttle-callback"
|
import { useThrottleCallback } from "@shared/hooks/use-throttle-callback"
|
||||||
import { getOutput } from "@shared/editor/lib/utils"
|
import { getOutput, randomId } from "@shared/editor/lib/utils"
|
||||||
import { StarterKit } from "@shared/editor/extensions/starter-kit"
|
import { StarterKit } from "@shared/editor/extensions/starter-kit"
|
||||||
import { TaskList } from "@shared/editor/extensions/task-list"
|
import { TaskList } from "@shared/editor/extensions/task-list"
|
||||||
import { TaskItem } from "@shared/editor/extensions/task-item"
|
import { TaskItem } from "@shared/editor/extensions/task-item"
|
||||||
@@ -80,25 +80,39 @@ const createExtensions = ({ placeholder }: { placeholder: string }) => [
|
|||||||
formData.append("width", dimensions.width.toString())
|
formData.append("width", dimensions.width.toString())
|
||||||
formData.append("height", dimensions.height.toString())
|
formData.append("height", dimensions.height.toString())
|
||||||
|
|
||||||
const store = await storeImageFn(formData)
|
const store = await storeImageFn({ data: formData })
|
||||||
|
|
||||||
return { id: store.fileModel.id, src: store.fileModel.content.src }
|
return { id: store.fileModel.id, src: store.fileModel.content.src }
|
||||||
},
|
},
|
||||||
onImageRemoved({ id }) {
|
onImageRemoved(props) {
|
||||||
if (id) {
|
if (props.id) {
|
||||||
deleteImageFn({ id: id?.toString() })
|
deleteImageFn({ data: props.id })
|
||||||
}
|
|
||||||
|
|
||||||
toast.success("Image removed", {
|
console.log("Image removed", props)
|
||||||
position: "bottom-right",
|
|
||||||
description: "Image removed successfully",
|
toast.success("Image removed", {
|
||||||
})
|
position: "bottom-right",
|
||||||
|
description: "Image removed successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onToggle(editor, files, pos) {
|
onToggle(editor, files, pos) {
|
||||||
files.forEach((file) =>
|
editor.commands.insertContentAt(
|
||||||
editor.commands.insertContentAt(pos, {
|
pos,
|
||||||
type: "image",
|
files.map((image) => {
|
||||||
attrs: { src: URL.createObjectURL(file) },
|
const blobUrl = URL.createObjectURL(image)
|
||||||
|
const id = randomId()
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "image",
|
||||||
|
attrs: {
|
||||||
|
id,
|
||||||
|
src: blobUrl,
|
||||||
|
alt: image.name,
|
||||||
|
title: image.name,
|
||||||
|
fileName: image.name,
|
||||||
|
},
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const ImageEditBlock = ({
|
|||||||
formData.append("file", files[0])
|
formData.append("file", files[0])
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await storeImageFn(formData)
|
const response = await storeImageFn({ data: formData })
|
||||||
|
|
||||||
editor
|
editor
|
||||||
.chain()
|
.chain()
|
||||||
|
|||||||
Reference in New Issue
Block a user