espial-0.0.36: frontend/src/components/NNote.tsx
import React from 'react';
import { useTranslation } from 'react-i18next';
import { destroyNote, editNote } from '../api';
import { app, closeWindow, fromNow, setFocus, shdatetime } from '../globals';
import type { Note } from '../types';
import { curQuerystring, fromNullableStr, lookupQueryStringValue } from '../util';
import { Markdown } from './Markdown';
function toTextarea(input: string) {
const lines = input.split('\n');
return (
<>
{lines[0]}
{lines.slice(1).map((x, idx) => (
<React.Fragment key={idx}>
<br />
{x}
</React.Fragment>
))}
</>
);
}
/** Displays a single note with inline editing and deletion, used in the popup window. */
export function NNote({ initial }: { initial: Note }) {
const { t } = useTranslation();
const a = app();
const [note, setNote] = React.useState<Note>(initial);
const [editNoteState, setEditNoteState] = React.useState<Note>(initial);
const [deleteAsk, setDeleteAsk] = React.useState(false);
const [edit, setEdit] = React.useState(initial.id <= 0);
const [destroyed, setDestroyed] = React.useState(false);
const [apiError, setApiError] = React.useState<string | null>(null);
const fromNowVal = fromNow(a.lang, note.created);
const notetextid = `${note.id.toString()}_text`;
function startEdit(next: boolean) {
setEditNoteState(note);
setEdit(next);
const qs = curQuerystring();
const q = lookupQueryStringValue(qs, 'next');
if (!next && q === 'closeWindow') closeWindow(window);
else if (next) setFocus(notetextid);
}
async function onDestroy() {
await destroyNote(note.id);
setDestroyed(true);
}
const onSubmit = async (e: React.SyntheticEvent<HTMLFormElement, SubmitEvent>): Promise<void> => {
e.preventDefault();
setApiError(null);
const res = await editNote(editNoteState);
if (res.ok && res.status >= 200 && res.status < 300) {
const qs = curQuerystring();
const next = lookupQueryStringValue(qs, 'next');
const ref = document.referrer;
const org = window.location.origin;
if (next === 'closeWindow') closeWindow(window);
else if (next === 'back') {
if (ref.startsWith(org)) window.location.href = ref;
else window.location.href = org;
} else if (editNoteState.id === 0) {
window.location.href = fromNullableStr(a.noteR);
} else {
setNote(editNoteState);
setEdit(false);
}
} else {
setApiError(res.bodyText);
// eslint-disable-next-line no-console
console.log(res.bodyText);
}
};
if (destroyed) return <p className="thm-text-error">{t('note.killed')}</p>;
return (
<div id={String(note.id)} className="note w-100 mw7 pa1 mb2">
{!edit ? (
<>
<div className="display">
<div className="link f5 lh-title">{note.title === '' ? t('noTitle') : note.title}</div>
<br />
{note.isMarkdown ? (
<div className="description mt1">
<Markdown text={note.text} />
</div>
) : (
<div className="description mt1 thm-text-secondary">{toTextarea(note.text)}</div>
)}
<div className="link f7 mt2 dib thm-text-tertiary w4">
<span data-created={note.created} title={shdatetime(a.lang, note.created)}>
{fromNowVal}
</span>
{' - '}
<span className="thm-text-tertiary">
{note.shared ? t('filter.public') : t('filter.private')}
</span>
</div>
</div>
{a.dat.isowner ? (
<div className="edit_links db mt3">
<button
type="button"
onClick={() => {
startEdit(true);
}}
className="edit thm-text-muted thm-hover-link-color"
>
{t('edit')}
</button>
<div className="delete_link di">
<button
type="button"
onClick={() => {
setDeleteAsk(true);
}}
className={`delete thm-text-muted thm-hover-link-color${deleteAsk ? ' dn' : ''}`}
>
{t('delete')}
</button>
<span className={`confirm thm-text-error${!deleteAsk ? ' dn' : ''}`}>
<button
type="button"
onClick={() => {
setDeleteAsk(false);
}}
>
{t('cancel')} /
</button>
<button type="button" onClick={() => void onDestroy()} className="thm-text-error">
{t('destroy')}
</button>
</span>
</div>
</div>
) : null}
</>
) : (
<form
onSubmit={(e) => {
void onSubmit(e);
}}
>
{apiError ? <div className="alert alert-err">{apiError}</div> : null}
<p className="mt2 mb1">{t('note.titleLabel')}</p>
<input
type="text"
className="title w-100 mb1 pt1 edit_form_input"
name="title"
value={editNoteState.title}
onChange={(e) => {
setEditNoteState((x) => ({ ...x, title: e.target.value }));
}}
autoFocus={editNoteState.title === ''}
/>
<br />
<p className="mt2 mb1">{t('note.descriptionLabel')}</p>
<textarea
id={notetextid}
className="description w-100 mb1 pt1 edit_form_input"
name="text"
rows={25}
value={editNoteState.text}
onChange={(e) => {
setEditNoteState((x) => ({ ...x, text: e.target.value }));
}}
/>
<div className="edit_form_checkboxes mb3">
<input
type="checkbox"
className="is-markdown pointer"
id="edit_ismarkdown"
name="ismarkdown"
checked={editNoteState.isMarkdown}
onChange={(e) => {
setEditNoteState((x) => ({ ...x, isMarkdown: e.target.checked }));
}}
/>{' '}
<label htmlFor="edit_ismarkdown" className="mr2">
{t('note.useMarkdown')}
</label>
<br />
</div>
<div className="edit_form_checkboxes mb3">
<input
type="checkbox"
className="is-markdown pointer"
id="edit_shared"
name="shared"
checked={editNoteState.shared}
onChange={(e) => {
setEditNoteState((x) => ({ ...x, shared: e.target.checked }));
}}
/>{' '}
<label htmlFor="edit_shared" className="mr2">
{t('note.publicLabel')}
</label>
<br />
</div>
<input
type="submit"
className="mr1 pv1 ph2 thm-text-primary ba thm-border-default thm-bg-secondary pointer rdim"
value={t('save')}
/>{' '}
<input
type="reset"
className="pv1 ph2 thm-text-primary ba thm-border-default thm-bg-secondary pointer rdim"
value={t('cancel')}
onClick={() => {
startEdit(false);
}}
/>
</form>
)}
</div>
);
}