From 081ab3bd6367d6e2e5660da1f2615ce322def85c Mon Sep 17 00:00:00 2001 From: andrewdoro Date: Thu, 2 May 2024 17:51:48 +0300 Subject: [PATCH] feat: fix biome linting --- .gitignore | 2 - .vscode/extensions.json | 8 + .vscode/settings.json | 29 ++++ apps/web/app/api/generate/route.ts | 108 ++++++------ apps/web/app/api/upload/route.ts | 16 +- apps/web/app/layout.tsx | 22 +-- apps/web/app/page.tsx | 17 +- apps/web/app/providers.tsx | 18 +- .../components/tailwind/advanced-editor.tsx | 51 +++--- apps/web/components/tailwind/extensions.ts | 38 ++--- .../generative/ai-completion-command.tsx | 8 +- .../generative/ai-selector-commands.tsx | 28 ++-- .../tailwind/generative/ai-selector.tsx | 48 +++--- .../generative/generative-menu-switch.tsx | 16 +- apps/web/components/tailwind/image-upload.ts | 31 ++-- .../tailwind/selectors/color-selector.tsx | 108 ++++++------ .../tailwind/selectors/link-selector.tsx | 32 ++-- .../tailwind/selectors/node-selector.tsx | 64 +++---- .../tailwind/selectors/text-buttons.tsx | 38 ++--- .../web/components/tailwind/slash-command.tsx | 90 +++++----- apps/web/components/tailwind/ui/button.tsx | 38 ++--- apps/web/components/tailwind/ui/command.tsx | 30 ++-- apps/web/components/tailwind/ui/dialog.tsx | 26 +-- .../tailwind/ui/icons/crazy-spinner.tsx | 8 +- .../tailwind/ui/icons/font-default.tsx | 1 + .../tailwind/ui/icons/font-mono.tsx | 1 + .../tailwind/ui/icons/font-serif.tsx | 2 + .../components/tailwind/ui/icons/github.tsx | 14 -- .../components/tailwind/ui/icons/index.tsx | 8 +- .../tailwind/ui/icons/loading-circle.tsx | 2 +- .../components/tailwind/ui/icons/magic.tsx | 2 + apps/web/components/tailwind/ui/menu.tsx | 22 +-- apps/web/components/tailwind/ui/popover.tsx | 12 +- .../components/tailwind/ui/scroll-area.tsx | 18 +- apps/web/components/tailwind/ui/separator.tsx | 12 +- apps/web/hooks/use-local-storage.ts | 2 +- apps/web/lib/content.ts | 158 +++++++++--------- apps/web/lib/utils.ts | 4 +- apps/web/styles/fonts.ts | 34 ++-- biome.json | 50 +++--- .../src/components/editor-bubble-item.tsx | 16 +- .../headless/src/components/editor-bubble.tsx | 22 +-- .../src/components/editor-command-item.tsx | 16 +- .../src/components/editor-command.tsx | 30 ++-- packages/headless/src/components/editor.tsx | 23 ++- packages/headless/src/components/index.ts | 16 +- .../headless/src/extensions/ai-highlight.ts | 22 +-- .../headless/src/extensions/custom-keymap.ts | 8 +- .../headless/src/extensions/image-resizer.tsx | 40 ++--- packages/headless/src/extensions/index.ts | 44 ++--- .../headless/src/extensions/slash-command.tsx | 59 +++---- .../headless/src/extensions/updated-image.ts | 4 +- packages/headless/src/plugins/index.ts | 2 +- .../headless/src/plugins/upload-images.tsx | 23 +-- packages/headless/src/utils/atoms.ts | 6 +- packages/headless/src/utils/store.ts | 2 +- packages/headless/src/utils/utils.ts | 19 +-- 57 files changed, 784 insertions(+), 784 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json delete mode 100644 apps/web/components/tailwind/ui/icons/github.tsx diff --git a/.gitignore b/.gitignore index d4d7f693d..1131dbbb2 100644 --- a/.gitignore +++ b/.gitignore @@ -37,8 +37,6 @@ yarn.lock *.tsbuildinfo next-env.d.ts -# vscode -.vscode # intellij .idea diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..7c9694d6e --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "yoavbls.pretty-ts-errors", + "bradlc.vscode-tailwindcss", + "biomejs.biome" + ] + } + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..8b9879b01 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,29 @@ +{ + "editor.codeActionsOnSave": { + "source.organizeImports.biome": "explicit", + "source.fixAll.biome": "explicit", + // "quickfix.biome": "explicit" + }, + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true, + "tailwindCSS.experimental.classRegex": [ + ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], + ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] + ], + "typescript.enablePromptUseWorkspaceTsdk": true, + "typescript.tsdk": "node_modules/typescript/lib", + + "typescript.preferences.autoImportFileExcludePatterns": [ + "next/router.d.ts", + "next/dist/client/router.d.ts" + ], + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features" + } +} diff --git a/apps/web/app/api/generate/route.ts b/apps/web/app/api/generate/route.ts index d19485650..ecd9effb8 100644 --- a/apps/web/app/api/generate/route.ts +++ b/apps/web/app/api/generate/route.ts @@ -1,48 +1,42 @@ -import OpenAI from 'openai'; -import { OpenAIStream, StreamingTextResponse } from 'ai'; -import { kv } from '@vercel/kv'; -import { Ratelimit } from '@upstash/ratelimit'; -import { match } from 'ts-pattern'; -import type { ChatCompletionMessageParam } from 'openai/resources/index.mjs'; +import { Ratelimit } from "@upstash/ratelimit"; +import { kv } from "@vercel/kv"; +import { OpenAIStream, StreamingTextResponse } from "ai"; +import OpenAI from "openai"; +import type { ChatCompletionMessageParam } from "openai/resources/index.mjs"; +import { match } from "ts-pattern"; // Create an OpenAI API client (that's edge friendly!) -// Using LLamma's OpenAI client: // IMPORTANT! Set the runtime to edge: https://vercel.com/docs/functions/edge-functions/edge-runtime -export const runtime = 'edge'; - -const llama = new OpenAI({ - apiKey: 'ollama', - baseURL: 'http://localhost:11434/v1', -}); +export const runtime = "edge"; export async function POST(req: Request): Promise { const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, - baseURL: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1', + baseURL: process.env.OPENAI_BASE_URL || "https://api.openai.com/v1", }); // Check if the OPENAI_API_KEY is set, if not return 400 - if (!process.env.OPENAI_API_KEY || process.env.OPENAI_API_KEY === '') { - return new Response('Missing OPENAI_API_KEY - make sure to add it to your .env file.', { + if (!process.env.OPENAI_API_KEY || process.env.OPENAI_API_KEY === "") { + return new Response("Missing OPENAI_API_KEY - make sure to add it to your .env file.", { status: 400, }); } if (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN) { - const ip = req.headers.get('x-forwarded-for'); + const ip = req.headers.get("x-forwarded-for"); const ratelimit = new Ratelimit({ redis: kv, - limiter: Ratelimit.slidingWindow(50, '1 d'), + limiter: Ratelimit.slidingWindow(50, "1 d"), }); const { success, limit, reset, remaining } = await ratelimit.limit(`novel_ratelimit_${ip}`); if (!success) { - return new Response('You have reached your request limit for the day.', { + return new Response("You have reached your request limit for the day.", { status: 429, headers: { - 'X-RateLimit-Limit': limit.toString(), - 'X-RateLimit-Remaining': remaining.toString(), - 'X-RateLimit-Reset': reset.toString(), + "X-RateLimit-Limit": limit.toString(), + "X-RateLimit-Remaining": remaining.toString(), + "X-RateLimit-Reset": reset.toString(), }, }); } @@ -50,86 +44,86 @@ export async function POST(req: Request): Promise { const { prompt, option, command } = await req.json(); const messages = match(option) - .with('continue', () => [ + .with("continue", () => [ { - role: 'system', + role: "system", content: - 'You are an AI writing assistant that continues existing text based on context from prior text. ' + - 'Give more weight/priority to the later characters than the beginning ones. ' + - 'Limit your response to no more than 200 characters, but make sure to construct complete sentences.' + - 'Use Markdown formatting when appropriate.', + "You are an AI writing assistant that continues existing text based on context from prior text. " + + "Give more weight/priority to the later characters than the beginning ones. " + + "Limit your response to no more than 200 characters, but make sure to construct complete sentences." + + "Use Markdown formatting when appropriate.", }, { - role: 'user', + role: "user", content: prompt, }, ]) - .with('improve', () => [ + .with("improve", () => [ { - role: 'system', + role: "system", content: - 'You are an AI writing assistant that improves existing text. ' + - 'Limit your response to no more than 200 characters, but make sure to construct complete sentences.' + - 'Use Markdown formatting when appropriate.', + "You are an AI writing assistant that improves existing text. " + + "Limit your response to no more than 200 characters, but make sure to construct complete sentences." + + "Use Markdown formatting when appropriate.", }, { - role: 'user', + role: "user", content: `The existing text is: ${prompt}`, }, ]) - .with('shorter', () => [ + .with("shorter", () => [ { - role: 'system', + role: "system", content: - 'You are an AI writing assistant that shortens existing text. ' + 'Use Markdown formatting when appropriate.', + "You are an AI writing assistant that shortens existing text. " + "Use Markdown formatting when appropriate.", }, { - role: 'user', + role: "user", content: `The existing text is: ${prompt}`, }, ]) - .with('longer', () => [ + .with("longer", () => [ { - role: 'system', + role: "system", content: - 'You are an AI writing assistant that lengthens existing text. ' + - 'Use Markdown formatting when appropriate.', + "You are an AI writing assistant that lengthens existing text. " + + "Use Markdown formatting when appropriate.", }, { - role: 'user', + role: "user", content: `The existing text is: ${prompt}`, }, ]) - .with('fix', () => [ + .with("fix", () => [ { - role: 'system', + role: "system", content: - 'You are an AI writing assistant that fixes grammar and spelling errors in existing text. ' + - 'Limit your response to no more than 200 characters, but make sure to construct complete sentences.' + - 'Use Markdown formatting when appropriate.', + "You are an AI writing assistant that fixes grammar and spelling errors in existing text. " + + "Limit your response to no more than 200 characters, but make sure to construct complete sentences." + + "Use Markdown formatting when appropriate.", }, { - role: 'user', + role: "user", content: `The existing text is: ${prompt}`, }, ]) - .with('zap', () => [ + .with("zap", () => [ { - role: 'system', + role: "system", content: - 'You area an AI writing assistant that generates text based on a prompt. ' + - 'You take an input from the user and a command for manipulating the text' + - 'Use Markdown formatting when appropriate.', + "You area an AI writing assistant that generates text based on a prompt. " + + "You take an input from the user and a command for manipulating the text" + + "Use Markdown formatting when appropriate.", }, { - role: 'user', + role: "user", content: `For this text: ${prompt}. You have to respect the command: ${command}`, }, ]) .run() as ChatCompletionMessageParam[]; const response = await openai.chat.completions.create({ - model: 'gpt-3.5-turbo', + model: "gpt-3.5-turbo", stream: true, messages, temperature: 0.7, diff --git a/apps/web/app/api/upload/route.ts b/apps/web/app/api/upload/route.ts index 4cc76ae22..ce7f15136 100644 --- a/apps/web/app/api/upload/route.ts +++ b/apps/web/app/api/upload/route.ts @@ -1,7 +1,7 @@ -import { put } from '@vercel/blob'; -import { NextResponse } from 'next/server'; +import { put } from "@vercel/blob"; +import { NextResponse } from "next/server"; -export const runtime = 'edge'; +export const runtime = "edge"; export async function POST(req: Request) { if (!process.env.BLOB_READ_WRITE_TOKEN) { @@ -10,16 +10,16 @@ export async function POST(req: Request) { }); } - const file = req.body || ''; - const filename = req.headers.get('x-vercel-filename') || 'file.txt'; - const contentType = req.headers.get('content-type') || 'text/plain'; - const fileType = `.${contentType.split('/')[1]}`; + const file = req.body || ""; + const filename = req.headers.get("x-vercel-filename") || "file.txt"; + const contentType = req.headers.get("content-type") || "text/plain"; + const fileType = `.${contentType.split("/")[1]}`; // construct final filename based on content-type if not provided const finalName = filename.includes(fileType) ? filename : `${filename}${fileType}`; const blob = await put(finalName, file, { contentType, - access: 'public', + access: "public", }); return NextResponse.json(blob); diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index d4b27560e..cbe30e1c0 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,13 +1,13 @@ -import '@/styles/globals.css'; -import '@/styles/prosemirror.css'; +import "@/styles/globals.css"; +import "@/styles/prosemirror.css"; -import type { Metadata, Viewport } from 'next'; -import type { ReactNode } from 'react'; -import Providers from './providers'; +import type { Metadata, Viewport } from "next"; +import type { ReactNode } from "react"; +import Providers from "./providers"; -const title = 'Novel - Notion-style WYSIWYG editor with AI-powered autocompletions'; +const title = "Novel - Notion-style WYSIWYG editor with AI-powered autocompletions"; const description = - 'Novel is a Notion-style WYSIWYG editor with AI-powered autocompletions. Built with Tiptap, OpenAI, and Vercel AI SDK.'; + "Novel is a Notion-style WYSIWYG editor with AI-powered autocompletions. Built with Tiptap, OpenAI, and Vercel AI SDK."; export const metadata: Metadata = { title, @@ -19,14 +19,14 @@ export const metadata: Metadata = { twitter: { title, description, - card: 'summary_large_image', - creator: '@steventey', + card: "summary_large_image", + creator: "@steventey", }, - metadataBase: new URL('https://novel.sh'), + metadataBase: new URL("https://novel.sh"), }; export const viewport: Viewport = { - themeColor: '#ffffff', + themeColor: "#ffffff", }; export default function RootLayout({ children }: { children: ReactNode }) { diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 33b323ae6..06b8c2acc 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,11 +1,10 @@ -import { Github } from '@/components/tailwind/ui/icons'; -import { Button } from '@/components/tailwind/ui/button'; -import Menu from '@/components/tailwind/ui/menu'; -import Link from 'next/link'; -import TailwindAdvancedEditor from '@/components/tailwind/advanced-editor'; -import { Dialog, DialogContent, DialogTrigger } from '@/components/tailwind/ui/dialog'; -import { ScrollArea } from '@/components/tailwind/ui/scroll-area'; -import { BookOpen } from 'lucide-react'; +import TailwindAdvancedEditor from "@/components/tailwind/advanced-editor"; +import { Button } from "@/components/tailwind/ui/button"; +import { Dialog, DialogContent, DialogTrigger } from "@/components/tailwind/ui/dialog"; +import Menu from "@/components/tailwind/ui/menu"; +import { ScrollArea } from "@/components/tailwind/ui/scroll-area"; +import { BookOpen, GithubIcon } from "lucide-react"; +import Link from "next/link"; export default function Page() { return ( @@ -13,7 +12,7 @@ export default function Page() {
diff --git a/apps/web/app/providers.tsx b/apps/web/app/providers.tsx index 9769aa6d5..ddce1c6d7 100644 --- a/apps/web/app/providers.tsx +++ b/apps/web/app/providers.tsx @@ -1,28 +1,28 @@ -'use client'; +"use client"; -import { type Dispatch, type ReactNode, type SetStateAction, createContext } from 'react'; -import { ThemeProvider, useTheme } from 'next-themes'; -import { Toaster } from 'sonner'; -import { Analytics } from '@vercel/analytics/react'; -import useLocalStorage from '@/hooks/use-local-storage'; +import { type Dispatch, type ReactNode, type SetStateAction, createContext } from "react"; +import { ThemeProvider, useTheme } from "next-themes"; +import { Toaster } from "sonner"; +import { Analytics } from "@vercel/analytics/react"; +import useLocalStorage from "@/hooks/use-local-storage"; export const AppContext = createContext<{ font: string; setFont: Dispatch>; }>({ - font: 'Default', + font: "Default", setFont: () => {}, }); const ToasterProvider = () => { const { theme } = useTheme() as { - theme: 'light' | 'dark' | 'system'; + theme: "light" | "dark" | "system"; }; return ; }; export default function Providers({ children }: { children: ReactNode }) { - const [font, setFont] = useLocalStorage('novel__font', 'Default'); + const [font, setFont] = useLocalStorage("novel__font", "Default"); return ( diff --git a/apps/web/components/tailwind/advanced-editor.tsx b/apps/web/components/tailwind/advanced-editor.tsx index 4c49cffd0..5e2c57662 100644 --- a/apps/web/components/tailwind/advanced-editor.tsx +++ b/apps/web/components/tailwind/advanced-editor.tsx @@ -1,5 +1,5 @@ -'use client'; -import { defaultEditorContent } from '@/lib/content'; +"use client"; +import { defaultEditorContent } from "@/lib/content"; import { EditorCommand, EditorCommandEmpty, @@ -9,27 +9,27 @@ import { type EditorInstance, EditorRoot, type JSONContent, -} from 'novel'; -import { ImageResizer, handleCommandNavigation } from 'novel/extensions'; -import { useEffect, useState } from 'react'; -import { useDebouncedCallback } from 'use-debounce'; -import { defaultExtensions } from './extensions'; -import { ColorSelector } from './selectors/color-selector'; -import { LinkSelector } from './selectors/link-selector'; -import { NodeSelector } from './selectors/node-selector'; -import { Separator } from './ui/separator'; +} from "novel"; +import { ImageResizer, handleCommandNavigation } from "novel/extensions"; +import { useEffect, useState } from "react"; +import { useDebouncedCallback } from "use-debounce"; +import { defaultExtensions } from "./extensions"; +import { ColorSelector } from "./selectors/color-selector"; +import { LinkSelector } from "./selectors/link-selector"; +import { NodeSelector } from "./selectors/node-selector"; +import { Separator } from "./ui/separator"; -import { handleImageDrop, handleImagePaste } from 'novel/plugins'; -import GenerativeMenuSwitch from './generative/generative-menu-switch'; -import { uploadFn } from './image-upload'; -import { TextButtons } from './selectors/text-buttons'; -import { slashCommand, suggestionItems } from './slash-command'; +import { handleImageDrop, handleImagePaste } from "novel/plugins"; +import GenerativeMenuSwitch from "./generative/generative-menu-switch"; +import { uploadFn } from "./image-upload"; +import { TextButtons } from "./selectors/text-buttons"; +import { slashCommand, suggestionItems } from "./slash-command"; const extensions = [...defaultExtensions, slashCommand]; const TailwindAdvancedEditor = () => { const [initialContent, setInitialContent] = useState(null); - const [saveStatus, setSaveStatus] = useState('Saved'); + const [saveStatus, setSaveStatus] = useState("Saved"); const [openNode, setOpenNode] = useState(false); const [openColor, setOpenColor] = useState(false); @@ -38,14 +38,14 @@ const TailwindAdvancedEditor = () => { const debouncedUpdates = useDebouncedCallback(async (editor: EditorInstance) => { const json = editor.getJSON(); - window.localStorage.setItem('html-content', editor.getHTML()); - window.localStorage.setItem('novel-content', JSON.stringify(json)); - window.localStorage.setItem('markdown', editor.storage.markdown.getMarkdown()); - setSaveStatus('Saved'); + window.localStorage.setItem("html-content", editor.getHTML()); + window.localStorage.setItem("novel-content", JSON.stringify(json)); + window.localStorage.setItem("markdown", editor.storage.markdown.getMarkdown()); + setSaveStatus("Saved"); }, 500); useEffect(() => { - const content = window.localStorage.getItem('novel-content'); + const content = window.localStorage.getItem("novel-content"); if (content) setInitialContent(JSON.parse(content)); else setInitialContent(defaultEditorContent); }, []); @@ -69,12 +69,13 @@ const TailwindAdvancedEditor = () => { handlePaste: (view, event) => handleImagePaste(view, event, uploadFn), handleDrop: (view, event, _slice, moved) => 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`, + class: + "prose prose-lg dark:prose-invert prose-headings:font-title font-default focus:outline-none max-w-full", }, }} onUpdate={({ editor }) => { debouncedUpdates(editor); - setSaveStatus('Unsaved'); + setSaveStatus("Unsaved"); }} slotAfter={} > @@ -85,7 +86,7 @@ const TailwindAdvancedEditor = () => { item.command(val)} - className={`flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm hover:bg-accent aria-selected:bg-accent `} + className="flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm hover:bg-accent aria-selected:bg-accent" key={item.title} >
diff --git a/apps/web/components/tailwind/extensions.ts b/apps/web/components/tailwind/extensions.ts index 86f74d4a5..dd76fdb01 100644 --- a/apps/web/components/tailwind/extensions.ts +++ b/apps/web/components/tailwind/extensions.ts @@ -9,11 +9,11 @@ import { TiptapImage, TiptapLink, UpdatedImage, -} from 'novel/extensions'; -import { UploadImagesPlugin } from 'novel/plugins'; +} from "novel/extensions"; +import { UploadImagesPlugin } from "novel/plugins"; -import { cx } from 'class-variance-authority'; -import { common, createLowlight } from 'lowlight'; +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; @@ -22,7 +22,7 @@ const placeholder = Placeholder; const tiptapLink = TiptapLink.configure({ HTMLAttributes: { class: cx( - 'text-muted-foreground underline underline-offset-[3px] hover:text-primary transition-colors cursor-pointer', + "text-muted-foreground underline underline-offset-[3px] hover:text-primary transition-colors cursor-pointer", ), }, }); @@ -31,76 +31,76 @@ const tiptapImage = TiptapImage.extend({ addProseMirrorPlugins() { return [ UploadImagesPlugin({ - imageClass: cx('opacity-40 rounded-lg border border-stone-200'), + imageClass: cx("opacity-40 rounded-lg border border-stone-200"), }), ]; }, }).configure({ allowBase64: true, HTMLAttributes: { - class: cx('rounded-lg border border-muted'), + class: cx("rounded-lg border border-muted"), }, }); const updatedImage = UpdatedImage.configure({ HTMLAttributes: { - class: cx('rounded-lg border border-muted'), + class: cx("rounded-lg border border-muted"), }, }); const taskList = TaskList.configure({ HTMLAttributes: { - class: cx('not-prose pl-2 '), + class: cx("not-prose pl-2 "), }, }); const taskItem = TaskItem.configure({ HTMLAttributes: { - class: cx('flex gap-2 items-start my-4'), + 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'), + 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'), + class: cx("list-disc list-outside leading-3 -mt-2"), }, }, orderedList: { HTMLAttributes: { - class: cx('list-decimal list-outside leading-3 -mt-2'), + class: cx("list-decimal list-outside leading-3 -mt-2"), }, }, listItem: { HTMLAttributes: { - class: cx('leading-normal -mb-2'), + class: cx("leading-normal -mb-2"), }, }, blockquote: { HTMLAttributes: { - class: cx('border-l-4 border-primary'), + 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'), + 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', + class: cx("rounded-md bg-muted px-1.5 py-1 font-mono font-medium"), + spellcheck: "false", }, }, horizontalRule: false, dropcursor: { - color: '#DBEAFE', + color: "#DBEAFE", width: 4, }, gapcursor: false, diff --git a/apps/web/components/tailwind/generative/ai-completion-command.tsx b/apps/web/components/tailwind/generative/ai-completion-command.tsx index 64954db1c..9f0f6d9b3 100644 --- a/apps/web/components/tailwind/generative/ai-completion-command.tsx +++ b/apps/web/components/tailwind/generative/ai-completion-command.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import { CommandGroup, CommandItem, CommandSeparator } from '../ui/command'; -import { useEditor } from 'novel'; -import { Check, TextQuote, TrashIcon } from 'lucide-react'; + +import { CommandGroup, CommandItem, CommandSeparator } from "../ui/command"; +import { useEditor } from "novel"; +import { Check, TextQuote, TrashIcon } from "lucide-react"; const AICompletionCommands = ({ completion, diff --git a/apps/web/components/tailwind/generative/ai-selector-commands.tsx b/apps/web/components/tailwind/generative/ai-selector-commands.tsx index a576a68c8..83bef23a1 100644 --- a/apps/web/components/tailwind/generative/ai-selector-commands.tsx +++ b/apps/web/components/tailwind/generative/ai-selector-commands.tsx @@ -1,29 +1,29 @@ -import React from 'react'; -import { CommandGroup, CommandItem, CommandSeparator } from '../ui/command'; -import { ArrowDownWideNarrow, CheckCheck, RefreshCcwDot, StepForward, WrapText } from 'lucide-react'; -import { useEditor } from 'novel'; -import { getPrevText } from 'novel/extensions'; + +import { CommandGroup, CommandItem, CommandSeparator } from "../ui/command"; +import { ArrowDownWideNarrow, CheckCheck, RefreshCcwDot, StepForward, WrapText } from "lucide-react"; +import { useEditor } from "novel"; +import { getPrevText } from "novel/extensions"; const options = [ { - value: 'improve', - label: 'Improve writing', + value: "improve", + label: "Improve writing", icon: RefreshCcwDot, }, { - value: 'fix', - label: 'Fix grammar', + value: "fix", + label: "Fix grammar", icon: CheckCheck, }, { - value: 'shorter', - label: 'Make shorter', + value: "shorter", + label: "Make shorter", icon: ArrowDownWideNarrow, }, { - value: 'longer', - label: 'Make longer', + value: "longer", + label: "Make longer", icon: WrapText, }, ]; @@ -59,7 +59,7 @@ const AISelectorCommands = ({ onSelect }: AISelectorCommandsProps) => { { const text = getPrevText(editor, { chars: 5000 }); - onSelect(text, 'continue'); + onSelect(text, "continue"); }} value="continue" className="gap-2 px-4" diff --git a/apps/web/components/tailwind/generative/ai-selector.tsx b/apps/web/components/tailwind/generative/ai-selector.tsx index 217d0e89f..88ca1681c 100644 --- a/apps/web/components/tailwind/generative/ai-selector.tsx +++ b/apps/web/components/tailwind/generative/ai-selector.tsx @@ -1,20 +1,20 @@ -'use client'; +"use client"; -import { Command, CommandInput } from '@/components/tailwind/ui/command'; +import { Command, CommandInput } from "@/components/tailwind/ui/command"; -import { useCompletion } from 'ai/react'; -import { toast } from 'sonner'; -import { useEditor } from 'novel'; -import { useEffect, useState } from 'react'; -import Markdown from 'react-markdown'; -import AISelectorCommands from './ai-selector-commands'; -import AICompletionCommands from './ai-completion-command'; -import { ScrollArea } from '../ui/scroll-area'; -import { Button } from '../ui/button'; -import { ArrowUp } from 'lucide-react'; -import Magic from '../ui/icons/magic'; -import CrazySpinner from '../ui/icons/crazy-spinner'; -import { addAIHighlight } from 'novel/extensions'; +import { useCompletion } from "ai/react"; +import { ArrowUp } from "lucide-react"; +import { useEditor } from "novel"; +import { addAIHighlight } from "novel/extensions"; +import { useState } from "react"; +import Markdown from "react-markdown"; +import { toast } from "sonner"; +import { Button } from "../ui/button"; +import CrazySpinner from "../ui/icons/crazy-spinner"; +import Magic from "../ui/icons/magic"; +import { ScrollArea } from "../ui/scroll-area"; +import AICompletionCommands from "./ai-completion-command"; +import AISelectorCommands from "./ai-selector-commands"; //TODO: I think it makes more sense to create a custom Tiptap extension for this functionality https://tiptap.dev/docs/editor/ai/introduction interface AISelectorProps { @@ -22,16 +22,16 @@ interface AISelectorProps { onOpenChange: (open: boolean) => void; } -export function AISelector({ open, onOpenChange }: AISelectorProps) { +export function AISelector({ onOpenChange }: AISelectorProps) { const { editor } = useEditor(); - const [inputValue, setInputValue] = useState(''); + const [inputValue, setInputValue] = useState(""); const { completion, complete, isLoading } = useCompletion({ // id: "novel", - api: '/api/generate', + api: "/api/generate", onResponse: (response) => { if (response.status === 429) { - toast.error('You have reached your request limit for the day.'); + toast.error("You have reached your request limit for the day."); return; } }, @@ -70,7 +70,7 @@ export function AISelector({ open, onOpenChange }: AISelectorProps) { value={inputValue} onValueChange={setInputValue} autoFocus - placeholder={hasCompletion ? 'Tell AI what to do next' : 'Ask AI to edit or generate...'} + placeholder={hasCompletion ? "Tell AI what to do next" : "Ask AI to edit or generate..."} onFocus={() => addAIHighlight(editor)} />
- {editor.isActive('highlight', { color }) && } + {editor.isActive("highlight", { color }) && } ))}
diff --git a/apps/web/components/tailwind/selectors/link-selector.tsx b/apps/web/components/tailwind/selectors/link-selector.tsx index a345f71b9..9b78dcbcc 100644 --- a/apps/web/components/tailwind/selectors/link-selector.tsx +++ b/apps/web/components/tailwind/selectors/link-selector.tsx @@ -1,26 +1,26 @@ -import { cn } from '@/lib/utils'; -import { useEditor } from 'novel'; -import { Check, Trash } from 'lucide-react'; -import { type Dispatch, type FC, type SetStateAction, useEffect, useRef } from 'react'; -import { Popover, PopoverTrigger } from '@radix-ui/react-popover'; -import { Button } from '@/components/tailwind/ui/button'; -import { PopoverContent } from '@/components/tailwind/ui/popover'; +import { Button } from "@/components/tailwind/ui/button"; +import { PopoverContent } from "@/components/tailwind/ui/popover"; +import { cn } from "@/lib/utils"; +import { Popover, PopoverTrigger } from "@radix-ui/react-popover"; +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) { + } catch (_e) { return false; } } export function getUrlFromString(str: string) { if (isValidUrl(str)) return str; try { - if (str.includes('.') && !str.includes(' ')) { + if (str.includes(".") && !str.includes(" ")) { return new URL(`https://${str}`).toString(); } - } catch (e) { + } catch (_e) { return null; } } @@ -35,7 +35,7 @@ export const LinkSelector = ({ open, onOpenChange }: LinkSelectorProps) => { // Autofocus on input by default useEffect(() => { - inputRef.current && inputRef.current?.focus(); + inputRef.current?.focus(); }); if (!editor) return null; @@ -45,8 +45,8 @@ export const LinkSelector = ({ open, onOpenChange }: LinkSelectorProps) => { - {items.map((item, index) => ( + {items.map((item) => ( { item.command(editor); onOpenChange(false); diff --git a/apps/web/components/tailwind/selectors/text-buttons.tsx b/apps/web/components/tailwind/selectors/text-buttons.tsx index 733d101fb..139737025 100644 --- a/apps/web/components/tailwind/selectors/text-buttons.tsx +++ b/apps/web/components/tailwind/selectors/text-buttons.tsx @@ -1,57 +1,57 @@ -import { cn } from '@/lib/utils'; -import { EditorBubbleItem, useEditor } from 'novel'; -import { BoldIcon, ItalicIcon, UnderlineIcon, StrikethroughIcon, CodeIcon } from 'lucide-react'; -import type { SelectorItem } from './node-selector'; -import { Button } from '@/components/tailwind/ui/button'; +import { Button } from "@/components/tailwind/ui/button"; +import { cn } from "@/lib/utils"; +import { BoldIcon, CodeIcon, ItalicIcon, StrikethroughIcon, UnderlineIcon } from "lucide-react"; +import { EditorBubbleItem, useEditor } from "novel"; +import type { SelectorItem } from "./node-selector"; export const TextButtons = () => { const { editor } = useEditor(); if (!editor) return null; const items: SelectorItem[] = [ { - name: 'bold', - isActive: (editor) => editor.isActive('bold'), + name: "bold", + isActive: (editor) => editor.isActive("bold"), command: (editor) => editor.chain().focus().toggleBold().run(), icon: BoldIcon, }, { - name: 'italic', - isActive: (editor) => editor.isActive('italic'), + name: "italic", + isActive: (editor) => editor.isActive("italic"), command: (editor) => editor.chain().focus().toggleItalic().run(), icon: ItalicIcon, }, { - name: 'underline', - isActive: (editor) => editor.isActive('underline'), + name: "underline", + isActive: (editor) => editor.isActive("underline"), command: (editor) => editor.chain().focus().toggleUnderline().run(), icon: UnderlineIcon, }, { - name: 'strike', - isActive: (editor) => editor.isActive('strike'), + name: "strike", + isActive: (editor) => editor.isActive("strike"), command: (editor) => editor.chain().focus().toggleStrike().run(), icon: StrikethroughIcon, }, { - name: 'code', - isActive: (editor) => editor.isActive('code'), + name: "code", + isActive: (editor) => editor.isActive("code"), command: (editor) => editor.chain().focus().toggleCode().run(), icon: CodeIcon, }, ]; return (
- {items.map((item, index) => ( + {items.map((item) => ( { item.command(editor); }} > diff --git a/apps/web/components/tailwind/slash-command.tsx b/apps/web/components/tailwind/slash-command.tsx index cc1a82f8b..ab5aec031 100644 --- a/apps/web/components/tailwind/slash-command.tsx +++ b/apps/web/components/tailwind/slash-command.tsx @@ -10,110 +10,110 @@ import { MessageSquarePlus, Text, TextQuote, -} from 'lucide-react'; -import { createSuggestionItems } from 'novel/extensions'; -import { Command, renderItems } from 'novel/extensions'; -import { uploadFn } from './image-upload'; +} 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.', + 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'); + window.open("/feedback", "_blank"); }, }, { - title: 'Text', - description: 'Just start typing with plain text.', - searchTerms: ['p', 'paragraph'], + 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(); + 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'], + 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'], + 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(); + editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run(); }, }, { - title: 'Heading 2', - description: 'Medium section heading.', - searchTerms: ['subtitle', 'medium'], + title: "Heading 2", + description: "Medium section heading.", + searchTerms: ["subtitle", "medium"], icon: , command: ({ editor, range }) => { - editor.chain().focus().deleteRange(range).setNode('heading', { level: 2 }).run(); + editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run(); }, }, { - title: 'Heading 3', - description: 'Small section heading.', - searchTerms: ['subtitle', 'small'], + title: "Heading 3", + description: "Small section heading.", + searchTerms: ["subtitle", "small"], icon: , command: ({ editor, range }) => { - editor.chain().focus().deleteRange(range).setNode('heading', { level: 3 }).run(); + editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run(); }, }, { - title: 'Bullet List', - description: 'Create a simple bullet list.', - searchTerms: ['unordered', 'point'], + 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'], + 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'], + title: "Quote", + description: "Capture a quote.", + searchTerms: ["blockquote"], icon: , command: ({ editor, range }) => - editor.chain().focus().deleteRange(range).toggleNode('paragraph', 'paragraph').toggleBlockquote().run(), + editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run(), }, { - title: 'Code', - description: 'Capture a code snippet.', - searchTerms: ['codeblock'], + 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'], + 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/*'; + const input = document.createElement("input"); + input.type = "file"; + input.accept = "image/*"; input.onchange = async () => { if (input.files?.length) { const file = input.files[0]; diff --git a/apps/web/components/tailwind/ui/button.tsx b/apps/web/components/tailwind/ui/button.tsx index 7eaaf1630..1fbccede8 100644 --- a/apps/web/components/tailwind/ui/button.tsx +++ b/apps/web/components/tailwind/ui/button.tsx @@ -1,31 +1,31 @@ -import * as React from 'react'; -import { Slot } from '@radix-ui/react-slot'; -import { cva, type VariantProps } from 'class-variance-authority'; +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from '@/lib/utils'; +import { cn } from "@/lib/utils"; const buttonVariants = cva( - 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { - default: 'bg-primary text-primary-foreground hover:bg-primary/90', - destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', - outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', - secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', - ghost: 'hover:bg-accent hover:text-accent-foreground', - link: 'text-primary underline-offset-4 hover:underline', + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", }, size: { - default: 'h-10 px-4 py-2', - sm: 'h-9 rounded-md px-3', - lg: 'h-11 rounded-md px-8', - icon: 'h-10 w-10', + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", }, }, defaultVariants: { - variant: 'default', - size: 'default', + variant: "default", + size: "default", }, }, ); @@ -38,10 +38,10 @@ export interface ButtonProps const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : 'button'; + const Comp = asChild ? Slot : "button"; return ; }, ); -Button.displayName = 'Button'; +Button.displayName = "Button"; export { Button, buttonVariants }; diff --git a/apps/web/components/tailwind/ui/command.tsx b/apps/web/components/tailwind/ui/command.tsx index ba4dbc9d3..e0f1037d7 100644 --- a/apps/web/components/tailwind/ui/command.tsx +++ b/apps/web/components/tailwind/ui/command.tsx @@ -1,12 +1,12 @@ -'use client'; +"use client"; -import * as React from 'react'; -import type { DialogProps } from '@radix-ui/react-dialog'; -import { Command as CommandPrimitive } from 'cmdk'; +import * as React from "react"; +import type { DialogProps } from "@radix-ui/react-dialog"; +import { Command as CommandPrimitive } from "cmdk"; -import { cn } from '@/lib/utils'; -import { Dialog, DialogContent } from '@/components/tailwind/ui/dialog'; -import Magic from '@/components/tailwind/ui/icons/magic'; +import { cn } from "@/lib/utils"; +import { Dialog, DialogContent } from "@/components/tailwind/ui/dialog"; +import Magic from "@/components/tailwind/ui/icons/magic"; const Command = React.forwardRef< React.ElementRef, @@ -15,7 +15,7 @@ const Command = React.forwardRef< (({ className, ...props }, ref) => ( )); @@ -83,7 +83,7 @@ const CommandGroup = React.forwardRef< , React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); CommandSeparator.displayName = CommandPrimitive.Separator.displayName; @@ -107,7 +107,7 @@ const CommandItem = React.forwardRef< ) => { - return ; + return ; }; -CommandShortcut.displayName = 'CommandShortcut'; +CommandShortcut.displayName = "CommandShortcut"; export { Command, diff --git a/apps/web/components/tailwind/ui/dialog.tsx b/apps/web/components/tailwind/ui/dialog.tsx index 2559a21b3..9921ebf9e 100644 --- a/apps/web/components/tailwind/ui/dialog.tsx +++ b/apps/web/components/tailwind/ui/dialog.tsx @@ -1,10 +1,10 @@ -'use client'; +"use client"; -import * as React from 'react'; -import * as DialogPrimitive from '@radix-ui/react-dialog'; -import { X } from 'lucide-react'; +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; -import { cn } from '@/lib/utils'; +import { cn } from "@/lib/utils"; const Dialog = DialogPrimitive.Root; @@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef< ) => ( -
+
); -DialogHeader.displayName = 'DialogHeader'; +DialogHeader.displayName = "DialogHeader"; const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( -
+
); -DialogFooter.displayName = 'DialogFooter'; +DialogFooter.displayName = "DialogFooter"; const DialogTitle = React.forwardRef< React.ElementRef, @@ -69,7 +69,7 @@ const DialogTitle = React.forwardRef< >(({ className, ...props }, ref) => ( )); @@ -79,7 +79,7 @@ const DialogDescription = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); DialogDescription.displayName = DialogPrimitive.Description.displayName; diff --git a/apps/web/components/tailwind/ui/icons/crazy-spinner.tsx b/apps/web/components/tailwind/ui/icons/crazy-spinner.tsx index 4f1ded683..e1a6ef8ff 100644 --- a/apps/web/components/tailwind/ui/icons/crazy-spinner.tsx +++ b/apps/web/components/tailwind/ui/icons/crazy-spinner.tsx @@ -1,11 +1,9 @@ -import React from 'react'; - const CrazySpinner = () => { return (
-
-
-
+
+
+
); }; diff --git a/apps/web/components/tailwind/ui/icons/font-default.tsx b/apps/web/components/tailwind/ui/icons/font-default.tsx index 46a35c9ab..bf29e4b51 100644 --- a/apps/web/components/tailwind/ui/icons/font-default.tsx +++ b/apps/web/components/tailwind/ui/icons/font-default.tsx @@ -1,6 +1,7 @@ export default function FontDefault({ className }: { className?: string }) { return ( + Font Default Icon + Font Mono Icon + Font Serif Icon + - - - ); -} diff --git a/apps/web/components/tailwind/ui/icons/index.tsx b/apps/web/components/tailwind/ui/icons/index.tsx index 5d177ea90..213134ad7 100644 --- a/apps/web/components/tailwind/ui/icons/index.tsx +++ b/apps/web/components/tailwind/ui/icons/index.tsx @@ -1,4 +1,4 @@ -export { default as FontDefault } from './font-default'; -export { default as FontSerif } from './font-serif'; -export { default as FontMono } from './font-mono'; -export { default as Github } from './github'; +export { default as FontDefault } from "./font-default"; +export { default as FontSerif } from "./font-serif"; +export { default as FontMono } from "./font-mono"; +export { default as Github } from "./github"; diff --git a/apps/web/components/tailwind/ui/icons/loading-circle.tsx b/apps/web/components/tailwind/ui/icons/loading-circle.tsx index 0d0198da1..2a6381174 100644 --- a/apps/web/components/tailwind/ui/icons/loading-circle.tsx +++ b/apps/web/components/tailwind/ui/icons/loading-circle.tsx @@ -2,7 +2,7 @@ export default function LoadingCircle({ dimensions }: { dimensions?: string }) { return (