aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components/dashboard/EditableText.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/components/dashboard/EditableText.tsx')
-rw-r--r--apps/web/components/dashboard/EditableText.tsx146
1 files changed, 146 insertions, 0 deletions
diff --git a/apps/web/components/dashboard/EditableText.tsx b/apps/web/components/dashboard/EditableText.tsx
new file mode 100644
index 00000000..7539bd8f
--- /dev/null
+++ b/apps/web/components/dashboard/EditableText.tsx
@@ -0,0 +1,146 @@
+import { useEffect, useRef, useState } from "react";
+import { ActionButtonWithTooltip } from "@/components/ui/action-button";
+import { ButtonWithTooltip } from "@/components/ui/button";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipPortal,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { Check, Pencil, X } from "lucide-react";
+
+interface Props {
+ viewClassName?: string;
+ untitledClassName?: string;
+ editClassName?: string;
+ onSave: (title: string | null) => void;
+ isSaving: boolean;
+ originalText: string | null;
+ setEditable: (editable: boolean) => void;
+}
+
+function EditMode({
+ onSave: onSaveCB,
+ editClassName: className,
+ isSaving,
+ originalText,
+ setEditable,
+}: Props) {
+ const ref = useRef<HTMLDivElement>(null);
+
+ useEffect(() => {
+ if (ref.current) {
+ ref.current.focus();
+ ref.current.textContent = originalText;
+ }
+ }, [ref]);
+
+ const onSave = () => {
+ let toSave: string | null = ref.current?.textContent ?? null;
+ if (originalText == toSave) {
+ // Nothing to do here
+ return;
+ }
+ if (toSave == "") {
+ toSave = null;
+ }
+ onSaveCB(toSave);
+ setEditable(false);
+ };
+
+ return (
+ <div className="flex gap-3">
+ <div
+ ref={ref}
+ role="presentation"
+ className={className}
+ contentEditable={true}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ }
+ }}
+ />
+ <ActionButtonWithTooltip
+ tooltip="Save"
+ delayDuration={500}
+ size="none"
+ variant="ghost"
+ className="align-middle text-gray-400"
+ loading={isSaving}
+ onClick={() => onSave()}
+ >
+ <Check className="size-4" />
+ </ActionButtonWithTooltip>
+ <ButtonWithTooltip
+ tooltip="Cancel"
+ delayDuration={500}
+ size="none"
+ variant="ghost"
+ className="align-middle text-gray-400"
+ onClick={() => {
+ setEditable(false);
+ }}
+ >
+ <X className="size-4" />
+ </ButtonWithTooltip>
+ </div>
+ );
+}
+
+function ViewMode({
+ originalText,
+ setEditable,
+ viewClassName,
+ untitledClassName,
+}: Props) {
+ return (
+ <Tooltip delayDuration={500}>
+ <div className="flex items-center gap-3 text-center">
+ <TooltipTrigger asChild>
+ {originalText ? (
+ <p className={viewClassName}>{originalText}</p>
+ ) : (
+ <p className={untitledClassName}>Untitled</p>
+ )}
+ </TooltipTrigger>
+ <ButtonWithTooltip
+ delayDuration={500}
+ tooltip="Edit title"
+ size="none"
+ variant="ghost"
+ className="align-middle text-gray-400"
+ onClick={() => {
+ setEditable(true);
+ }}
+ >
+ <Pencil className="size-4" />
+ </ButtonWithTooltip>
+ </div>
+ <TooltipPortal>
+ {originalText && (
+ <TooltipContent side="bottom" className="max-w-[40ch]">
+ {originalText}
+ </TooltipContent>
+ )}
+ </TooltipPortal>
+ </Tooltip>
+ );
+}
+
+export function EditableText(props: {
+ viewClassName?: string;
+ untitledClassName?: string;
+ editClassName?: string;
+ originalText: string | null;
+ onSave: (title: string | null) => void;
+ isSaving: boolean;
+}) {
+ const [editable, setEditable] = useState(false);
+
+ return editable ? (
+ <EditMode setEditable={setEditable} {...props} />
+ ) : (
+ <ViewMode setEditable={setEditable} {...props} />
+ );
+}