Skip to content

Commit

Permalink
Implement rich text styling
Browse files Browse the repository at this point in the history
  • Loading branch information
evadecker committed Dec 4, 2024
1 parent 02c9c28 commit 42599c6
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 88 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ node_modules

# Private keys
.env
.env.local
.npmrc

# Local files
*.local
10 changes: 10 additions & 0 deletions convex/quests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/components/app/AppContent/AppContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ type AppContentProps = {
};

export const AppContent = ({ children }: AppContentProps) => {
return <main className="flex-1 w-full app-padding">{children}</main>;
return <main className="flex-1 w-full app-padding pb-8">{children}</main>;
};
144 changes: 68 additions & 76 deletions src/components/common/RichText/RichText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,82 +105,74 @@ export function RichText({
editor={editor}
className={twMerge("w-full prose max-w-none", className)}
/>
{editable && (
<>
<BubbleMenu
editor={editor}
className="bg-gray-1 dark:bg-graydark-2 border border-gray-dim p-1.5 rounded-xl shadow-md flex gap-1 items-center"
>
<ToggleButton
onPress={() => 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"
/>
<ToggleButton
onPress={() => 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"
/>
<Separator orientation="vertical" />
<ToggleButton
onPress={() =>
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"
/>
<ToggleButton
onPress={() =>
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"
/>
<Separator orientation="vertical" />
{/* Lists */}
<ToggleButton
onPress={() => 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"
/>
<ToggleButton
onPress={() => 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"
/>
</BubbleMenu>
{showReadingScore && (
<div className="border-t border-gray-dim">
<ReadingScore text={editor.state.doc.textContent} />
</div>
)}
</>
<BubbleMenu
editor={editor}
className="bg-gray-1 dark:bg-graydark-2 border border-gray-dim p-1.5 gap-px rounded-xl shadow-md flex items-center data-[state=visible]:opacity-100 data-[state=hidden]:opacity-0 transition-opacity *:border-none"
>
<ToggleButton
onPress={() => 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"
/>
<ToggleButton
onPress={() => 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"
/>
<Separator orientation="vertical" />
<ToggleButton
onPress={() =>
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"
/>
<ToggleButton
onPress={() =>
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"
/>
<Separator orientation="vertical" />
{/* Lists */}
<ToggleButton
onPress={() => 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"
/>
<ToggleButton
onPress={() => 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"
/>
</BubbleMenu>
{showReadingScore && (
<div className="border-t border-gray-dim">
<ReadingScore text={editor.state.doc.textContent} />
</div>
)}
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/Separator/Separator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
42 changes: 37 additions & 5 deletions src/routes/_authenticated/_home/quests.$questId.edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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 <Empty title="Quest not found" icon={Milestone} />;
Expand All @@ -50,8 +76,14 @@ function QuestEditRoute() {
<Link
href={{ to: "/quests/$questId", params: { questId: quest._id } }}
button={{ variant: "ghost" }}
onPress={handleSave}
isDisabled={isSaving}
>
<Check size={16} />
{isSaving ? (
<LoaderCircle className="animate-spin" />
) : (
<Check size={16} />
)}
Save
</Link>
</PageHeader>
Expand All @@ -61,7 +93,7 @@ function QuestEditRoute() {
</div>
<QuestUrls urls={quest.urls} />
<QuestForms questId={quest._id} />
<RichText initialContent={quest.content} />
<RichText initialContent={quest.content} onChange={setContent} />
</AppContent>
);
}
49 changes: 44 additions & 5 deletions src/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,27 +35,28 @@
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;
}

li {
&::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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
}
}

0 comments on commit 42599c6

Please sign in to comment.