Files
Investidor-V2/resources/js/pages/portal/article-editor.tsx
T

252 lines
9.4 KiB
TypeScript

import { Link, useForm } from '@inertiajs/react';
import { useEffect, useState } from 'react';
import { Management, Field, button, control } from '../management/shared';
import EditorialReading, {
type EditorialArticle,
} from '@/components/editorial-reading';
type Content = {
body: string;
image_resource_id: number | null;
image_mode?: string;
};
export default function ArticleEditor({
resource,
content,
source,
images,
revisions,
}: {
resource: {
id: number;
title: string;
metadata_version: number;
published: boolean;
};
content: Content | null;
source?: {
source_url?: string;
image_source_url?: string;
published_at?: string;
modified_at?: string;
legacy_id?: number;
} | null;
images: { id: number; title: string }[];
revisions: {
revision_no: number;
change_type: string;
created_at: string;
}[];
}) {
const initial = () => ({
body: content?.body ?? '',
image_resource_id: content?.image_resource_id
? String(content.image_resource_id)
: '',
image_mode:
content?.image_mode ??
(content?.image_resource_id ? 'selected' : 'none'),
metadata_version: resource.metadata_version,
});
const form = useForm(initial());
const [preview, setPreview] = useState<EditorialArticle | null>(null);
const [error, setError] = useState('');
const [busy, setBusy] = useState(false);
useEffect(() => {
form.setData(initial());
setPreview(null);
setError('');
}, [resource.id, resource.metadata_version]);
useEffect(() => {
setPreview(null);
}, [form.data.body, form.data.image_mode, form.data.image_resource_id]);
const showPreview = async () => {
setBusy(true);
setError('');
try {
const token = document.cookie
.split('; ')
.find((v) => v.startsWith('XSRF-TOKEN='))
?.slice(11);
const response = await fetch(
`/management/portal-services/resources/${resource.id}/article/preview`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-XSRF-TOKEN': decodeURIComponent(token ?? ''),
},
body: JSON.stringify(form.data),
},
);
const data = await response.json().catch(() => ({
message:
'Não foi possível abrir a prévia. Recupere o editor e confirme a sessão e o MFA existentes.',
}));
if (!response.ok)
throw new Error(
Object.values(data.errors ?? {})
.flat()
.join(' ') ||
data.message ||
'Não foi possível preparar a prévia.',
);
setPreview(data.article);
} catch (e) {
setError(
e instanceof Error
? e.message
: 'Não foi possível preparar a prévia.',
);
} finally {
setBusy(false);
}
};
return (
<Management title={`Editar artigo — ${resource.title}`}>
<p>
O original, a origem e as versões permanecem na biblioteca.
Guardar cria uma revisão editorial e retira a publicação até à
sua acção expressa no acervo.
</p>
<p role="status">
Revisão guardada {resource.metadata_version} ·{' '}
{resource.published ? 'Publicado' : 'Rascunho privado'}
</p>
{source && (
<p>
Original {source.legacy_id} · publicado em{' '}
{source.published_at} · modificado em {source.modified_at}.
A imagem de origem mantém a sua referência mesmo quando não
existe exemplar disponível.
</p>
)}
<form
className="max-w-3xl space-y-5"
onSubmit={(e) => {
e.preventDefault();
setError('');
form.put(
`/management/portal-services/resources/${resource.id}/article`,
);
}}
>
{!!Object.keys(form.errors).length && (
<div role="alert">
{Object.values(form.errors).join(' ')}
</div>
)}
<Field label="Corpo do artigo" error={form.errors.body}>
<textarea
className={control}
rows={16}
value={form.data.body}
onChange={(e) => form.setData('body', e.target.value)}
/>
<p>
Texto simples, com uma linha em branco entre parágrafos.
Se mantiver o texto original, a leitura conserva também
a sua formatação; as correcções ficam numa edição
separada.
</p>
</Field>
<Field
label="Tratamento da imagem"
error={form.errors.image_mode}
>
<select
className={control}
value={form.data.image_mode}
onChange={(e) =>
form.setData('image_mode', e.target.value)
}
>
<option value="original">
Manter referência e imagem de origem disponível
</option>
<option value="selected">
Escolher imagem pública da biblioteca
</option>
<option value="none">
Não mostrar imagem nesta edição
</option>
</select>
</Field>
{form.data.image_mode === 'selected' && (
<Field
label="Imagem da biblioteca"
error={form.errors.image_resource_id}
>
<select
className={control}
value={form.data.image_resource_id}
onChange={(e) =>
form.setData(
'image_resource_id',
e.target.value,
)
}
>
<option value="">Seleccione uma imagem</option>
{images.map((i) => (
<option key={i.id} value={i.id}>
{i.title}
</option>
))}
</select>
</Field>
)}
<div className="flex flex-wrap gap-4">
<button className={button} disabled={form.processing}>
Guardar artigo como rascunho
</button>
<button
className={button}
type="button"
disabled={busy}
onClick={() => void showPreview()}
>
Pré-visualizar alterações
</button>
<Link
className="underline"
href={`/management/portal-services/resources/${resource.id}/article/preview`}
>
Ler revisão guardada
</Link>
<Link
className="underline"
href="/management/portal-services"
>
Voltar ao acervo para metadados e publicação
</Link>
</div>
</form>
{error && <p role="alert">{error}</p>}
{preview && (
<section
aria-label="Prévia das alterações"
className="my-6 max-w-3xl rounded border p-5"
>
<EditorialReading article={preview} />
</section>
)}
<h2>Histórico</h2>
<ul>
{revisions.map((r) => (
<li key={r.revision_no}>
Revisão {r.revision_no} · {r.change_type} ·{' '}
{r.created_at}
</li>
))}
</ul>
<p>
Título, descrição, autor, fonte, direitos e acesso são
corrigidos em Editar metadados no acervo. Carregue e analise
imagens na biblioteca antes de as seleccionar. Publicar continua
a ser uma acção separada.
</p>
</Management>
);
}