From 42599c6afad823fc6d61693dc2fa643a38c67131 Mon Sep 17 00:00:00 2001 From: Eva Decker Date: Tue, 3 Dec 2024 21:11:15 -0500 Subject: [PATCH] Implement rich text styling --- .gitignore | 2 + convex/quests.ts | 10 ++ package.json | 1 + pnpm-lock.yaml | 3 + src/components/app/AppContent/AppContent.tsx | 2 +- src/components/common/RichText/RichText.tsx | 144 +++++++++--------- src/components/common/Separator/Separator.tsx | 2 +- .../_home/quests.$questId.edit.tsx | 42 ++++- src/styles/index.css | 49 +++++- 9 files changed, 167 insertions(+), 88 deletions(-) diff --git a/.gitignore b/.gitignore index 08a1c46..08a4462 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,8 @@ node_modules # Private keys .env +.env.local +.npmrc # Local files *.local diff --git a/convex/quests.ts b/convex/quests.ts index 76cc00d..d7dd9de 100644 --- a/convex/quests.ts +++ b/convex/quests.ts @@ -134,6 +134,16 @@ export const setTimeRequired = userMutation({ }, }); +export const setContent = userMutation({ + args: { + questId: v.id("quests"), + content: v.string(), + }, + handler: async (ctx, args) => { + await ctx.db.patch(args.questId, { content: args.content }); + }, +}); + export const softDelete = userMutation({ args: { questId: v.id("quests") }, handler: async (ctx, args) => { diff --git a/package.json b/package.json index edab305..abf81d8 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@tanstack/react-router": "^1.85.0", "@tiptap/extension-blockquote": "^2.10.3", "@tiptap/extension-bold": "^2.10.3", + "@tiptap/extension-bubble-menu": "^2.10.3", "@tiptap/extension-bullet-list": "^2.10.3", "@tiptap/extension-document": "^2.10.3", "@tiptap/extension-hard-break": "^2.10.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1ff4f3..696be0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,6 +42,9 @@ importers: '@tiptap/extension-bold': specifier: ^2.10.3 version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + '@tiptap/extension-bubble-menu': + specifier: ^2.10.3 + version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3) '@tiptap/extension-bullet-list': specifier: ^2.10.3 version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) diff --git a/src/components/app/AppContent/AppContent.tsx b/src/components/app/AppContent/AppContent.tsx index bfb0917..c7afbe7 100644 --- a/src/components/app/AppContent/AppContent.tsx +++ b/src/components/app/AppContent/AppContent.tsx @@ -3,5 +3,5 @@ type AppContentProps = { }; export const AppContent = ({ children }: AppContentProps) => { - return
{children}
; + return
{children}
; }; diff --git a/src/components/common/RichText/RichText.tsx b/src/components/common/RichText/RichText.tsx index c610f56..e04674a 100644 --- a/src/components/common/RichText/RichText.tsx +++ b/src/components/common/RichText/RichText.tsx @@ -105,82 +105,74 @@ export function RichText({ editor={editor} className={twMerge("w-full prose max-w-none", className)} /> - {editable && ( - <> - - editor.chain().focus().toggleBold().run()} - isDisabled={!editor.can().chain().focus().toggleBold().run()} - isSelected={editor.isActive("bold")} - icon={BoldIcon} - aria-label="Toggle bold text" - size="small" - /> - editor.chain().focus().toggleItalic().run()} - isDisabled={!editor.can().chain().focus().toggleItalic().run()} - isSelected={editor.isActive("italic")} - icon={ItalicIcon} - aria-label="Toggle italic text" - size="small" - /> - - - editor.chain().focus().toggleHeading({ level: 2 }).run() - } - isDisabled={ - !editor.can().chain().focus().toggleHeading({ level: 2 }).run() - } - isSelected={editor.isActive("heading", { level: 2 })} - icon={Heading2} - aria-label="Toggle second-level heading text" - size="small" - /> - - editor.chain().focus().toggleHeading({ level: 3 }).run() - } - isDisabled={ - !editor.can().chain().focus().toggleHeading({ level: 3 }).run() - } - isSelected={editor.isActive("heading", { level: 3 })} - icon={Heading3} - aria-label="Toggle third-level heading text" - size="small" - /> - - {/* Lists */} - editor.chain().focus().toggleBulletList().run()} - isDisabled={ - !editor.can().chain().focus().toggleBulletList().run() - } - isSelected={editor.isActive("bulletList")} - icon={List} - aria-label="Toggle bulleted list" - size="small" - /> - editor.chain().focus().toggleOrderedList().run()} - isDisabled={ - !editor.can().chain().focus().toggleOrderedList().run() - } - isSelected={editor.isActive("orderedList")} - icon={ListOrdered} - aria-label="Toggle numbered list" - size="small" - /> - - {showReadingScore && ( -
- -
- )} - + + editor.chain().focus().toggleBold().run()} + isDisabled={!editor.can().chain().focus().toggleBold().run()} + isSelected={editor.isActive("bold")} + icon={BoldIcon} + aria-label="Toggle bold text" + size="small" + /> + editor.chain().focus().toggleItalic().run()} + isDisabled={!editor.can().chain().focus().toggleItalic().run()} + isSelected={editor.isActive("italic")} + icon={ItalicIcon} + aria-label="Toggle italic text" + size="small" + /> + + + editor.chain().focus().toggleHeading({ level: 2 }).run() + } + isDisabled={ + !editor.can().chain().focus().toggleHeading({ level: 2 }).run() + } + isSelected={editor.isActive("heading", { level: 2 })} + icon={Heading2} + aria-label="Toggle second-level heading text" + size="small" + /> + + editor.chain().focus().toggleHeading({ level: 3 }).run() + } + isDisabled={ + !editor.can().chain().focus().toggleHeading({ level: 3 }).run() + } + isSelected={editor.isActive("heading", { level: 3 })} + icon={Heading3} + aria-label="Toggle third-level heading text" + size="small" + /> + + {/* Lists */} + editor.chain().focus().toggleBulletList().run()} + isDisabled={!editor.can().chain().focus().toggleBulletList().run()} + isSelected={editor.isActive("bulletList")} + icon={List} + aria-label="Toggle bulleted list" + size="small" + /> + editor.chain().focus().toggleOrderedList().run()} + isDisabled={!editor.can().chain().focus().toggleOrderedList().run()} + isSelected={editor.isActive("orderedList")} + icon={ListOrdered} + aria-label="Toggle numbered list" + size="small" + /> + + {showReadingScore && ( +
+ +
)} ); diff --git a/src/components/common/Separator/Separator.tsx b/src/components/common/Separator/Separator.tsx index 1e62ade..939e320 100644 --- a/src/components/common/Separator/Separator.tsx +++ b/src/components/common/Separator/Separator.tsx @@ -9,7 +9,7 @@ const styles = tv({ variants: { orientation: { horizontal: "h-px w-full", - vertical: "w-px", + vertical: "w-px h-6 mx-1.5 bg-gray-4 dark:bg-graydark-4 shrink-0", }, }, defaultVariants: { diff --git a/src/routes/_authenticated/_home/quests.$questId.edit.tsx b/src/routes/_authenticated/_home/quests.$questId.edit.tsx index 237ebcc..e1c8907 100644 --- a/src/routes/_authenticated/_home/quests.$questId.edit.tsx +++ b/src/routes/_authenticated/_home/quests.$questId.edit.tsx @@ -8,9 +8,11 @@ import { } from "@/components/quests"; import { api } from "@convex/_generated/api"; import type { Id } from "@convex/_generated/dataModel"; -import { createFileRoute, redirect } from "@tanstack/react-router"; -import { useQuery } from "convex/react"; -import { Check, Milestone } from "lucide-react"; +import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router"; +import { useMutation, useQuery } from "convex/react"; +import { Check, LoaderCircle, Milestone } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; export const Route = createFileRoute( "/_authenticated/_home/quests/$questId/edit", @@ -37,6 +39,30 @@ function QuestEditRoute() { questId: questId as Id<"quests">, }); + const [content, setContent] = useState(quest?.content ?? ""); + const [isSaving, setIsSaving] = useState(false); + const navigate = useNavigate(); + + const updateContent = useMutation(api.quests.setContent); + + const handleSave = async () => { + try { + setIsSaving(true); + await updateContent({ + questId: questId as Id<"quests">, + content, + }); + navigate({ + to: "/quests/$questId", + params: { questId: questId as Id<"quests"> }, + }); + } catch (err) { + toast.error("Failed to save changes"); + } finally { + setIsSaving(false); + } + }; + // TODO: Improve loading state to prevent flash of empty if (quest === undefined) return; if (quest === null) return ; @@ -50,8 +76,14 @@ function QuestEditRoute() { - + {isSaving ? ( + + ) : ( + + )} Save @@ -61,7 +93,7 @@ function QuestEditRoute() { - + ); } diff --git a/src/styles/index.css b/src/styles/index.css index 0548af0..664b53e 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -35,14 +35,15 @@ blockquote { @apply border-l-2 border-gray-dim pl-4; } - + li { margin-block: 0; padding-inline-start: 1.5em; position: relative; } - - ul, ol { + + ul, + ol { list-style: none; } @@ -50,12 +51,12 @@ &::before { position: absolute; left: 0; - @apply text-gray-dim; + @apply tabular-nums text-gray-dim; } } ul > li::before { - content: '•'; + content: "•"; margin-inline-start: 0.25em; scale: 1.3; translate: 0 -0.05em; @@ -68,6 +69,9 @@ ol > li::before { counter-increment: list; content: counter(list) "."; + text-align: right; + width: 1.5em; + margin-inline-start: -0.5em; } ol ol > li::before { @@ -107,3 +111,38 @@ body { @apply bg-gray-app; color-scheme: dark light; } + +.tippy-box { + transition: 0.25s ease; + transition-property: opacity, transform, scale; + + &[data-state="hidden"] { + opacity: 0; + scale: 0.96; + + &[data-placement^="top"] { + transform: translateY(10px); + transform-origin: bottom; + } + + &[data-placement^="bottom"] { + transform: translateY(-10px); + transform-origin: top; + } + + &[data-placement^="left"] { + transform: translateX(10px); + transform-origin: right; + } + + &[data-placement^="right"] { + transform: translateX(-10px); + transform-origin: left; + } + } + + &[data-state="visible"] { + opacity: 1; + transition-timing-function: cubic-bezier(0.54, 1.5, 0.38, 1.11); + } +}