From 2ac315c0390d4ffa0cc109051d5719168048df83 Mon Sep 17 00:00:00 2001 From: codetorso Date: Sat, 22 Jun 2024 14:08:13 +0530 Subject: [PATCH] improve canvas drag and drop make sidepanel work --- apps/web/app/(canvas)/canvas/page.tsx | 81 +++--- apps/web/app/(dash)/menu.tsx | 8 +- apps/web/app/(editor)/ai.md | 43 ---- .../app/(editor)/components/aigenerate.tsx | 186 -------------- .../(editor)/components/editorcommands.tsx | 85 ------- .../web/app/(editor)/components/extensions.ts | 141 ----------- .../app/(editor)/components/image-upload.ts | 50 ---- .../components/selectors/bgcolor-selector.tsx | 107 -------- .../components/selectors/color-selector.tsx | 111 --------- .../components/selectors/link-selector.tsx | 95 ------- .../components/selectors/node-selector.tsx | 126 ---------- .../components/selectors/text-buttons.tsx | 63 ----- .../app/(editor)/components/slash-command.tsx | 163 ------------ apps/web/app/(editor)/components/topbar.tsx | 68 ------ .../web/app/(editor)/components/ui/asksvg.tsx | 12 - .../components/ui/autocompletesvg.tsx | 12 - .../(editor)/components/ui/crazy-spinner.tsx | 11 - apps/web/app/(editor)/components/ui/magic.tsx | 8 - .../app/(editor)/components/ui/rewritesvg.tsx | 11 - .../(editor)/components/ui/translatesvg.tsx | 12 - apps/web/app/(editor)/editor.tsx | 55 ----- apps/web/app/(editor)/editor/page.tsx | 8 - apps/web/app/(editor)/layout.tsx | 12 - apps/web/app/(editor)/lib/content.ts | 231 ------------------ apps/web/app/(editor)/lib/debouncedsave.ts | 20 -- apps/web/app/(editor)/lib/editorprops.ts | 16 -- .../web/app/(editor)/lib/use-local-storage.ts | 27 -- apps/web/app/(editor)/styles/globals.css | 168 ------------- apps/web/app/(editor)/styles/prosemirror.css | 203 --------------- .../components/canvas/draggableComponent.tsx | 72 ++++++ packages/ui/icons/autocomplete.svg | 15 ++ packages/ui/icons/block.svg | 15 ++ packages/ui/icons/canvas.svg | 3 + .../svg.tsx => packages/ui/icons/drag.svg | 85 +------ packages/ui/icons/index.ts | 16 +- packages/ui/icons/link.svg | 15 ++ packages/ui/icons/settings.svg | 14 ++ 37 files changed, 183 insertions(+), 2185 deletions(-) delete mode 100644 apps/web/app/(editor)/ai.md delete mode 100644 apps/web/app/(editor)/components/aigenerate.tsx delete mode 100644 apps/web/app/(editor)/components/editorcommands.tsx delete mode 100644 apps/web/app/(editor)/components/extensions.ts delete mode 100644 apps/web/app/(editor)/components/image-upload.ts delete mode 100644 apps/web/app/(editor)/components/selectors/bgcolor-selector.tsx delete mode 100644 apps/web/app/(editor)/components/selectors/color-selector.tsx delete mode 100644 apps/web/app/(editor)/components/selectors/link-selector.tsx delete mode 100644 apps/web/app/(editor)/components/selectors/node-selector.tsx delete mode 100644 apps/web/app/(editor)/components/selectors/text-buttons.tsx delete mode 100644 apps/web/app/(editor)/components/slash-command.tsx delete mode 100644 apps/web/app/(editor)/components/topbar.tsx delete mode 100644 apps/web/app/(editor)/components/ui/asksvg.tsx delete mode 100644 apps/web/app/(editor)/components/ui/autocompletesvg.tsx delete mode 100644 apps/web/app/(editor)/components/ui/crazy-spinner.tsx delete mode 100644 apps/web/app/(editor)/components/ui/magic.tsx delete mode 100644 apps/web/app/(editor)/components/ui/rewritesvg.tsx delete mode 100644 apps/web/app/(editor)/components/ui/translatesvg.tsx delete mode 100644 apps/web/app/(editor)/editor.tsx delete mode 100644 apps/web/app/(editor)/editor/page.tsx delete mode 100644 apps/web/app/(editor)/layout.tsx delete mode 100644 apps/web/app/(editor)/lib/content.ts delete mode 100644 apps/web/app/(editor)/lib/debouncedsave.ts delete mode 100644 apps/web/app/(editor)/lib/editorprops.ts delete mode 100644 apps/web/app/(editor)/lib/use-local-storage.ts delete mode 100644 apps/web/app/(editor)/styles/globals.css delete mode 100644 apps/web/app/(editor)/styles/prosemirror.css create mode 100644 packages/ui/components/canvas/draggableComponent.tsx create mode 100644 packages/ui/icons/autocomplete.svg create mode 100644 packages/ui/icons/block.svg create mode 100644 packages/ui/icons/canvas.svg rename apps/web/app/(canvas)/svg.tsx => packages/ui/icons/drag.svg (51%) create mode 100644 packages/ui/icons/link.svg create mode 100644 packages/ui/icons/settings.svg diff --git a/apps/web/app/(canvas)/canvas/page.tsx b/apps/web/app/(canvas)/canvas/page.tsx index c24e2c73..ea3cee1b 100644 --- a/apps/web/app/(canvas)/canvas/page.tsx +++ b/apps/web/app/(canvas)/canvas/page.tsx @@ -1,17 +1,12 @@ "use client"; -// import Canvas from "./_components/canvas"; import { Canvas } from "../canvas"; import React, { useState } from "react"; -// import ReactTextareaAutosize from "react-textarea-autosize"; import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; -import { - DragSvg, - SettingsSvg, - LinkSvg, - ThreeDBlock, - TextLoadingSvg, -} from "../svg"; +import { SettingsIcon, DragIcon } from "@repo/ui/icons"; +import DraggableComponentsContainer from "@repo/ui/components/canvas/draggableComponent"; +import { AutocompleteIcon, blockIcon } from "@repo/ui/icons"; +import Image from "next/image"; function page() { const [value, setValue] = useState(""); @@ -45,7 +40,7 @@ function page() { >
Change Filters - + setting-icon
{visible ? ( -
-
- -

- Nvidia will most likely create monopoly in software - industry as they are already largest player in GPU - hardware by 20... -

-
-
- -
-
-

- Nvidia currently dominates the GPU hardware market, - with a market share over 97%. This has led some to - argue... -

-
-

- From space: GPU GOATS -

-
-
-
- -
-
-

- Nvidia currently dominates the GPU hardware market, - with a market share over 97%. This has led some to - argue... -

-
-

- Page url: - https://www.cnbc.com/2024/05/23/nvidia-keeps-hitting-records-can-investors-still-buy-the-stock.html?&qsearchterm=nvidia -

-
-
-
+ ) : (

Need more space to show!

)} @@ -112,7 +67,7 @@ function page() {
- + drag-icon
{/* */} @@ -129,4 +84,26 @@ function page() { ); } +const content= [ + { + content: "Nvidia currently dominates the GPU hardware market, with a market share over 97%. This has led some to argue...", + icon: AutocompleteIcon, + iconAlt: "Autocomplete", + extraInfo: "Page Url: https://www.cnbc.com/2024/05/23/nvidia-keeps-hitting-records-can-investors-still-buy-the-stock.html?&qsearchterm=nvidia", + }, + { + content: "Nvidia currently dominates the GPU hardware market, with a market share over 97%. This has led some to argue...", + icon: blockIcon, + iconAlt: "Autocomplete", + extraInfo: "Page Url: https://www.cnbc.com/2024/05/23/nvidia-keeps-hitting-records-can-investors-still-buy-the-stock.html?&qsearchterm=nvidia", + }, + +] + +function SidePanel(){ + return ( + + ) +} + export default page; diff --git a/apps/web/app/(dash)/menu.tsx b/apps/web/app/(dash)/menu.tsx index 035e8e3f..f40c22e4 100644 --- a/apps/web/app/(dash)/menu.tsx +++ b/apps/web/app/(dash)/menu.tsx @@ -1,6 +1,6 @@ import React from "react"; import Image from "next/image"; -import { MemoriesIcon, ExploreIcon, HistoryIcon } from "@repo/ui/icons"; +import { MemoriesIcon, ExploreIcon, HistoryIcon, CanvasIcon } from "@repo/ui/icons"; import Link from "next/link"; function Menu() { @@ -16,9 +16,9 @@ function Menu() { url: "/explore", }, { - icon: HistoryIcon, - text: "History", - url: "/history", + icon: CanvasIcon, + text: "Canvas", + url: "/canvas", }, ]; diff --git a/apps/web/app/(editor)/ai.md b/apps/web/app/(editor)/ai.md deleted file mode 100644 index 1528a7bd..00000000 --- a/apps/web/app/(editor)/ai.md +++ /dev/null @@ -1,43 +0,0 @@ -## to access the editor -``` -import { useEditor } from "novel"; -const editor = useEditor() -``` - -## to get previous text -``` -import { getPrevText } from "novel/utils"; -const pos = editor.state.selection.from; -const text = getPrevText(editor, pos); -``` - -## selected content into markdown format -``` -const slice = editor.state.selection.content(); -const text = editor.storage.markdown.serializer.serialize(slice.content); -``` - -## replace Selection -``` -const selection = editor.view.state.selection; -editor.chain().focus() - .insertContentAt( - { - from: selection.from, - to: selection.to, - }, - completion, - ) - .run(); -``` - - -## to insert after -``` -const selection = editor.view.state.selection; -editor - .chain() - .focus() - .insertContentAt(selection.to + 1, completion) - .run(); -``` diff --git a/apps/web/app/(editor)/components/aigenerate.tsx b/apps/web/app/(editor)/components/aigenerate.tsx deleted file mode 100644 index de9b2a3f..00000000 --- a/apps/web/app/(editor)/components/aigenerate.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import React, { useEffect, useRef, useState } from "react"; -import Magic from "./ui/magic"; -import CrazySpinner from "./ui/crazy-spinner"; -import Asksvg from "./ui/asksvg"; -import Rewritesvg from "./ui/rewritesvg"; -import Translatesvg from "./ui/translatesvg"; -import Autocompletesvg from "./ui/autocompletesvg"; -import { motion, AnimatePresence } from "framer-motion"; -import type { Editor } from "@tiptap/core"; -import { useEditor } from "novel"; -import { NodeSelection } from "prosemirror-state"; - -function Aigenerate() { - const [visible, setVisible] = useState(false); - const [generating, setGenerating] = useState(false); - - const { editor } = useEditor(); - const setGeneratingfn = (v: boolean) => setGenerating(v); - - return ( -
- { - setVisible(!visible); - if (visible) editor?.commands.unsetAIHighlight(); - }} - className={`select-none relative z-[70] rounded-3xl text-[#369DFD] bg-[#21303D] px-4 py-3 text-sm flex gap-2 items-center font-medium whitespace-nowrap overflow-hidden transition-[width] w-[6.25rem] ${visible && "w-[10.55rem]"}`} - > - - {visible && generating ? ( - <> - Generating - - ) : visible ? ( - <>Press Commands - ) : ( - <>Ask AI - )} - - - {/* TODO: handle Editor not initalised, maybe with a loading state. */} - -
- -
- ); -} - -export default Aigenerate; - -const options = [ - <> - - Translate - , - <> - - Change Tone - , - <> - - Ask Gemini - , - <> - - Auto Complete - , -]; - -function ToolBar({ - editor, - setGeneratingfn, -}: { - editor: Editor; - setGeneratingfn: (v: boolean) => void; -}) { - const [index, setIndex] = useState(0); - - return ( -
- {options.map((item, idx) => ( -
setIndex(idx)} - > - - {index === idx && ( - - AigenerateContent({ idx, editor, setGeneratingfn }) - } - className="absolute select-none inset-0 block h-full w-full rounded-xl bg-[#33393D]" - layoutId="hoverBackground" - initial={{ opacity: 0 }} - animate={{ - opacity: 1, - transition: { duration: 0.15 }, - }} - exit={{ - opacity: 0, - transition: { duration: 0.15, delay: 0.2 }, - }} - /> - )} - -
- {item} -
-
- ))} -
- ); -} - -async function AigenerateContent({ - idx, - editor, - setGeneratingfn, -}: { - idx: number; - editor: Editor; - setGeneratingfn: (v: boolean) => void; -}) { - setGeneratingfn(true); - - const { from, to } = editor.view.state.selection; - - const slice = editor.state.selection.content(); - const text = editor.storage.markdown.serializer.serialize(slice.content); - - const request = [ - "Translate to hindi written in english, do not write anything else", - "change tone, improve the way be more formal", - "ask, answer the question", - "continue this, minimum 80 characters, do not repeat just continue don't use ... to denote start", - ]; - - const resp = await fetch("/api/editorai", { - method: "POST", - body: JSON.stringify({ - context: text, - request: request[idx], - }), - }); - - const reader = resp.body?.getReader(); - let done = false; - let position = to; - while (!done && reader) { - const { value, done: d } = await reader.read(); - done = d; - - const decoded = new TextDecoder().decode(value); - console.log(decoded); - editor - .chain() - .focus() - .insertContentAt(position + 1, decoded) - .run(); - position += decoded.length; - } - - setGeneratingfn(false); -} diff --git a/apps/web/app/(editor)/components/editorcommands.tsx b/apps/web/app/(editor)/components/editorcommands.tsx deleted file mode 100644 index 8e80df46..00000000 --- a/apps/web/app/(editor)/components/editorcommands.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React, { useState } from "react"; -import { - EditorBubble, - EditorCommand, - EditorCommandEmpty, - EditorCommandItem, - EditorCommandList, -} from "novel"; -import { suggestionItems } from "./slash-command"; -import { Separator } from "@repo/ui/shadcn/separator"; -import { NodeSelector } from "./selectors/node-selector"; -import { LinkSelector } from "./selectors/link-selector"; -import { TextButtons } from "./selectors/text-buttons"; -import { ColorSelector } from "./selectors/color-selector"; -import { BgColorSelector } from "./selectors/bgcolor-selector"; - -function EditorCommands() { - return ( - <> - - - - ); -} - -function SlashCommand() { - return ( - - - No results - - - {suggestionItems.map((item) => ( - item.command(val)} - className="flex w-full items-center space-x-4 rounded-md px-4 py-3 text-left text-sm hover:bg-accent aria-selected:bg-[#21303D] group/command" - key={item.title} - > -
- {item.icon} -
-
-

- {item.title} -

-

- {item.description} -

-
-
- ))} -
-
- ); -} - -function PopupMenu() { - const [openNode, setOpenNode] = useState(false); - const [openColor, setOpenColor] = useState(false); - const [openBgColor, setOpenBgColor] = useState(false); - const [openLink, setOpenLink] = useState(false); - const [openMenu, setOpenMenu] = useState(false); - return ( - - - - - - - - - - - - - ); -} - -export default EditorCommands; diff --git a/apps/web/app/(editor)/components/extensions.ts b/apps/web/app/(editor)/components/extensions.ts deleted file mode 100644 index 0c581154..00000000 --- a/apps/web/app/(editor)/components/extensions.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { - AIHighlight, - CharacterCount, - CodeBlockLowlight, - GlobalDragHandle, - HorizontalRule, - Placeholder, - StarterKit, - TaskItem, - TaskList, - TiptapImage, - TiptapLink, - UpdatedImage, - Youtube, -} from "novel/extensions"; -import { UploadImagesPlugin } from "novel/plugins"; - -import { cx } from "class-variance-authority"; -import { common, createLowlight } from "lowlight"; - -//TODO I am using cx here to get tailwind autocomplete working, idk if someone else can write a regex to just capture the class key in objects -const aiHighlight = AIHighlight; -//You can overwrite the placeholder with your own configuration -const placeholder = Placeholder; -const tiptapLink = TiptapLink.configure({ - HTMLAttributes: { - class: cx( - "text-muted-foreground underline underline-offset-[3px] hover:text-primary transition-colors cursor-pointer", - ), - }, -}); - -const tiptapImage = TiptapImage.extend({ - addProseMirrorPlugins() { - return [ - UploadImagesPlugin({ - imageClass: cx("opacity-40 rounded-lg border border-stone-200"), - }), - ]; - }, -}).configure({ - allowBase64: true, - HTMLAttributes: { - class: cx("rounded-lg border border-muted"), - }, -}); - -const updatedImage = UpdatedImage.configure({ - HTMLAttributes: { - class: cx("rounded-lg border border-muted"), - }, -}); - -const taskList = TaskList.configure({ - HTMLAttributes: { - class: cx("not-prose pl-2 "), - }, -}); -const taskItem = TaskItem.configure({ - HTMLAttributes: { - class: cx("flex gap-2 items-start my-4"), - }, - nested: true, -}); - -const horizontalRule = HorizontalRule.configure({ - HTMLAttributes: { - class: cx("mt-4 mb-6 border-t border-muted-foreground"), - }, -}); - -const starterKit = StarterKit.configure({ - bulletList: { - HTMLAttributes: { - class: cx("list-disc list-outside leading-3 -mt-2"), - }, - }, - orderedList: { - HTMLAttributes: { - class: cx("list-decimal list-outside leading-3 -mt-2"), - }, - }, - listItem: { - HTMLAttributes: { - class: cx("leading-normal -mb-2"), - }, - }, - blockquote: { - HTMLAttributes: { - class: cx("border-l-4 border-primary"), - }, - }, - codeBlock: { - HTMLAttributes: { - class: cx("rounded-md bg-muted text-muted-foreground border p-5 font-mono font-medium"), - }, - }, - code: { - HTMLAttributes: { - class: cx("rounded-md bg-muted px-1.5 py-1 font-mono font-medium"), - spellcheck: "false", - }, - }, - horizontalRule: false, - dropcursor: { - color: "#DBEAFE", - width: 4, - }, - gapcursor: false, -}); - -const codeBlockLowlight = CodeBlockLowlight.configure({ - // configure lowlight: common / all / use highlightJS in case there is a need to specify certain language grammars only - // common: covers 37 language grammars which should be good enough in most cases - lowlight: createLowlight(common), -}); - -const youtube = Youtube.configure({ - HTMLAttributes: { - class: cx("rounded-lg border border-muted"), - }, - inline: false, -}); - -const characterCount = CharacterCount.configure(); - -export const defaultExtensions = [ - starterKit, - placeholder, - tiptapLink, - tiptapImage, - updatedImage, - taskList, - taskItem, - horizontalRule, - aiHighlight, - codeBlockLowlight, - youtube, - characterCount, - GlobalDragHandle, -]; diff --git a/apps/web/app/(editor)/components/image-upload.ts b/apps/web/app/(editor)/components/image-upload.ts deleted file mode 100644 index d10be168..00000000 --- a/apps/web/app/(editor)/components/image-upload.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createImageUpload } from "novel/plugins"; -import { toast } from "sonner"; - -const onUpload = (file: File) => { - //Endpoint: to upload the image - const promise = fetch("", { - method: "POST", - body: file, - }); - - return new Promise((resolve, reject) => { - toast.promise( - promise.then(async (res) => { - if (res.status === 200) { - const { url } = (await res.json()) as { url: string }; - const image = new Image(); - image.src = url; - image.onload = () => { - resolve(url); - }; - } else { - throw new Error("Error uploading image. Please try again."); - } - }), - { - loading: "Uploading image...", - success: "Image uploaded successfully.", - error: (e) => { - reject(e); - return e.message; - }, - }, - ); - }); -}; - -export const uploadFn = createImageUpload({ - onUpload, - validateFn: (file) => { - if (!file.type.includes("image/")) { - toast.error("File type not supported."); - return false; - } - if (file.size / 1024 / 1024 > 20) { - toast.error("File size too big (max 20MB)."); - return false; - } - return true; - }, -}); diff --git a/apps/web/app/(editor)/components/selectors/bgcolor-selector.tsx b/apps/web/app/(editor)/components/selectors/bgcolor-selector.tsx deleted file mode 100644 index 77da0f03..00000000 --- a/apps/web/app/(editor)/components/selectors/bgcolor-selector.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { Check, ChevronDown } from "lucide-react"; -import { EditorBubbleItem, useEditor } from "novel"; - -import { Button } from "@repo/ui/shadcn/button"; -import { Popover, PopoverContent, PopoverTrigger } from "@repo/ui/shadcn/popover"; -export interface BubbleColorMenuItem { - name: string; - color: string; -} - -const HIGHLIGHT_COLORS: BubbleColorMenuItem[] = [ - { - name: "Default", - color: "var(--novel-highlight-default)", - }, - { - name: "Purple", - color: "var(--novel-highlight-purple)", - }, - { - name: "Red", - color: "var(--novel-highlight-red)", - }, - { - name: "Yellow", - color: "var(--novel-highlight-yellow)", - }, - { - name: "Blue", - color: "var(--novel-highlight-blue)", - }, - { - name: "Green", - color: "var(--novel-highlight-green)", - }, - { - name: "Orange", - color: "var(--novel-highlight-orange)", - }, - { - name: "Pink", - color: "var(--novel-highlight-pink)", - }, - { - name: "Gray", - color: "var(--novel-highlight-gray)", - }, -]; - -interface ColorSelectorProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export const BgColorSelector = ({ open, onOpenChange }: ColorSelectorProps) => { - const { editor } = useEditor(); - - if (!editor) return null; - - const activeHighlightItem = HIGHLIGHT_COLORS.find(({ color }) => editor.isActive("highlight", { color })); - - return ( - - - - - - -
-
Background
- {HIGHLIGHT_COLORS.map(({ name, color }) => ( - { - editor.commands.unsetHighlight(); - name !== "Default" && editor.commands.setHighlight({ color }); - }} - className="flex cursor-pointer items-center justify-between px-2 py-1 text-sm hover:bg-[#21303D]" - > -
-
- A -
- {name} -
- {editor.isActive("highlight", { color }) && } -
- ))} -
-
-
- ); -}; diff --git a/apps/web/app/(editor)/components/selectors/color-selector.tsx b/apps/web/app/(editor)/components/selectors/color-selector.tsx deleted file mode 100644 index 557c4255..00000000 --- a/apps/web/app/(editor)/components/selectors/color-selector.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { Check, ChevronDown } from "lucide-react"; -import { EditorBubbleItem, useEditor } from "novel"; - -import { Button } from "@repo/ui/shadcn/button"; -import { Popover, PopoverContent, PopoverTrigger } from "@repo/ui/shadcn/popover"; -export interface BubbleColorMenuItem { - name: string; - color: string; -} - -const TEXT_COLORS: BubbleColorMenuItem[] = [ - { - name: "Default", - color: "var(--novel-black)", - }, - { - name: "Purple", - color: "#9333EA", - }, - { - name: "Red", - color: "#E00000", - }, - { - name: "Yellow", - color: "#EAB308", - }, - { - name: "Blue", - color: "#2563EB", - }, - { - name: "Green", - color: "#008A00", - }, - { - name: "Orange", - color: "#FFA500", - }, - { - name: "Pink", - color: "#BA4081", - }, - { - name: "Gray", - color: "#A8A29E", - }, -]; - - -interface ColorSelectorProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export const ColorSelector = ({ open, onOpenChange }: ColorSelectorProps) => { - const { editor } = useEditor(); - - if (!editor) return null; - const activeColorItem = TEXT_COLORS.find(({ color }) => editor.isActive("textStyle", { color })); - - return ( - - - - - - -
-
Color
- {TEXT_COLORS.map(({ name, color }) => ( - { - editor.commands.unsetColor(); - name !== "Default" && - editor - .chain() - .focus() - .setColor(color || "") - .run(); - }} - className="flex cursor-pointer items-center justify-between px-2 py-1 text-sm hover:bg-accent" - > -
-
- A -
- {name} -
-
- ))} -
-
-
- ); -}; diff --git a/apps/web/app/(editor)/components/selectors/link-selector.tsx b/apps/web/app/(editor)/components/selectors/link-selector.tsx deleted file mode 100644 index 3dc28266..00000000 --- a/apps/web/app/(editor)/components/selectors/link-selector.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { Button } from "@repo/ui/shadcn/button"; -import { Popover, PopoverContent, PopoverTrigger } from "@repo/ui/shadcn/popover"; -import { cn } from "@repo/ui/lib/utils"; -import { Check, Trash } from "lucide-react"; -import { useEditor } from "novel"; -import { useEffect, useRef } from "react"; - -export function isValidUrl(url: string) { - try { - new URL(url); - return true; - } catch (_e) { - return false; - } -} -export function getUrlFromString(str: string) { - if (isValidUrl(str)) return str; - try { - if (str.includes(".") && !str.includes(" ")) { - return new URL(`https://${str}`).toString(); - } - } catch (_e) { - return null; - } -} -interface LinkSelectorProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export const LinkSelector = ({ open, onOpenChange }: LinkSelectorProps) => { - const inputRef = useRef(null); - const { editor } = useEditor(); - - // Autofocus on input by default - useEffect(() => { - inputRef.current?.focus(); - }); - if (!editor) return null; - - return ( - - - - - -
{ - const target = e.currentTarget as HTMLFormElement; - e.preventDefault(); - const input = target[0] as HTMLInputElement; - const url = getUrlFromString(input.value); - url && editor.chain().focus().setLink({ href: url }).run(); - }} - className="flex p-1 " - > - - {editor.getAttributes("link").href ? ( - - ) : ( - - )} -
-
-
- ); -}; diff --git a/apps/web/app/(editor)/components/selectors/node-selector.tsx b/apps/web/app/(editor)/components/selectors/node-selector.tsx deleted file mode 100644 index c6092b68..00000000 --- a/apps/web/app/(editor)/components/selectors/node-selector.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { - Check, - CheckSquare, - ChevronDown, - Code, - Heading1, - Heading2, - Heading3, - ListOrdered, - type LucideIcon, - TextIcon, - TextQuote, -} from "lucide-react"; -import { EditorBubbleItem, useEditor } from "novel"; - -import { Button } from "@repo/ui/shadcn/button"; -import { Popover, PopoverContent, PopoverTrigger } from "@repo/ui/shadcn/popover"; - -export type SelectorItem = { - name: string; - icon: LucideIcon; - command: (editor: ReturnType["editor"]) => void; - isActive: (editor: ReturnType["editor"]) => boolean; -}; - -const items: SelectorItem[] = [ - { - name: "Text", - icon: TextIcon, - command: (editor) => editor.chain().focus().clearNodes().run(), - // I feel like there has to be a more efficient way to do this – feel free to PR if you know how! - isActive: (editor) => - editor.isActive("paragraph") && !editor.isActive("bulletList") && !editor.isActive("orderedList"), - }, - { - name: "Heading 1", - icon: Heading1, - command: (editor) => editor.chain().focus().clearNodes().toggleHeading({ level: 1 }).run(), - isActive: (editor) => editor.isActive("heading", { level: 1 }), - }, - { - name: "Heading 2", - icon: Heading2, - command: (editor) => editor.chain().focus().clearNodes().toggleHeading({ level: 2 }).run(), - isActive: (editor) => editor.isActive("heading", { level: 2 }), - }, - { - name: "Heading 3", - icon: Heading3, - command: (editor) => editor.chain().focus().clearNodes().toggleHeading({ level: 3 }).run(), - isActive: (editor) => editor.isActive("heading", { level: 3 }), - }, - { - name: "To-do List", - icon: CheckSquare, - command: (editor) => editor.chain().focus().clearNodes().toggleTaskList().run(), - isActive: (editor) => editor.isActive("taskItem"), - }, - { - name: "Bullet List", - icon: ListOrdered, - command: (editor) => editor.chain().focus().clearNodes().toggleBulletList().run(), - isActive: (editor) => editor.isActive("bulletList"), - }, - { - name: "Numbered List", - icon: ListOrdered, - command: (editor) => editor.chain().focus().clearNodes().toggleOrderedList().run(), - isActive: (editor) => editor.isActive("orderedList"), - }, - { - name: "Quote", - icon: TextQuote, - command: (editor) => editor.chain().focus().clearNodes().toggleBlockquote().run(), - isActive: (editor) => editor.isActive("blockquote"), - }, - { - name: "Code", - icon: Code, - command: (editor) => editor.chain().focus().clearNodes().toggleCodeBlock().run(), - isActive: (editor) => editor.isActive("codeBlock"), - }, -]; -interface NodeSelectorProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export const NodeSelector = ({ open, onOpenChange }: NodeSelectorProps) => { - const { editor } = useEditor(); - if (!editor) return null; - const activeItem = items.filter((item) => item.isActive(editor)).pop() ?? { - name: "Multiple", - }; - - return ( - - - - - - {items.map((item) => ( - { - item.command(editor); - onOpenChange(false); - }} - className="flex cursor-pointer items-center justify-between rounded-sm px-2 py-1 text-sm hover:bg-accent" - > -
-
- -
- {item.name} -
- {activeItem.name === item.name && } -
- ))} -
-
- ); -}; diff --git a/apps/web/app/(editor)/components/selectors/text-buttons.tsx b/apps/web/app/(editor)/components/selectors/text-buttons.tsx deleted file mode 100644 index f75d7b3c..00000000 --- a/apps/web/app/(editor)/components/selectors/text-buttons.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Button } from "@repo/ui/shadcn/button"; -import { Popover, PopoverContent, PopoverTrigger } from "@repo/ui/shadcn/popover"; -import { BoldIcon, CodeIcon, ItalicIcon, StrikethroughIcon, UnderlineIcon } from "lucide-react"; -import { EditorBubbleItem, useEditor } from "novel"; -import type { SelectorItem } from "./node-selector"; -import { cn } from "@repo/ui/lib/utils"; - -export const TextButtons = () => { - const { editor } = useEditor(); - if (!editor) return null; - const items: SelectorItem[] = [ - { - name: "bold", - isActive: (editor) => editor.isActive("bold"), - command: (editor) => editor.chain().focus().toggleBold().run(), - icon: BoldIcon, - }, - { - name: "italic", - isActive: (editor) => editor.isActive("italic"), - command: (editor) => editor.chain().focus().toggleItalic().run(), - icon: ItalicIcon, - }, - { - name: "underline", - isActive: (editor) => editor.isActive("underline"), - command: (editor) => editor.chain().focus().toggleUnderline().run(), - icon: UnderlineIcon, - }, - { - name: "strike", - isActive: (editor) => editor.isActive("strike"), - command: (editor) => editor.chain().focus().toggleStrike().run(), - icon: StrikethroughIcon, - }, - { - name: "code", - isActive: (editor) => editor.isActive("code"), - command: (editor) => editor.chain().focus().toggleCode().run(), - icon: CodeIcon, - }, - ]; - return ( -
- {items.map((item) => ( - { - item.command(editor); - }} - > - - - ))} -
- ); -}; diff --git a/apps/web/app/(editor)/components/slash-command.tsx b/apps/web/app/(editor)/components/slash-command.tsx deleted file mode 100644 index 1bfb1690..00000000 --- a/apps/web/app/(editor)/components/slash-command.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { - CheckSquare, - Code, - Heading1, - Heading2, - Heading3, - ImageIcon, - List, - ListOrdered, - MessageSquarePlus, - Text, - TextQuote, - Youtube -} from "lucide-react"; -import { createSuggestionItems } from "novel/extensions"; -import { Command, renderItems } from "novel/extensions"; -import { uploadFn } from "./image-upload"; - -export const suggestionItems = createSuggestionItems([ - { - title: "Send Feedback", - description: "Let us know how we can improve.", - icon: , - command: ({ editor, range }) => { - editor.chain().focus().deleteRange(range).run(); - window.open("/feedback", "_blank"); - }, - }, - { - title: "Text", - description: "Just start typing with plain text.", - searchTerms: ["p", "paragraph"], - icon: , - command: ({ editor, range }) => { - editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run(); - }, - }, - { - title: "To-do List", - description: "Track tasks with a to-do list.", - searchTerms: ["todo", "task", "list", "check", "checkbox"], - icon: , - command: ({ editor, range }) => { - editor.chain().focus().deleteRange(range).toggleTaskList().run(); - }, - }, - { - title: "Heading 1", - description: "Big section heading.", - searchTerms: ["title", "big", "large"], - icon: , - command: ({ editor, range }) => { - editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run(); - }, - }, - { - title: "Heading 2", - description: "Medium section heading.", - searchTerms: ["subtitle", "medium"], - icon: , - command: ({ editor, range }) => { - editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run(); - }, - }, - { - title: "Heading 3", - description: "Small section heading.", - searchTerms: ["subtitle", "small"], - icon: , - command: ({ editor, range }) => { - editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run(); - }, - }, - { - title: "Bullet List", - description: "Create a simple bullet list.", - searchTerms: ["unordered", "point"], - icon: , - command: ({ editor, range }) => { - editor.chain().focus().deleteRange(range).toggleBulletList().run(); - }, - }, - { - title: "Numbered List", - description: "Create a list with numbering.", - searchTerms: ["ordered"], - icon: , - command: ({ editor, range }) => { - editor.chain().focus().deleteRange(range).toggleOrderedList().run(); - }, - }, - { - title: "Quote", - description: "Capture a quote.", - searchTerms: ["blockquote"], - icon: , - command: ({ editor, range }) => - editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run(), - }, - { - title: "Code", - description: "Capture a code snippet.", - searchTerms: ["codeblock"], - icon: , - command: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(), - }, - // { - // title: "Image", - // description: "Upload an image from your computer.", - // searchTerms: ["photo", "picture", "media"], - // icon: , - // command: ({ editor, range }) => { - // editor.chain().focus().deleteRange(range).run(); - // // upload image - // const input = document.createElement("input"); - // input.type = "file"; - // input.accept = "image/*"; - // input.onchange = async () => { - // if (input.files?.length) { - // const file = input.files[0]; - // const pos = editor.view.state.selection.from; - // uploadFn(file, editor.view, pos); - // } - // }; - // input.click(); - // }, - // }, - // { - // title: "Youtube", - // description: "Embed a Youtube video.", - // searchTerms: ["video", "youtube", "embed"], - // icon: , - // command: ({ editor, range }) => { - // const videoLink = prompt("Please enter Youtube Video Link"); - // //From https://regexr.com/3dj5t - // const ytregex = new RegExp( - // /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/, - // ); - - // if (ytregex.test(videoLink)) { - // editor - // .chain() - // .focus() - // .deleteRange(range) - // .setYoutubeVideo({ - // src: videoLink, - // }) - // .run(); - // } else { - // if (videoLink !== null) { - // alert("Please enter a correct Youtube Video Link"); - // } - // } - // }, - // }, -]); - -export const slashCommand = Command.configure({ - suggestion: { - items: () => suggestionItems, - render: renderItems, - }, -}); diff --git a/apps/web/app/(editor)/components/topbar.tsx b/apps/web/app/(editor)/components/topbar.tsx deleted file mode 100644 index 49b5179c..00000000 --- a/apps/web/app/(editor)/components/topbar.tsx +++ /dev/null @@ -1,68 +0,0 @@ -"use client"; - -import { - AnimatePresence, - useMotionValueEvent, - useScroll, - motion, -} from "framer-motion"; -import React, { useState } from "react"; - -function Topbar({ - charsCount, - saveStatus, -}: { - charsCount: number | undefined; - saveStatus: string; -}) { - const [visible, setVisible] = useState(true); - - const { scrollYProgress } = useScroll(); - useMotionValueEvent(scrollYProgress, "change", (current) => { - if (typeof current === "number") { - let direction = current! - scrollYProgress.getPrevious()!; - - if (direction < 0 || direction === 1) { - setVisible(true); - } else { - setVisible(false); - } - } - }); - return ( -
- - -
-
- Untitled -
-
- {saveStatus} -
- {charsCount && ( -
- {`${charsCount} words`} -
- )} -
-
-
-
- ); -} - -export default Topbar; diff --git a/apps/web/app/(editor)/components/ui/asksvg.tsx b/apps/web/app/(editor)/components/ui/asksvg.tsx deleted file mode 100644 index aa38fe08..00000000 --- a/apps/web/app/(editor)/components/ui/asksvg.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' - -function Asksvg() { - return ( - - - - - ) -} - -export default Asksvg \ No newline at end of file diff --git a/apps/web/app/(editor)/components/ui/autocompletesvg.tsx b/apps/web/app/(editor)/components/ui/autocompletesvg.tsx deleted file mode 100644 index c433fcad..00000000 --- a/apps/web/app/(editor)/components/ui/autocompletesvg.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' - -function Autocompletesvg() { - return ( - - - - - ) -} - -export default Autocompletesvg \ No newline at end of file diff --git a/apps/web/app/(editor)/components/ui/crazy-spinner.tsx b/apps/web/app/(editor)/components/ui/crazy-spinner.tsx deleted file mode 100644 index 2e95deee..00000000 --- a/apps/web/app/(editor)/components/ui/crazy-spinner.tsx +++ /dev/null @@ -1,11 +0,0 @@ -const CrazySpinner = () => { - return ( -
-
-
-
-
- ); -}; - -export default CrazySpinner; diff --git a/apps/web/app/(editor)/components/ui/magic.tsx b/apps/web/app/(editor)/components/ui/magic.tsx deleted file mode 100644 index 04dce39e..00000000 --- a/apps/web/app/(editor)/components/ui/magic.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export default function Magic({ className }: { className: string }) { - return ( - - - - - ); -} diff --git a/apps/web/app/(editor)/components/ui/rewritesvg.tsx b/apps/web/app/(editor)/components/ui/rewritesvg.tsx deleted file mode 100644 index fad9eb90..00000000 --- a/apps/web/app/(editor)/components/ui/rewritesvg.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' - -function Rewritesvg() { - return ( - - - - ) -} - -export default Rewritesvg \ No newline at end of file diff --git a/apps/web/app/(editor)/components/ui/translatesvg.tsx b/apps/web/app/(editor)/components/ui/translatesvg.tsx deleted file mode 100644 index cde82da6..00000000 --- a/apps/web/app/(editor)/components/ui/translatesvg.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' - -function Translatesvg() { - return ( - - - - - ) -} - -export default Translatesvg \ No newline at end of file diff --git a/apps/web/app/(editor)/editor.tsx b/apps/web/app/(editor)/editor.tsx deleted file mode 100644 index f7f9a098..00000000 --- a/apps/web/app/(editor)/editor.tsx +++ /dev/null @@ -1,55 +0,0 @@ -"use client"; -import { defaultEditorContent } from "./lib/content"; -import { EditorContent, EditorRoot, type JSONContent } from "novel"; -import { ImageResizer } from "novel/extensions"; -import { useEffect, useState } from "react"; -import { defaultExtensions } from "./components/extensions"; - -import { slashCommand } from "./components/slash-command"; -import { Updates } from "./lib/debouncedsave"; -import { editorProps } from "./lib/editorprops"; -import EditorCommands from "./components/editorcommands"; -import Aigenerate from "./components/aigenerate"; -import { useMotionValueEvent, useScroll } from "framer-motion"; -import Topbar from "./components/topbar"; - -const Editor = () => { - const [initialContent, setInitialContent] = useState( - null, - ); - const [saveStatus, setSaveStatus] = useState("Saved"); - const [charsCount, setCharsCount] = useState(); - const [visible, setVisible] = useState(true); - - useEffect(() => { - if (typeof window === "undefined") return; - const content = window.localStorage.getItem("novel-content"); - if (content) setInitialContent(JSON.parse(content)); - else setInitialContent(defaultEditorContent); - }, []); - - if (!initialContent) return <>Loading...; - - return ( -
- - - { - Updates({ editor, setCharsCount, setSaveStatus }); - }} - slotAfter={} - > - - - - -
- ); -}; - -export default Editor; diff --git a/apps/web/app/(editor)/editor/page.tsx b/apps/web/app/(editor)/editor/page.tsx deleted file mode 100644 index d0298065..00000000 --- a/apps/web/app/(editor)/editor/page.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import TailwindAdvancedEditor from "../editor"; -export default function Page() { - return ( -
- -
- ); -} diff --git a/apps/web/app/(editor)/layout.tsx b/apps/web/app/(editor)/layout.tsx deleted file mode 100644 index 1bf97715..00000000 --- a/apps/web/app/(editor)/layout.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import "./styles/prosemirror.css"; -import "./styles/globals.css" -import type { ReactNode } from "react"; - - -export default function RootLayout({ children }: { children: ReactNode }) { - return ( -
- {children} -
- ); -} diff --git a/apps/web/app/(editor)/lib/content.ts b/apps/web/app/(editor)/lib/content.ts deleted file mode 100644 index 6464cfa1..00000000 --- a/apps/web/app/(editor)/lib/content.ts +++ /dev/null @@ -1,231 +0,0 @@ -export const defaultEditorContent = { - type: "doc", - content: [ - { - type: "heading", - attrs: { level: 2 }, - content: [{ type: "text", text: "Introducing Novel" }], - }, - { - type: "paragraph", - content: [ - { - type: "text", - marks: [ - { - type: "link", - attrs: { - href: "https://github.com/steven-tey/novel", - target: "_blank", - }, - }, - ], - text: "Novel", - }, - { - type: "text", - text: " is a Notion-style WYSIWYG editor with AI-powered autocompletion. Built with ", - }, - { - type: "text", - marks: [ - { - type: "link", - attrs: { - href: "https://tiptap.dev/", - target: "_blank", - }, - }, - ], - text: "Tiptap", - }, - { type: "text", text: " + " }, - { - type: "text", - marks: [ - { - type: "link", - attrs: { - href: "https://sdk.vercel.ai/docs", - target: "_blank", - }, - }, - ], - text: "Vercel AI SDK", - }, - { type: "text", text: "." }, - ], - }, - { - type: "heading", - attrs: { level: 3 }, - content: [{ type: "text", text: "Installation" }], - }, - { - type: "codeBlock", - attrs: { language: null }, - content: [{ type: "text", text: "npm i novel" }], - }, - { - type: "heading", - attrs: { level: 3 }, - content: [{ type: "text", text: "Usage" }], - }, - { - type: "codeBlock", - attrs: { language: null }, - content: [ - { - type: "text", - text: 'import { Editor } from "novel";\n\nexport default function App() {\n return (\n \n )\n}', - }, - ], - }, - { - type: "heading", - attrs: { level: 3 }, - content: [{ type: "text", text: "Features" }], - }, - { - type: "orderedList", - attrs: { tight: true, start: 1 }, - content: [ - { - type: "listItem", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: "Slash menu & bubble menu" }], - }, - ], - }, - { - type: "listItem", - content: [ - { - type: "paragraph", - content: [ - { type: "text", text: "AI autocomplete (type " }, - { type: "text", marks: [{ type: "code" }], text: "++" }, - { - type: "text", - text: " to activate, or select from slash menu)", - }, - ], - }, - ], - }, - { - type: "listItem", - content: [ - { - type: "paragraph", - content: [ - { - type: "text", - text: "Image uploads (drag & drop / copy & paste, or select from slash menu) ", - }, - ], - }, - ], - }, - ], - }, - { - type: "image", - attrs: { - src: "https://public.blob.vercel-storage.com/pJrjXbdONOnAeZAZ/banner-2wQk82qTwyVgvlhTW21GIkWgqPGD2C.png", - alt: "banner.png", - title: "banner.png", - width: null, - height: null, - }, - }, - { type: "horizontalRule" }, - { - type: "heading", - attrs: { level: 3 }, - content: [{ type: "text", text: "Learn more" }], - }, - { - type: "taskList", - content: [ - { - type: "taskItem", - attrs: { checked: false }, - content: [ - { - type: "paragraph", - content: [ - { type: "text", text: "Star us on " }, - { - type: "text", - marks: [ - { - type: "link", - attrs: { - href: "https://github.com/steven-tey/novel", - target: "_blank", - }, - }, - ], - text: "GitHub", - }, - ], - }, - ], - }, - { - type: "taskItem", - attrs: { checked: false }, - content: [ - { - type: "paragraph", - content: [ - { type: "text", text: "Install the " }, - { - type: "text", - marks: [ - { - type: "link", - attrs: { - href: "https://www.npmjs.com/package/novel", - target: "_blank", - }, - }, - ], - text: "NPM package", - }, - ], - }, - ], - }, - { - type: "taskItem", - attrs: { checked: false }, - content: [ - { - type: "paragraph", - content: [ - { - type: "text", - marks: [ - { - type: "link", - attrs: { - href: "https://vercel.com/templates/next.js/novel", - target: "_blank", - }, - }, - ], - text: "Deploy your own", - }, - { type: "text", text: " to Vercel" }, - ], - }, - ], - }, - ], - }, - ], -}; diff --git a/apps/web/app/(editor)/lib/debouncedsave.ts b/apps/web/app/(editor)/lib/debouncedsave.ts deleted file mode 100644 index 6490c6c4..00000000 --- a/apps/web/app/(editor)/lib/debouncedsave.ts +++ /dev/null @@ -1,20 +0,0 @@ -import hljs from 'highlight.js' -import { debounce } from 'tldraw'; -import { useDebouncedCallback } from "use-debounce"; - -export const Updates = debounce(({editor, setCharsCount, setSaveStatus})=> { - const json = editor.getJSON(); - setCharsCount(editor.storage.characterCount.words()); - window.localStorage.setItem("html-content", highlightCodeblocks(editor.getHTML())); - window.localStorage.setItem("novel-content", JSON.stringify(json)); - window.localStorage.setItem("markdown", editor.storage.markdown.getMarkdown()); - setSaveStatus("Saved"); -}, 500) - -export const highlightCodeblocks = (content: string) => { - const doc = new DOMParser().parseFromString(content, 'text/html'); - doc.querySelectorAll('pre code').forEach((el) => { - hljs.highlightElement(el); - }); - return new XMLSerializer().serializeToString(doc); -}; \ No newline at end of file diff --git a/apps/web/app/(editor)/lib/editorprops.ts b/apps/web/app/(editor)/lib/editorprops.ts deleted file mode 100644 index 00d89264..00000000 --- a/apps/web/app/(editor)/lib/editorprops.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { handleCommandNavigation } from "novel/extensions"; -import { handleImageDrop, handleImagePaste } from "novel/plugins"; -import { uploadFn } from "../components/image-upload"; -import { EditorView } from "prosemirror-view"; - -export const editorProps = { - handleDOMEvents: { - keydown: (_view: EditorView, event: KeyboardEvent) => handleCommandNavigation(event), - }, - handlePaste: (view: EditorView, event: ClipboardEvent) => handleImagePaste(view, event, uploadFn), - handleDrop: (view: EditorView, event: DragEvent, slice, moved:boolean) => handleImageDrop(view, event, moved, uploadFn), - attributes: { - class: - "prose prose-lg dark:prose-invert prose-headings:font-title font-default focus:outline-none max-w-full", - }, -} \ No newline at end of file diff --git a/apps/web/app/(editor)/lib/use-local-storage.ts b/apps/web/app/(editor)/lib/use-local-storage.ts deleted file mode 100644 index 5f2ebeb9..00000000 --- a/apps/web/app/(editor)/lib/use-local-storage.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect, useState } from "react"; - -const useLocalStorage = ( - key: string, - initialValue: T, - // eslint-disable-next-line no-unused-vars -): [T, (value: T) => void] => { - const [storedValue, setStoredValue] = useState(initialValue); - - useEffect(() => { - // Retrieve from localStorage - const item = window.localStorage.getItem(key); - if (item) { - setStoredValue(JSON.parse(item)); - } - }, [key]); - - const setValue = (value: T) => { - // Save state - setStoredValue(value); - // Save to localStorage - window.localStorage.setItem(key, JSON.stringify(value)); - }; - return [storedValue, setValue]; -}; - -export default useLocalStorage; diff --git a/apps/web/app/(editor)/styles/globals.css b/apps/web/app/(editor)/styles/globals.css deleted file mode 100644 index 336e2dae..00000000 --- a/apps/web/app/(editor)/styles/globals.css +++ /dev/null @@ -1,168 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; - - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; - - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; - - --radius: 0.5rem; - - --novel-highlight-default: #ffffff; - --novel-highlight-purple: #f6f3f8; - --novel-highlight-red: #fdebeb; - --novel-highlight-yellow: #fbf4a2; - --novel-highlight-blue: #c1ecf9; - --novel-highlight-green: #acf79f; - --novel-highlight-orange: #faebdd; - --novel-highlight-pink: #faf1f5; - --novel-highlight-gray: #f1f1ef; - } - - .dark { - --background: #171B1F; - --foreground: 210 40% 98%; - - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; - - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; - - --novel-highlight-default: #000000; - --novel-highlight-purple: #3f2c4b; - --novel-highlight-red: #5c1a1a; - --novel-highlight-yellow: #5c4b1a; - --novel-highlight-blue: #1a3d5c; - --novel-highlight-green: #1a5c20; - --novel-highlight-orange: #5c3a1a; - --novel-highlight-pink: #5c1a3a; - --novel-highlight-gray: #3a3a3a; - } -} - -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } -} - - -pre { - background: #0d0d0d; - border-radius: 0.5rem; - color: #fff; - font-family: "JetBrainsMono", monospace; - padding: 0.75rem 1rem; - - code { - background: none; - color: inherit; - font-size: 0.8rem; - padding: 0; - } - - .hljs-comment, - .hljs-quote { - color: #616161; - } - - .hljs-variable, - .hljs-template-variable, - .hljs-attribute, - .hljs-tag, - .hljs-name, - .hljs-regexp, - .hljs-link, - .hljs-name, - .hljs-selector-id, - .hljs-selector-class { - color: #f98181; - } - - .hljs-number, - .hljs-meta, - .hljs-built_in, - .hljs-builtin-name, - .hljs-literal, - .hljs-type, - .hljs-params { - color: #fbbc88; - } - - .hljs-string, - .hljs-symbol, - .hljs-bullet { - color: #b9f18d; - } - - .hljs-title, - .hljs-section { - color: #faf594; - } - - .hljs-keyword, - .hljs-selector-tag { - color: #70cff8; - } - - .hljs-emphasis { - font-style: italic; - } - - .hljs-strong { - font-weight: 700; - } -} -::-webkit-scrollbar{ - width: 0; -} - diff --git a/apps/web/app/(editor)/styles/prosemirror.css b/apps/web/app/(editor)/styles/prosemirror.css deleted file mode 100644 index 7298c98b..00000000 --- a/apps/web/app/(editor)/styles/prosemirror.css +++ /dev/null @@ -1,203 +0,0 @@ -.ProseMirror { - @apply p-12 px-8 sm:px-12; -} - -.ProseMirror .is-editor-empty:first-child::before { - content: attr(data-placeholder); - float: left; - color: hsl(var(--muted-foreground)); - pointer-events: none; - height: 0; -} -.ProseMirror .is-empty::before { - content: attr(data-placeholder); - float: left; - color: hsl(var(--muted-foreground)); - pointer-events: none; - height: 0; -} - -/* Custom image styles */ - -.ProseMirror img { - transition: filter 0.1s ease-in-out; - - &:hover { - cursor: pointer; - filter: brightness(90%); - } - - &.ProseMirror-selectednode { - outline: 3px solid #5abbf7; - filter: brightness(90%); - } -} - -.img-placeholder { - position: relative; - - &:before { - content: ""; - box-sizing: border-box; - position: absolute; - top: 50%; - left: 50%; - width: 36px; - height: 36px; - border-radius: 50%; - border: 3px solid var(--novel-stone-200); - border-top-color: var(--novel-stone-800); - animation: spinning 0.6s linear infinite; - } -} - -@keyframes spinning { - to { - transform: rotate(360deg); - } -} - -/* Custom TODO list checkboxes – shoutout to this awesome tutorial: https://moderncss.dev/pure-css-custom-checkbox-style/ */ - -ul[data-type="taskList"] li > label { - margin-right: 0.2rem; - user-select: none; -} - -@media screen and (max-width: 768px) { - ul[data-type="taskList"] li > label { - margin-right: 0.5rem; - } -} - -ul[data-type="taskList"] li > label input[type="checkbox"] { - -webkit-appearance: none; - appearance: none; - background-color: hsl(var(--background)); - margin: 0; - cursor: pointer; - width: 1.2em; - height: 1.2em; - position: relative; - top: 5px; - border: 2px solid hsl(var(--border)); - margin-right: 0.3rem; - display: grid; - place-content: center; - - &:hover { - background-color: hsl(var(--accent)); - } - - &:active { - background-color: hsl(var(--accent)); - } - - &::before { - content: ""; - width: 0.65em; - height: 0.65em; - transform: scale(0); - transition: 120ms transform ease-in-out; - box-shadow: inset 1em 1em; - transform-origin: center; - clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); - } - - &:checked::before { - transform: scale(1); - } -} - -ul[data-type="taskList"] li[data-checked="true"] > div > p { - color: var(--muted-foreground); - text-decoration: line-through; - text-decoration-thickness: 2px; -} - -/* Overwrite tippy-box original max-width */ - -.tippy-box { - max-width: 400px !important; -} - -.ProseMirror:not(.dragging) .ProseMirror-selectednode { - outline: none !important; - background-color: var(--novel-highlight-blue); - transition: background-color 0.2s; - box-shadow: none; -} - -.drag-handle { - position: fixed; - opacity: 1; - transition: opacity ease-in 0.2s; - border-radius: 0.25rem; - - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' style='fill: rgba(0, 0, 0, 0.5)'%3E%3Cpath d='M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z'%3E%3C/path%3E%3C/svg%3E"); - background-size: calc(0.5em + 0.375rem) calc(0.5em + 0.375rem); - background-repeat: no-repeat; - background-position: center; - width: 1.2rem; - height: 1.5rem; - z-index: 50; - cursor: grab; - - &:hover { - background-color: var(--novel-stone-100); - transition: background-color 0.2s; - } - - &:active { - background-color: var(--novel-stone-200); - transition: background-color 0.2s; - cursor: grabbing; - } - - &.hide { - opacity: 0; - pointer-events: none; - } - - @media screen and (max-width: 600px) { - display: none; - pointer-events: none; - } -} - -.dark .drag-handle { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' style='fill: rgba(255, 255, 255, 0.5)'%3E%3Cpath d='M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z'%3E%3C/path%3E%3C/svg%3E"); -} - -/* Custom Youtube Video CSS */ -iframe { - border: 8px solid #ffd00027; - border-radius: 4px; - min-width: 200px; - min-height: 200px; - display: block; - outline: 0px solid transparent; -} - -div[data-youtube-video] > iframe { - cursor: move; - aspect-ratio: 16 / 9; - width: 100%; -} - -.ProseMirror-selectednode iframe { - transition: outline 0.15s; - outline: 6px solid #fbbf24; -} - -@media only screen and (max-width: 480px) { - div[data-youtube-video] > iframe { - max-height: 50px; - } -} - -@media only screen and (max-width: 720px) { - div[data-youtube-video] > iframe { - max-height: 100px; - } -} \ No newline at end of file diff --git a/packages/ui/components/canvas/draggableComponent.tsx b/packages/ui/components/canvas/draggableComponent.tsx new file mode 100644 index 00000000..da3c42c7 --- /dev/null +++ b/packages/ui/components/canvas/draggableComponent.tsx @@ -0,0 +1,72 @@ +import Image from "next/image"; +import { useRef, useState } from "react"; + +interface DraggableComponentsProps { + content: string; + extraInfo?: string; + icon: string; + iconAlt: string; +} + +export default function DraggableComponentsContainer({ + content, +}: { + content: DraggableComponentsProps[]; +}) { + return ( +
+ {content.map((i) => { + return ( + + ); + })} +
+ ); +} + +function DraggableComponents({ + content, + extraInfo, + icon, + iconAlt, +}: DraggableComponentsProps) { + const [isDragging, setIsDragging] = useState(false); + const containerRef = useRef(null); + + const handleDragStart = (event: React.DragEvent) => { + setIsDragging(true); + if (containerRef.current) { + // Serialize the children as a string for dataTransfer + const childrenHtml = containerRef.current.innerHTML; + event.dataTransfer.setData("text/html", childrenHtml); + } + }; + + const handleDragEnd = () => { + setIsDragging(false); + }; + + + return ( +
+ {iconAlt} +
+
+

{content}

+
+

{extraInfo}

+
+
+ ); +} diff --git a/packages/ui/icons/autocomplete.svg b/packages/ui/icons/autocomplete.svg new file mode 100644 index 00000000..a4452242 --- /dev/null +++ b/packages/ui/icons/autocomplete.svg @@ -0,0 +1,15 @@ + + + \ No newline at end of file diff --git a/packages/ui/icons/block.svg b/packages/ui/icons/block.svg new file mode 100644 index 00000000..ca4cc20e --- /dev/null +++ b/packages/ui/icons/block.svg @@ -0,0 +1,15 @@ + + + \ No newline at end of file diff --git a/packages/ui/icons/canvas.svg b/packages/ui/icons/canvas.svg new file mode 100644 index 00000000..529c75de --- /dev/null +++ b/packages/ui/icons/canvas.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/app/(canvas)/svg.tsx b/packages/ui/icons/drag.svg similarity index 51% rename from apps/web/app/(canvas)/svg.tsx rename to packages/ui/icons/drag.svg index bae4e614..366a4cb0 100644 --- a/apps/web/app/(canvas)/svg.tsx +++ b/packages/ui/icons/drag.svg @@ -1,24 +1,3 @@ -export function SettingsSvg() { - return ( - - - - ); -} - -export function DragSvg() { - return ( - - ); -} - -export function TextLoadingSvg() { - return ( - - - - ); -} - -export function ThreeDBlock() { - return ( - - - - ); -} - -export function LinkSvg() { - return ( - - - - ); -} + \ No newline at end of file diff --git a/packages/ui/icons/index.ts b/packages/ui/icons/index.ts index 7788c20f..88d4ebc0 100644 --- a/packages/ui/icons/index.ts +++ b/packages/ui/icons/index.ts @@ -8,6 +8,13 @@ import SelectIcon from "./select.svg"; import SearchIcon from "./search.svg"; import NextIcon from "./nextarrow.svg"; import UrlIcon from "./url.svg"; +import CanvasIcon from "./canvas.svg"; +import blockIcon from "./block.svg"; +import LinkIcon from "./link.svg"; +import AutocompleteIcon from "./autocomplete.svg"; +import BlockIcon from "./block.svg"; +import DragIcon from "./drag.svg"; +import SettingsIcon from "./settings.svg"; export { AddIcon, @@ -19,5 +26,12 @@ export { SelectIcon, SearchIcon, NextIcon, - UrlIcon + UrlIcon, + CanvasIcon, + blockIcon, + LinkIcon, + AutocompleteIcon, + BlockIcon, + DragIcon, + SettingsIcon }; diff --git a/packages/ui/icons/link.svg b/packages/ui/icons/link.svg new file mode 100644 index 00000000..25e2d04e --- /dev/null +++ b/packages/ui/icons/link.svg @@ -0,0 +1,15 @@ + + + \ No newline at end of file diff --git a/packages/ui/icons/settings.svg b/packages/ui/icons/settings.svg new file mode 100644 index 00000000..2059b976 --- /dev/null +++ b/packages/ui/icons/settings.svg @@ -0,0 +1,14 @@ + + + \ No newline at end of file