diff --git a/.gitignore b/.gitignore
index 08a1c46a..08a4462f 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 76cc00d1..d7dd9de9 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 edab3050..abf81d87 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 e1ff4f3c..696be0aa 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 bfb0917e..c7afbe7f 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 c610f56f..e04674a0 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 1e62ade5..939e320f 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 237ebccf..e1c89076 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 0548af06..664b53ee 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);
+ }
+}