* wip * wip * wip * format * wip(mobile): lint and format * cleanup * nits * nits * idiomatic react * nit * format all root files
79 lines
2.2 KiB
TypeScript
79 lines
2.2 KiB
TypeScript
import { useCallback, useState } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { toast } from 'sonner';
|
|
import type { Particle } from '@/api/types';
|
|
import {
|
|
particlePath,
|
|
parseParticlePath,
|
|
toFirestoreDocPath,
|
|
type ParticlePath,
|
|
} from '@/lib/particle-path';
|
|
import { editTextParticleContent } from '@/lib/firestore-particles';
|
|
import { TextEditor } from '@/features/compose/text-editor';
|
|
import { useSuspendPlayback } from '@/hooks/use-suspend-playback';
|
|
|
|
type TextParticle = Extract<Particle, { type: 'text' }>;
|
|
|
|
interface TextEditOverlayProps {
|
|
particle: TextParticle;
|
|
streamPath: ParticlePath;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function TextEditOverlay({
|
|
particle,
|
|
streamPath,
|
|
onClose,
|
|
}: TextEditOverlayProps) {
|
|
useSuspendPlayback(true, 'text-edit');
|
|
|
|
const [textContent, setTextContent] = useState(particle.properties.content);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
const handleSubmit = useCallback(async () => {
|
|
if (saving) return;
|
|
const trimmed = textContent.trim();
|
|
if (!trimmed) return;
|
|
if (trimmed === particle.properties.content) {
|
|
onClose();
|
|
return;
|
|
}
|
|
|
|
setSaving(true);
|
|
try {
|
|
const { networkId, segments } = parseParticlePath(streamPath);
|
|
const docPath = toFirestoreDocPath(
|
|
particlePath(networkId, [...segments, particle.id]),
|
|
);
|
|
await editTextParticleContent(docPath, trimmed);
|
|
onClose();
|
|
} catch (err) {
|
|
toast.error(err instanceof Error ? err.message : 'Failed to save');
|
|
setSaving(false);
|
|
}
|
|
}, [
|
|
saving,
|
|
textContent,
|
|
particle.properties.content,
|
|
particle.id,
|
|
streamPath,
|
|
onClose,
|
|
]);
|
|
|
|
return createPortal(
|
|
// React synthetic events bubble through the React tree (not the DOM tree),
|
|
// so clicks here would reach stream-view's click-to-navigate handler even
|
|
// though we're portaled to document.body. Stop propagation at the root.
|
|
<div className="fixed inset-0 z-[100]" onClick={(e) => e.stopPropagation()}>
|
|
<TextEditor
|
|
textContent={textContent}
|
|
onTextChange={setTextContent}
|
|
onSubmit={handleSubmit}
|
|
onCancel={onClose}
|
|
submitHint="save"
|
|
/>
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|