@shadospace/editor 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -17,6 +17,35 @@ export const ourFileRouter = {
|
|
|
17
17
|
maxFileSize: "4MB",
|
|
18
18
|
maxFileCount: 1,
|
|
19
19
|
},
|
|
20
|
+
})
|
|
21
|
+
.middleware(async ({ req }) => {
|
|
22
|
+
// This code runs on your server before upload
|
|
23
|
+
const user = await auth(req)
|
|
24
|
+
|
|
25
|
+
// If you throw, the user will not be able to upload
|
|
26
|
+
if (!user) throw new UploadThingError("Unauthorized")
|
|
27
|
+
|
|
28
|
+
// Whatever is returned here is accessible in onUploadComplete as `metadata`
|
|
29
|
+
return { userId: user.id }
|
|
30
|
+
})
|
|
31
|
+
.onUploadComplete(async ({ metadata, file }) => {
|
|
32
|
+
// This code RUNS ON YOUR SERVER after upload
|
|
33
|
+
console.log("Upload complete for userId:", metadata.userId)
|
|
34
|
+
|
|
35
|
+
console.log("file url", file.ufsUrl)
|
|
36
|
+
|
|
37
|
+
// !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback
|
|
38
|
+
return { uploadedBy: metadata.userId }
|
|
39
|
+
}),
|
|
40
|
+
coverImageUploader: f({
|
|
41
|
+
image: {
|
|
42
|
+
/**
|
|
43
|
+
* For full list of options and defaults, see the File Route API reference
|
|
44
|
+
* @see https://docs.uploadthing.com/file-routes#route-config
|
|
45
|
+
*/
|
|
46
|
+
maxFileSize: "4MB",
|
|
47
|
+
maxFileCount: 1,
|
|
48
|
+
},
|
|
20
49
|
})
|
|
21
50
|
// Set permissions and file types for this FileRoute
|
|
22
51
|
.middleware(async ({ req }) => {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { useEditor, EditorContent } from "@tiptap/react"
|
|
3
3
|
import StarterKit from "@tiptap/starter-kit"
|
|
4
4
|
import Image from "@tiptap/extension-image"
|
|
5
|
+
import Link from "@tiptap/extension-link"
|
|
5
6
|
import { TableKit } from "@tiptap/extension-table"
|
|
6
7
|
import { TextStyleKit } from "@tiptap/extension-text-style"
|
|
7
8
|
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"
|
|
@@ -35,6 +36,7 @@ export default function Editor({
|
|
|
35
36
|
TextStyleKit,
|
|
36
37
|
Image,
|
|
37
38
|
TableKit,
|
|
39
|
+
Link,
|
|
38
40
|
CodeBlockLowlight.configure({ lowlight }),
|
|
39
41
|
],
|
|
40
42
|
editorProps: {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
Heading3,
|
|
13
13
|
ImageIcon,
|
|
14
14
|
Italic,
|
|
15
|
+
Link,
|
|
15
16
|
List,
|
|
16
17
|
ListOrdered,
|
|
17
18
|
Minus,
|
|
@@ -30,8 +31,20 @@ import {
|
|
|
30
31
|
DropdownMenuItem,
|
|
31
32
|
DropdownMenuTrigger,
|
|
32
33
|
} from "../ui/dropdown-menu"
|
|
33
|
-
import {
|
|
34
|
+
import { UploadDropzone } from "@/lib/uploadthing"
|
|
34
35
|
import { toast } from "sonner"
|
|
36
|
+
import {
|
|
37
|
+
Dialog,
|
|
38
|
+
DialogClose,
|
|
39
|
+
DialogContent,
|
|
40
|
+
DialogDescription,
|
|
41
|
+
DialogFooter,
|
|
42
|
+
DialogHeader,
|
|
43
|
+
DialogTitle,
|
|
44
|
+
DialogTrigger,
|
|
45
|
+
} from "@/components/ui/dialog"
|
|
46
|
+
import { Label } from "../ui/label"
|
|
47
|
+
import { Input } from "../ui/input"
|
|
35
48
|
|
|
36
49
|
export const MenuBar = ({ editor }: { editor: Editor }) => {
|
|
37
50
|
const editorState = useEditorState({
|
|
@@ -151,7 +164,59 @@ export const MenuBar = ({ editor }: { editor: Editor }) => {
|
|
|
151
164
|
>
|
|
152
165
|
<SquareCode />
|
|
153
166
|
</Button>
|
|
154
|
-
|
|
167
|
+
<Dialog>
|
|
168
|
+
<DialogTrigger asChild>
|
|
169
|
+
<Button type="button" variant={"ghost"} size={"icon"}>
|
|
170
|
+
<Link />
|
|
171
|
+
</Button>
|
|
172
|
+
</DialogTrigger>
|
|
173
|
+
<DialogContent>
|
|
174
|
+
<DialogHeader>
|
|
175
|
+
<DialogTitle>Set Link</DialogTitle>
|
|
176
|
+
<DialogDescription>
|
|
177
|
+
Set the link in full URL like https://example.com
|
|
178
|
+
</DialogDescription>
|
|
179
|
+
</DialogHeader>
|
|
180
|
+
<div className="flex flex-col gap-4">
|
|
181
|
+
<div className="grid flex-1 gap-2">
|
|
182
|
+
<Label htmlFor="link-url">Link URL</Label>
|
|
183
|
+
<Input
|
|
184
|
+
placeholder="https://example.com"
|
|
185
|
+
id="link-url"
|
|
186
|
+
onChange={(e) =>
|
|
187
|
+
editor
|
|
188
|
+
.chain()
|
|
189
|
+
.focus()
|
|
190
|
+
.setLink({ href: e.target.value })
|
|
191
|
+
.run()
|
|
192
|
+
}
|
|
193
|
+
value={editorState.href || ""}
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
<DialogFooter>
|
|
198
|
+
<DialogClose asChild>
|
|
199
|
+
<Button type="button" variant="secondary">
|
|
200
|
+
Cancel
|
|
201
|
+
</Button>
|
|
202
|
+
</DialogClose>
|
|
203
|
+
<DialogClose asChild>
|
|
204
|
+
<Button
|
|
205
|
+
type="button"
|
|
206
|
+
onClick={() =>
|
|
207
|
+
editor
|
|
208
|
+
.chain()
|
|
209
|
+
.focus()
|
|
210
|
+
.setLink({ href: editorState.href })
|
|
211
|
+
.run()
|
|
212
|
+
}
|
|
213
|
+
>
|
|
214
|
+
Set Link
|
|
215
|
+
</Button>
|
|
216
|
+
</DialogClose>
|
|
217
|
+
</DialogFooter>
|
|
218
|
+
</DialogContent>
|
|
219
|
+
</Dialog>
|
|
155
220
|
<Button
|
|
156
221
|
type="button"
|
|
157
222
|
variant={"ghost"}
|
|
@@ -161,20 +226,62 @@ export const MenuBar = ({ editor }: { editor: Editor }) => {
|
|
|
161
226
|
>
|
|
162
227
|
<Quote />
|
|
163
228
|
</Button>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
229
|
+
|
|
230
|
+
<Dialog>
|
|
231
|
+
<DialogTrigger asChild>
|
|
232
|
+
<Button type="button" variant={"ghost"} size={"icon"}>
|
|
233
|
+
<ImageIcon />
|
|
234
|
+
</Button>
|
|
235
|
+
</DialogTrigger>
|
|
236
|
+
<DialogContent className="sm:max-w-md">
|
|
237
|
+
<DialogHeader>
|
|
238
|
+
<DialogTitle>Set Image</DialogTitle>
|
|
239
|
+
<DialogDescription>
|
|
240
|
+
Upload your image or provide a link.
|
|
241
|
+
</DialogDescription>
|
|
242
|
+
</DialogHeader>
|
|
243
|
+
<div className="flex flex-col gap-4">
|
|
244
|
+
<div className="grid flex-1 gap-2">
|
|
245
|
+
<Input
|
|
246
|
+
placeholder="https://example.com"
|
|
247
|
+
id="image-url"
|
|
248
|
+
onChange={(e) =>
|
|
249
|
+
editor
|
|
250
|
+
.chain()
|
|
251
|
+
.focus()
|
|
252
|
+
.setImage({ src: e.target.value })
|
|
253
|
+
.run()
|
|
254
|
+
}
|
|
255
|
+
value={editorState.image.src || ""}
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
<div className="w-full cursor-pointer">
|
|
259
|
+
<UploadDropzone
|
|
260
|
+
className="h-48 w-full border-2 border-red-500/20 hover:border-red-500/30 ut-button:bg-red-500 ut-button:p-3 ut-button:text-sm ut-button:hover:bg-red-500 ut-allowed-content:hidden ut-label:text-sm ut-label:text-muted-foreground ut-upload-icon:size-40"
|
|
261
|
+
endpoint="imageUploader"
|
|
262
|
+
onClientUploadComplete={(res) => {
|
|
263
|
+
res.forEach((item) => {
|
|
264
|
+
toast.success("Image uploaded successfully")
|
|
265
|
+
editor
|
|
266
|
+
.chain()
|
|
267
|
+
.focus()
|
|
268
|
+
.setImage({ src: item.ufsUrl })
|
|
269
|
+
.run()
|
|
270
|
+
})
|
|
271
|
+
}}
|
|
272
|
+
onUploadError={(error) => {
|
|
273
|
+
toast.error(`Upload failed: ${error.message}`)
|
|
274
|
+
}}
|
|
275
|
+
/>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
<DialogFooter className="justify-end">
|
|
279
|
+
<DialogClose asChild>
|
|
280
|
+
<Button type="button">Insert Image</Button>
|
|
281
|
+
</DialogClose>
|
|
282
|
+
</DialogFooter>
|
|
283
|
+
</DialogContent>
|
|
284
|
+
</Dialog>
|
|
178
285
|
<Button
|
|
179
286
|
type="button"
|
|
180
287
|
variant={"ghost"}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import type { Editor } from "@tiptap/core"
|
|
2
2
|
import type { EditorStateSnapshot } from "@tiptap/react"
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* State selector for the MenuBar component.
|
|
6
|
-
* Extracts the relevant editor state for rendering menu buttons.
|
|
7
|
-
*/
|
|
8
4
|
export function menuBarStateSelector(ctx: EditorStateSnapshot<Editor>) {
|
|
9
5
|
return {
|
|
10
6
|
// Text formatting
|
|
@@ -27,6 +23,15 @@ export function menuBarStateSelector(ctx: EditorStateSnapshot<Editor>) {
|
|
|
27
23
|
isHeading5: ctx.editor.isActive("heading", { level: 5 }) ?? false,
|
|
28
24
|
isHeading6: ctx.editor.isActive("heading", { level: 6 }) ?? false,
|
|
29
25
|
|
|
26
|
+
// others
|
|
27
|
+
href: ctx.editor.isActive("link")
|
|
28
|
+
? ctx.editor.getAttributes("link").href
|
|
29
|
+
: "",
|
|
30
|
+
|
|
31
|
+
image: ctx.editor.isActive("image")
|
|
32
|
+
? ctx.editor.getAttributes("image").src
|
|
33
|
+
: "",
|
|
34
|
+
|
|
30
35
|
// Lists and blocks
|
|
31
36
|
isBulletList: ctx.editor.isActive("bulletList") ?? false,
|
|
32
37
|
isOrderedList: ctx.editor.isActive("orderedList") ?? false,
|
package/package.json
CHANGED
package/scripts/init.js
CHANGED
|
@@ -1,132 +1,149 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import { fileURLToPath } from
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import { fileURLToPath } from "url"
|
|
4
4
|
|
|
5
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
6
|
-
const __dirname = path.dirname(__filename)
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
6
|
+
const __dirname = path.dirname(__filename)
|
|
7
7
|
|
|
8
8
|
// Target directory (where the user is installing the package)
|
|
9
9
|
// INIT_CWD is set by npm/yarn/pnpm/bun during postinstall
|
|
10
|
-
const targetDir = process.env.INIT_CWD || process.cwd()
|
|
10
|
+
const targetDir = process.env.INIT_CWD || process.cwd()
|
|
11
11
|
|
|
12
|
-
console.log(`[tiptap-starter] Initializing in ${targetDir}`)
|
|
12
|
+
console.log(`[tiptap-starter] Initializing in ${targetDir}`)
|
|
13
13
|
|
|
14
14
|
// Verify that the target directory is a Next.js project with shadcn installed
|
|
15
|
-
const targetPackageJsonPath = path.join(targetDir,
|
|
15
|
+
const targetPackageJsonPath = path.join(targetDir, "package.json")
|
|
16
16
|
|
|
17
17
|
if (!fs.existsSync(targetPackageJsonPath)) {
|
|
18
|
-
console.error(
|
|
19
|
-
|
|
18
|
+
console.error(
|
|
19
|
+
`[tiptap-starter] Error: package.json not found in ${targetDir}. Please run this in the root of your project.`
|
|
20
|
+
)
|
|
21
|
+
process.exit(1)
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
let pkg
|
|
24
|
+
let pkg
|
|
23
25
|
try {
|
|
24
|
-
pkg = JSON.parse(fs.readFileSync(targetPackageJsonPath,
|
|
25
|
-
} catch
|
|
26
|
-
console.error(
|
|
27
|
-
|
|
26
|
+
pkg = JSON.parse(fs.readFileSync(targetPackageJsonPath, "utf8"))
|
|
27
|
+
} catch {
|
|
28
|
+
console.error(
|
|
29
|
+
`[tiptap-starter] Error: Failed to parse package.json in ${targetDir}`
|
|
30
|
+
)
|
|
31
|
+
process.exit(1)
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
const hasNext = pkg.dependencies?.next || pkg.devDependencies?.next
|
|
31
|
-
const hasShadcn = fs.existsSync(path.join(targetDir,
|
|
34
|
+
const hasNext = pkg.dependencies?.next || pkg.devDependencies?.next
|
|
35
|
+
const hasShadcn = fs.existsSync(path.join(targetDir, "components.json"))
|
|
32
36
|
|
|
33
37
|
if (!hasNext) {
|
|
34
|
-
console.error(
|
|
35
|
-
|
|
38
|
+
console.error(
|
|
39
|
+
`[tiptap-starter] Error: Next.js is not detected. This package is designed for Next.js projects.`
|
|
40
|
+
)
|
|
41
|
+
process.exit(1)
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
if (!hasShadcn) {
|
|
39
|
-
console.error(
|
|
40
|
-
|
|
45
|
+
console.error(
|
|
46
|
+
`[tiptap-starter] Error: shadcn/ui is not detected (components.json not found). Please initialize shadcn first.`
|
|
47
|
+
)
|
|
48
|
+
process.exit(1)
|
|
41
49
|
}
|
|
42
50
|
|
|
43
|
-
const sourceEditorDir = path.join(__dirname,
|
|
51
|
+
const sourceEditorDir = path.join(__dirname, "../components/editor")
|
|
44
52
|
|
|
45
53
|
// Detect if target project uses src directory
|
|
46
|
-
const hasSrc = fs.existsSync(path.join(targetDir,
|
|
47
|
-
const baseTargetDir = hasSrc ? path.join(targetDir,
|
|
54
|
+
const hasSrc = fs.existsSync(path.join(targetDir, "src"))
|
|
55
|
+
const baseTargetDir = hasSrc ? path.join(targetDir, "src") : targetDir
|
|
48
56
|
|
|
49
|
-
const targetEditorDir = path.join(baseTargetDir,
|
|
57
|
+
const targetEditorDir = path.join(baseTargetDir, "components/editor")
|
|
50
58
|
|
|
51
59
|
// Function to copy files
|
|
52
60
|
function copyFile(src, dest) {
|
|
53
61
|
if (fs.existsSync(dest)) {
|
|
54
|
-
console.log(
|
|
55
|
-
|
|
62
|
+
console.log(
|
|
63
|
+
`[tiptap-starter] ${path.basename(dest)} already exists, skipping.`
|
|
64
|
+
)
|
|
65
|
+
return
|
|
56
66
|
}
|
|
57
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
58
|
-
fs.copyFileSync(src, dest)
|
|
59
|
-
console.log(`[tiptap-starter] Created ${dest}`)
|
|
67
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
68
|
+
fs.copyFileSync(src, dest)
|
|
69
|
+
console.log(`[tiptap-starter] Created ${dest}`)
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
// Files to copy
|
|
63
|
-
const files = [
|
|
64
|
-
'index.tsx',
|
|
65
|
-
'menu-bar.tsx',
|
|
66
|
-
'menubar-state.tsx',
|
|
67
|
-
'styles.css'
|
|
68
|
-
];
|
|
73
|
+
const files = ["index.tsx", "menu-bar.tsx", "menubar-state.tsx", "styles.css"]
|
|
69
74
|
|
|
70
75
|
try {
|
|
71
76
|
// Check if source files exist
|
|
72
|
-
files.forEach(file => {
|
|
73
|
-
const src = path.join(sourceEditorDir, file)
|
|
74
|
-
const dest = path.join(targetEditorDir, file)
|
|
77
|
+
files.forEach((file) => {
|
|
78
|
+
const src = path.join(sourceEditorDir, file)
|
|
79
|
+
const dest = path.join(targetEditorDir, file)
|
|
75
80
|
if (fs.existsSync(src)) {
|
|
76
|
-
copyFile(src, dest)
|
|
81
|
+
copyFile(src, dest)
|
|
77
82
|
} else {
|
|
78
|
-
console.error(`[tiptap-starter] Source file missing: ${src}`)
|
|
83
|
+
console.error(`[tiptap-starter] Source file missing: ${src}`)
|
|
79
84
|
}
|
|
80
|
-
})
|
|
85
|
+
})
|
|
81
86
|
|
|
82
87
|
// Copy UploadThing setup files
|
|
83
88
|
const uploadthingFiles = [
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
{
|
|
90
|
+
src: path.join(__dirname, "../lib/uploadthing.ts"),
|
|
91
|
+
dest: path.join(baseTargetDir, "lib/uploadthing.ts"),
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
src: path.join(__dirname, "../app/api/uploadthing/core.ts"),
|
|
95
|
+
dest: path.join(baseTargetDir, "app/api/uploadthing/core.ts"),
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
src: path.join(__dirname, "../app/api/uploadthing/route.ts"),
|
|
99
|
+
dest: path.join(baseTargetDir, "app/api/uploadthing/route.ts"),
|
|
100
|
+
},
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
uploadthingFiles.forEach((file) => {
|
|
90
104
|
if (fs.existsSync(file.src)) {
|
|
91
|
-
copyFile(file.src, file.dest)
|
|
105
|
+
copyFile(file.src, file.dest)
|
|
92
106
|
} else {
|
|
93
|
-
console.error(`[tiptap-starter] Source file missing: ${file.src}`)
|
|
107
|
+
console.error(`[tiptap-starter] Source file missing: ${file.src}`)
|
|
94
108
|
}
|
|
95
|
-
})
|
|
109
|
+
})
|
|
96
110
|
|
|
97
111
|
// Update globals.css
|
|
98
112
|
const possiblePaths = [
|
|
99
|
-
path.join(targetDir,
|
|
100
|
-
path.join(targetDir,
|
|
101
|
-
path.join(targetDir,
|
|
102
|
-
]
|
|
113
|
+
path.join(targetDir, "app/globals.css"),
|
|
114
|
+
path.join(targetDir, "src/app/globals.css"),
|
|
115
|
+
path.join(targetDir, "styles/globals.css"),
|
|
116
|
+
]
|
|
103
117
|
|
|
104
|
-
let globalsCssPath = possiblePaths.find(p => fs.existsSync(p))
|
|
118
|
+
let globalsCssPath = possiblePaths.find((p) => fs.existsSync(p))
|
|
105
119
|
|
|
106
120
|
if (globalsCssPath) {
|
|
107
|
-
const content = fs.readFileSync(globalsCssPath,
|
|
108
|
-
const importStatement = `@import "../components/editor/styles.css"
|
|
109
|
-
|
|
121
|
+
const content = fs.readFileSync(globalsCssPath, "utf8")
|
|
122
|
+
const importStatement = `@import "../components/editor/styles.css";`
|
|
123
|
+
|
|
110
124
|
if (!content.includes(importStatement)) {
|
|
111
|
-
const lines = content.split(
|
|
112
|
-
let insertIndex = 0
|
|
125
|
+
const lines = content.split("\n")
|
|
126
|
+
let insertIndex = 0
|
|
113
127
|
for (let i = 0; i < lines.length; i++) {
|
|
114
|
-
if (lines[i].startsWith(
|
|
115
|
-
insertIndex = i + 1
|
|
116
|
-
} else if (lines[i].trim() !==
|
|
117
|
-
break
|
|
128
|
+
if (lines[i].startsWith("@import")) {
|
|
129
|
+
insertIndex = i + 1
|
|
130
|
+
} else if (lines[i].trim() !== "") {
|
|
131
|
+
break
|
|
118
132
|
}
|
|
119
133
|
}
|
|
120
|
-
lines.splice(insertIndex, 0, importStatement)
|
|
121
|
-
fs.writeFileSync(globalsCssPath, lines.join(
|
|
122
|
-
console.log(`[tiptap-starter] Referenced styles in ${globalsCssPath}`)
|
|
134
|
+
lines.splice(insertIndex, 0, importStatement)
|
|
135
|
+
fs.writeFileSync(globalsCssPath, lines.join("\n"))
|
|
136
|
+
console.log(`[tiptap-starter] Referenced styles in ${globalsCssPath}`)
|
|
123
137
|
} else {
|
|
124
|
-
console.log(
|
|
138
|
+
console.log(
|
|
139
|
+
`[tiptap-starter] Styles already referenced in ${globalsCssPath}`
|
|
140
|
+
)
|
|
125
141
|
}
|
|
126
142
|
} else {
|
|
127
|
-
console.warn(
|
|
143
|
+
console.warn(
|
|
144
|
+
`[tiptap-starter] globals.css not found. Please add \`@import "../components/editor/styles.css";\` manually to your CSS file.`
|
|
145
|
+
)
|
|
128
146
|
}
|
|
129
|
-
|
|
130
147
|
} catch (error) {
|
|
131
|
-
console.error(`[tiptap-starter] Error during initialization:`, error)
|
|
148
|
+
console.error(`[tiptap-starter] Error during initialization:`, error)
|
|
132
149
|
}
|