feat: suppot remote image url
This commit is contained in:
parent
116e116332
commit
2b456637e9
24
README.md
24
README.md
@ -1,3 +1,5 @@
|
|||||||
|
# Inke
|
||||||
|
|
||||||
<a href="https://www.npmjs.org/package/inkejs" target='_blank'>
|
<a href="https://www.npmjs.org/package/inkejs" target='_blank'>
|
||||||
<img src="https://img.shields.io/npm/v/inkejs">
|
<img src="https://img.shields.io/npm/v/inkejs">
|
||||||
</a>
|
</a>
|
||||||
@ -11,8 +13,6 @@
|
|||||||
<img src="https://badgen.net/https/inke.app/api/status" alt="status"/>
|
<img src="https://badgen.net/https/inke.app/api/status" alt="status"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
# Inke
|
|
||||||
|
|
||||||
Inke is a Notion-style WYSIWYG editor with AI-powered autocompletions.
|
Inke is a Notion-style WYSIWYG editor with AI-powered autocompletions.
|
||||||
|
|
||||||
See live demo: [inke-web](https://inke.app)
|
See live demo: [inke-web](https://inke.app)
|
||||||
@ -54,18 +54,18 @@ export default function App() {
|
|||||||
|
|
||||||
The `Editor` is a React component that takes in the following props:
|
The `Editor` is a React component that takes in the following props:
|
||||||
|
|
||||||
| Prop | Type | Description | Default |
|
| Prop | Type | Description | Default |
|
||||||
| --------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
|
| --------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `completionApi` | `string` | The API route to use for the OpenAI completion API. | `/api/generate` |
|
| `completionApi` | `string` | The API route to use for the OpenAI completion API. | `/api/generate` |
|
||||||
| `className` | `string` | Editor container classname. | `"relative min-h-[500px] w-full max-w-screen-lg border-stone-200 bg-white sm:mb-[calc(20vh)] sm:rounded-lg sm:border sm:shadow-lg"` |
|
| `className` | `string` | Editor container classname. | `"relative min-h-[500px] w-full max-w-screen-lg border-stone-200 bg-white sm:mb-[calc(20vh)] sm:rounded-lg sm:border sm:shadow-lg"` |
|
||||||
| `defaultValue` | `JSONContent` or `string` | The default value to use for the editor. | [`defaultEditorContent`](https://github.com/yesmore/inke/blob/main/packages/core/src/ui/editor/default-content.tsx) |
|
| `defaultValue` | `JSONContent` or `string` | The default value to use for the editor. | [`defaultEditorContent`](https://github.com/yesmore/inke/blob/main/packages/core/src/ui/editor/default-content.tsx) |
|
||||||
| `extensions` | `Extension[]` | A list of extensions to use for the editor, in addition to the [default Novel extensions](https://github.com/yesmore/inke/blob/main/packages/core/src/ui/editor/extensions/index.tsx). | `[]` |
|
| `extensions` | `Extension[]` | A list of extensions to use for the editor, in addition to the [default Novel extensions](https://github.com/yesmore/inke/blob/main/packages/core/src/ui/editor/extensions/index.tsx). | `[]` |
|
||||||
| `editorProps` | `EditorProps` | Props to pass to the underlying Tiptap editor, in addition to the [default Novel editor props](https://github.com/yesmore/inke/blob/main/packages/core/src/ui/editor/props.ts). | `{}` |
|
| `editorProps` | `EditorProps` | Props to pass to the underlying Tiptap editor, in addition to the [default Novel editor props](https://github.com/yesmore/inke/blob/main/packages/core/src/ui/editor/props.ts). | `{}` |
|
||||||
| `onUpdate` | `(editor?: Editor) => void` | A callback function that is called whenever the editor is updated. | `() => {}` |
|
| `onUpdate` | `(editor?: Editor) => void` | A callback function that is called whenever the editor is updated. | `() => {}` |
|
||||||
| `onDebouncedUpdate` | `(editor?: Editor) => void` | A callback function that is called whenever the editor is updated, but only after the defined debounce duration. | `() => {}` |
|
| `onDebouncedUpdate` | `(editor?: Editor) => void` | A callback function that is called whenever the editor is updated, but only after the defined debounce duration. | `() => {}` |
|
||||||
| `debounceDuration` | `number` | The duration (in milliseconds) to debounce the `onDebouncedUpdate` callback. | `750` |
|
| `debounceDuration` | `number` | The duration (in milliseconds) to debounce the `onDebouncedUpdate` callback. | `750` |
|
||||||
| `storageKey` | `string` | The key to use for storing the editor's value in local storage. | `novel__content` |
|
| `storageKey` | `string` | The key to use for storing the editor's value in local storage. | `novel__content` |
|
||||||
| `disableLocalStorage` | `boolean` | Enabling this option will prevent read/write content from/to local storage. | `false` |
|
| `disableLocalStorage` | `boolean` | Enabling this option will prevent read/write content from/to local storage. | `false` |
|
||||||
|
|
||||||
> **Note**: Make sure to define an API endpoint that matches the `completionApi` prop (default is `/api/generate`). This is needed for the AI autocompletions to work. Here's an example: https://github.com/yesmore/inke/blob/main/apps/web/app/api/generate/route.ts
|
> **Note**: Make sure to define an API endpoint that matches the `completionApi` prop (default is `/api/generate`). This is needed for the AI autocompletions to work. Here's an example: https://github.com/yesmore/inke/blob/main/apps/web/app/api/generate/route.ts
|
||||||
|
|
||||||
|
@ -0,0 +1,95 @@
|
|||||||
|
# Inke
|
||||||
|
|
||||||
|
<a href="https://www.npmjs.org/package/inkejs" target='_blank'>
|
||||||
|
<img src="https://img.shields.io/npm/v/inkejs">
|
||||||
|
</a>
|
||||||
|
<a href="https://npmcharts.com/compare/inkejs?minimal=true" target='_blank'>
|
||||||
|
<img src="https://img.shields.io/npm/dt/inkejs.svg">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/yesmore/inke/blob/master/LICENSE">
|
||||||
|
<img src="https://img.shields.io/github/license/yesmore/inke?label=license&logo=github&color=f80&logoColor=fff" alt="License" />
|
||||||
|
</a>
|
||||||
|
<a href="https://inke.app">
|
||||||
|
<img src="https://badgen.net/https/inke.app/api/status" alt="status"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
Inke is a Notion-style WYSIWYG editor with AI-powered autocompletions.
|
||||||
|
|
||||||
|
See live demo: [inke-web](https://inke.app)
|
||||||
|
|
||||||
|
<img alt="Inke is a Notion-style WYSIWYG editor with AI-powered autocompletions." src="https://inke.app/desktop.png">
|
||||||
|
|
||||||
|
# Install Inke
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install inkejs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting Up Locally
|
||||||
|
|
||||||
|
To set up Inke locally, you'll need to clone the repository and set up the following environment variables:
|
||||||
|
|
||||||
|
- `OPENAI_API_KEY` – your OpenAI API key (you can get one [here](https://platform.openai.com/account/api-keys))
|
||||||
|
- `BLOB_READ_WRITE_TOKEN` – your Vercel Blob read/write token (currently [still in beta](https://vercel.com/docs/storage/vercel-blob/quickstart#quickstart), but feel free to [sign up on this form](https://vercel.fyi/blob-beta) for access)
|
||||||
|
|
||||||
|
If you've deployed this to Vercel, you can also use [`vc env pull`](https://vercel.com/docs/cli/env#exporting-development-environment-variables) to pull the environment variables from your Vercel project.
|
||||||
|
|
||||||
|
To run the app locally, you can run the following commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm i
|
||||||
|
pnpm build
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, you can use it in your code like this:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { Editor } from "inkejs";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return <Editor />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `Editor` is a React component that takes in the following props:
|
||||||
|
|
||||||
|
| Prop | Type | Description | Default |
|
||||||
|
| --------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `completionApi` | `string` | The API route to use for the OpenAI completion API. | `/api/generate` |
|
||||||
|
| `className` | `string` | Editor container classname. | `"relative min-h-[500px] w-full max-w-screen-lg border-stone-200 bg-white sm:mb-[calc(20vh)] sm:rounded-lg sm:border sm:shadow-lg"` |
|
||||||
|
| `defaultValue` | `JSONContent` or `string` | The default value to use for the editor. | [`defaultEditorContent`](https://github.com/yesmore/inke/blob/main/packages/core/src/ui/editor/default-content.tsx) |
|
||||||
|
| `extensions` | `Extension[]` | A list of extensions to use for the editor, in addition to the [default Novel extensions](https://github.com/yesmore/inke/blob/main/packages/core/src/ui/editor/extensions/index.tsx). | `[]` |
|
||||||
|
| `editorProps` | `EditorProps` | Props to pass to the underlying Tiptap editor, in addition to the [default Novel editor props](https://github.com/yesmore/inke/blob/main/packages/core/src/ui/editor/props.ts). | `{}` |
|
||||||
|
| `onUpdate` | `(editor?: Editor) => void` | A callback function that is called whenever the editor is updated. | `() => {}` |
|
||||||
|
| `onDebouncedUpdate` | `(editor?: Editor) => void` | A callback function that is called whenever the editor is updated, but only after the defined debounce duration. | `() => {}` |
|
||||||
|
| `debounceDuration` | `number` | The duration (in milliseconds) to debounce the `onDebouncedUpdate` callback. | `750` |
|
||||||
|
| `storageKey` | `string` | The key to use for storing the editor's value in local storage. | `novel__content` |
|
||||||
|
| `disableLocalStorage` | `boolean` | Enabling this option will prevent read/write content from/to local storage. | `false` |
|
||||||
|
|
||||||
|
> **Note**: Make sure to define an API endpoint that matches the `completionApi` prop (default is `/api/generate`). This is needed for the AI autocompletions to work. Here's an example: https://github.com/yesmore/inke/blob/main/apps/web/app/api/generate/route.ts
|
||||||
|
|
||||||
|
## Deploy Your Own
|
||||||
|
|
||||||
|
You can deploy your own version of Novel to Vercel with one click:
|
||||||
|
|
||||||
|
[![Deploy with Vercel](https://vercel.com/button)]()
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
Inke is built on the following stack:
|
||||||
|
|
||||||
|
- [Next.js](https://nextjs.org/) – framework
|
||||||
|
- [Tiptap](https://tiptap.dev/) – text editor
|
||||||
|
- [OpenAI](https://openai.com/) - AI completions
|
||||||
|
- [Vercel AI SDK](https://sdk.vercel.ai/docs) – AI library
|
||||||
|
- [Vercel](https://vercel.com) – deployments
|
||||||
|
- [TailwindCSS](https://tailwindcss.com/) – styles
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[![Star History Chart](https://api.star-history.com/svg?repos=yesmore/inke&type=Date)](https://star-history.com/#yesmore/inke&Date)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[Apache-2.0](./LICENSE) © [yesmore](https://github.com/yesmore)
|
@ -1,7 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "inke",
|
"name": "inkejs",
|
||||||
"version": "0.1.4",
|
"version": "0.0.5",
|
||||||
"description": "Notion-style WYSIWYG editor with AI-powered autocompletions",
|
"author": "yesmore",
|
||||||
|
"description": "A Notion-style WYSIWYG editor with AI-powered autocompletions. Built with Tiptap, OpenAI, and Vercel AI SDK.",
|
||||||
|
"keywords": [
|
||||||
|
"editor",
|
||||||
|
"markdown",
|
||||||
|
"openai",
|
||||||
|
"ai",
|
||||||
|
"nextjs",
|
||||||
|
"react"
|
||||||
|
],
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.mjs",
|
"module": "./dist/index.mjs",
|
||||||
@ -85,7 +94,6 @@
|
|||||||
"tsup": "^7.2.0",
|
"tsup": "^7.2.0",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4"
|
||||||
},
|
},
|
||||||
"author": "Steven Tey <stevensteel97@gmail.com>",
|
|
||||||
"homepage": "https://inke.app",
|
"homepage": "https://inke.app",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -93,13 +101,5 @@
|
|||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/yesmore/inke/issues"
|
"url": "https://github.com/yesmore/inke/issues"
|
||||||
},
|
}
|
||||||
"keywords": [
|
|
||||||
"ai",
|
|
||||||
"inke",
|
|
||||||
"editor",
|
|
||||||
"markdown",
|
|
||||||
"nextjs",
|
|
||||||
"react"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
@ -24,3 +24,11 @@ export function getUrlFromString(str: string) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isImageLink(link: string): boolean {
|
||||||
|
if (!isValidUrl(link)) return false;
|
||||||
|
const imageExtensions = [".jpg", ".jpeg", ".png", "webp", ".gif", ".bmp"];
|
||||||
|
const fileExtension = link.substring(link.lastIndexOf(".")).toLowerCase();
|
||||||
|
|
||||||
|
return imageExtensions.includes(fileExtension);
|
||||||
|
}
|
||||||
|
@ -10,8 +10,6 @@ import {
|
|||||||
ListPlus,
|
ListPlus,
|
||||||
PartyPopper,
|
PartyPopper,
|
||||||
PauseCircle,
|
PauseCircle,
|
||||||
Pipette,
|
|
||||||
Repeat,
|
|
||||||
Scissors,
|
Scissors,
|
||||||
Wand,
|
Wand,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
@ -28,8 +28,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
|
|||||||
className="novel-flex novel-h-full novel-items-center novel-space-x-2 novel-px-3 novel-py-1.5 novel-text-sm novel-font-medium novel-text-stone-600 hover:novel-bg-stone-100 active:novel-bg-stone-200"
|
className="novel-flex novel-h-full novel-items-center novel-space-x-2 novel-px-3 novel-py-1.5 novel-text-sm novel-font-medium novel-text-stone-600 hover:novel-bg-stone-100 active:novel-bg-stone-200"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsOpen(!isOpen);
|
setIsOpen(!isOpen);
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<p className="novel-text-base">↗</p>
|
<p className="novel-text-base">↗</p>
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -37,8 +36,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
|
|||||||
{
|
{
|
||||||
"novel-text-blue-500": editor.isActive("link"),
|
"novel-text-blue-500": editor.isActive("link"),
|
||||||
}
|
}
|
||||||
)}
|
)}>
|
||||||
>
|
|
||||||
Link
|
Link
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
@ -51,8 +49,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
|
|||||||
url && editor.chain().focus().setLink({ href: url }).run();
|
url && editor.chain().focus().setLink({ href: url }).run();
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
className="novel-fixed novel-top-full novel-z-[99999] novel-mt-1 novel-flex novel-w-60 novel-overflow-hidden novel-rounded novel-border novel-border-stone-200 novel-bg-white novel-p-1 novel-shadow-xl novel-animate-in novel-fade-in novel-slide-in-from-top-1"
|
className="novel-fixed novel-top-full novel-z-[99999] novel-mt-1 novel-flex novel-w-60 novel-overflow-hidden novel-rounded novel-border novel-border-stone-200 novel-bg-white novel-p-1 novel-shadow-xl novel-animate-in novel-fade-in novel-slide-in-from-top-1">
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
@ -67,8 +64,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor.chain().focus().unsetLink().run();
|
editor.chain().focus().unsetLink().run();
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<Trash className="novel-h-4 novel-w-4" />
|
<Trash className="novel-h-4 novel-w-4" />
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
|
@ -26,6 +26,7 @@ import {
|
|||||||
CheckSquare,
|
CheckSquare,
|
||||||
Table2,
|
Table2,
|
||||||
PauseCircle,
|
PauseCircle,
|
||||||
|
FileImage,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { LoadingCircle } from "@/ui/icons";
|
import { LoadingCircle } from "@/ui/icons";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@ -35,6 +36,7 @@ import { getPrevText } from "@/lib/editor";
|
|||||||
import { startImageUpload } from "@/ui/editor/plugins/upload-images";
|
import { startImageUpload } from "@/ui/editor/plugins/upload-images";
|
||||||
import { NovelContext } from "../provider";
|
import { NovelContext } from "../provider";
|
||||||
import { Youtube } from "lucide-react";
|
import { Youtube } from "lucide-react";
|
||||||
|
import { isImageLink } from "@/lib/utils";
|
||||||
|
|
||||||
interface CommandItemProps {
|
interface CommandItemProps {
|
||||||
title: string;
|
title: string;
|
||||||
@ -211,10 +213,10 @@ const getSuggestionItems = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Image",
|
title: "Local image",
|
||||||
description: "Upload an image from your computer.",
|
description: "Upload an image from your computer.",
|
||||||
searchTerms: ["photo", "picture", "media"],
|
searchTerms: ["photo", "picture", "media", "img"],
|
||||||
icon: <ImageIcon size={18} />,
|
icon: <FileImage size={18} />,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
editor.chain().focus().deleteRange(range).run();
|
editor.chain().focus().deleteRange(range).run();
|
||||||
// upload image
|
// upload image
|
||||||
@ -231,6 +233,25 @@ const getSuggestionItems = ({
|
|||||||
input.click();
|
input.click();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Remote image",
|
||||||
|
description: "Render an image from url.",
|
||||||
|
searchTerms: ["photo", "picture", "media", "img"],
|
||||||
|
icon: <ImageIcon size={18} />,
|
||||||
|
command: ({ editor, range }: CommandProps) => {
|
||||||
|
const url = prompt("Enter image url");
|
||||||
|
if (url && isImageLink(url)) {
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.deleteRange(range)
|
||||||
|
.setImage({
|
||||||
|
src: url,
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Youtube video",
|
title: "Youtube video",
|
||||||
description: "Play the Youtube video you filled out.",
|
description: "Play the Youtube video you filled out.",
|
||||||
|
@ -2,8 +2,6 @@ import { BlobResult } from "@vercel/blob";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
|
import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
|
||||||
import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
|
import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
|
||||||
import { NovelContext } from "../provider";
|
|
||||||
import { useContext } from "react";
|
|
||||||
|
|
||||||
const uploadKey = new PluginKey("upload-image");
|
const uploadKey = new PluginKey("upload-image");
|
||||||
|
|
||||||
@ -26,7 +24,7 @@ const UploadImagesPlugin = () =>
|
|||||||
const image = document.createElement("img");
|
const image = document.createElement("img");
|
||||||
image.setAttribute(
|
image.setAttribute(
|
||||||
"class",
|
"class",
|
||||||
"opacity-40 rounded-lg border border-stone-200"
|
"opacity-40 rounded-md border border-stone-200"
|
||||||
);
|
);
|
||||||
image.src = src;
|
image.src = src;
|
||||||
placeholder.appendChild(image);
|
placeholder.appendChild(image);
|
||||||
@ -62,8 +60,8 @@ export function startImageUpload(file: File, view: EditorView, pos: number) {
|
|||||||
if (!file.type.includes("image/")) {
|
if (!file.type.includes("image/")) {
|
||||||
toast.error("File type not supported.");
|
toast.error("File type not supported.");
|
||||||
return;
|
return;
|
||||||
} else if (file.size / 1024 / 1024 > 5) {
|
} else if (file.size / 1024 / 1024 > 1) {
|
||||||
toast.error(`File size too big (max ${15}MB).`);
|
toast.error(`File size too big (max ${1}MB).`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,7 +109,6 @@ export function startImageUpload(file: File, view: EditorView, pos: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const handleImageUpload = (file: File) => {
|
export const handleImageUpload = (file: File) => {
|
||||||
// upload to Vercel Blob
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
toast.promise(
|
toast.promise(
|
||||||
fetch("/api/upload", {
|
fetch("/api/upload", {
|
||||||
@ -125,6 +122,7 @@ export const handleImageUpload = (file: File) => {
|
|||||||
// Successfully uploaded image
|
// Successfully uploaded image
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const { url } = (await res.json()) as BlobResult;
|
const { url } = (await res.json()) as BlobResult;
|
||||||
|
|
||||||
// preload the image
|
// preload the image
|
||||||
let image = new Image();
|
let image = new Image();
|
||||||
image.src = url;
|
image.src = url;
|
||||||
@ -134,7 +132,6 @@ export const handleImageUpload = (file: File) => {
|
|||||||
// No blob store configured
|
// No blob store configured
|
||||||
} else if (res.status === 401) {
|
} else if (res.status === 401) {
|
||||||
resolve(file);
|
resolve(file);
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"`BLOB_READ_WRITE_TOKEN` environment variable not found, reading image locally instead."
|
"`BLOB_READ_WRITE_TOKEN` environment variable not found, reading image locally instead."
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user