style: padroniza visual do editor de redações

- Alinha estilo com StoryPage e CreateStoryPage
- Adiciona suporte a texto adaptativo
- Melhora feedback visual e estados interativos
- Implementa loading states animados
This commit is contained in:
Lucas Santana 2025-02-06 21:41:08 -03:00
parent e9005e429f
commit 1bc307d599

View File

@ -17,6 +17,11 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { useSession } from '@/hooks/useSession';
import { useUppercasePreference } from '@/hooks/useUppercasePreference';
import { AdaptiveText, AdaptiveTitle, AdaptiveParagraph } from '@/components/ui/adaptive-text';
import { TextCaseToggle } from '@/components/ui/text-case-toggle';
import { cn } from '@/lib/utils';
interface Essay {
id: string;
@ -48,6 +53,8 @@ export function EssayPage() {
const [saving, setSaving] = useState(false);
const [wordCount, setWordCount] = useState(0);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const { session } = useSession();
const { isUpperCase, toggleUppercase, isLoading: isUppercaseLoading } = useUppercasePreference(session?.user?.id);
useEffect(() => {
if (id) {
@ -157,136 +164,205 @@ export function EssayPage() {
return (
<div className="container mx-auto p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4">
<Button
variant="ghost"
onClick={() => navigate('/aluno/redacoes')}
trackingId="essay-back-to-list-button"
trackingProperties={{
action: 'back_to_essays_list',
page: 'essay_editor'
}}
>
<ArrowLeft className="mr-2 h-4 w-4" />
Voltar
</Button>
<div>
<Input
value={essay.title}
onChange={(e) => setEssay({ ...essay, title: e.target.value })}
className="text-2xl font-bold border-none focus:border-none"
placeholder="Título da redação"
/>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>{essay.essay_type.title}</span>
<span></span>
<span>{essay.essay_genre.title}</span>
<span></span>
<Badge variant={essay.status === 'draft' ? 'secondary' : 'default'}>
{essay.status === 'draft' ? 'Rascunho' : 'Enviada'}
</Badge>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
onClick={saveEssay}
disabled={saving}
trackingId="essay-save-button"
trackingProperties={{
action: 'save_essay',
page: 'essay_editor',
status: essay.status
}}
>
<Save className="mr-2 h-4 w-4" />
{saving ? 'Salvando...' : 'Salvar'}
</Button>
{essay.status === 'draft' && (
<>
<Button
onClick={submitForAnalysis}
disabled={!isWithinWordLimit}
title={!isWithinWordLimit ? 'Número de palavras fora do limite' : ''}
trackingId="essay-submit-analysis-button"
trackingProperties={{
action: 'submit_for_analysis',
page: 'essay_editor',
word_count: wordCount,
within_limit: isWithinWordLimit
}}
>
<Send className="mr-2 h-4 w-4" />
Enviar para análise
</Button>
<Button
variant="destructive"
onClick={() => setShowDeleteDialog(true)}
trackingId="essay-delete-button"
trackingProperties={{
action: 'delete_essay',
page: 'essay_editor',
status: essay.status
}}
>
<Trash2 className="mr-2 h-4 w-4" />
Deletar
</Button>
</>
)}
</div>
<div className="flex items-center gap-4 mb-6">
<Button
variant="ghost"
onClick={() => navigate('/aluno/redacoes')}
className="text-gray-600 hover:text-gray-900"
trackingId="essay-back-to-list-button"
trackingProperties={{
action: 'back_to_essays_list',
page: 'essay_editor'
}}
>
<ArrowLeft className="mr-2 h-4 w-4" />
<AdaptiveText text="Voltar para redações" isUpperCase={isUpperCase} />
</Button>
<TextCaseToggle
isUpperCase={isUpperCase}
onToggle={toggleUppercase}
isLoading={isUppercaseLoading}
className="ml-auto"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="md:col-span-3">
<Textarea
value={essay.content}
onChange={(e) => setEssay({ ...essay, content: e.target.value })}
className="min-h-[500px] font-mono"
placeholder="Escreva sua redação aqui..."
/>
<div className="mt-2 text-sm text-muted-foreground">
{wordCount} palavras
{!isWithinWordLimit && (
<span className="text-destructive">
{' '}(mínimo: {essay.essay_genre.requirements.min_words},
máximo: {essay.essay_genre.requirements.max_words})
</span>
)}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-6">
{loading ? (
<div className="animate-pulse space-y-4">
<div className="h-8 bg-gray-100 rounded w-1/3" />
<div className="h-4 bg-gray-100 rounded w-1/4" />
<div className="h-64 bg-gray-100 rounded mt-8" />
</div>
</div>
) : !essay ? (
<div className="text-center py-12">
<AdaptiveText
text="Redação não encontrada"
isUpperCase={isUpperCase}
className="text-gray-600"
/>
</div>
) : (
<>
<div className="flex items-center justify-between">
<div className="flex-1">
<Input
value={essay.title}
onChange={(e) => setEssay({ ...essay, title: e.target.value })}
className="text-2xl font-bold border-none focus:border-none bg-transparent px-0"
placeholder="Título da redação"
/>
<div className="flex items-center gap-2 text-sm text-gray-600 mt-1">
<AdaptiveText text={essay.essay_type?.title} isUpperCase={isUpperCase} />
<span></span>
<AdaptiveText text={essay.essay_genre?.title} isUpperCase={isUpperCase} />
<span></span>
<Badge variant={essay.status === 'draft' ? 'secondary' : 'default'}>
<AdaptiveText
text={essay.status === 'draft' ? 'Rascunho' : 'Enviada'}
isUpperCase={isUpperCase}
/>
</Badge>
</div>
</div>
<Card>
<CardContent className="p-4">
<h3 className="font-semibold mb-2">Requisitos do Gênero</h3>
<div className="text-sm text-muted-foreground">
<p>Mínimo: {essay.essay_genre.requirements.min_words} palavras</p>
<p>Máximo: {essay.essay_genre.requirements.max_words} palavras</p>
<p className="mt-2">Elementos necessários:</p>
<ul className="list-disc list-inside">
{essay.essay_genre.requirements.required_elements.map((element, index) => (
<li key={index}>{element}</li>
))}
</ul>
<div className="flex items-center gap-2">
<Button
variant="outline"
onClick={saveEssay}
disabled={saving}
trackingId="essay-save-button"
trackingProperties={{
action: 'save_essay',
page: 'essay_editor',
status: essay.status
}}
>
<Save className="mr-2 h-4 w-4" />
<AdaptiveText
text={saving ? 'Salvando...' : 'Salvar'}
isUpperCase={isUpperCase}
/>
</Button>
{essay.status === 'draft' && (
<>
<Button
onClick={submitForAnalysis}
disabled={!isWithinWordLimit}
title={!isWithinWordLimit ? 'Número de palavras fora do limite' : ''}
trackingId="essay-submit-analysis-button"
trackingProperties={{
action: 'submit_for_analysis',
page: 'essay_editor',
word_count: wordCount,
within_limit: isWithinWordLimit
}}
>
<Send className="mr-2 h-4 w-4" />
<AdaptiveText text="Enviar para análise" isUpperCase={isUpperCase} />
</Button>
<Button
variant="destructive"
onClick={() => setShowDeleteDialog(true)}
trackingId="essay-delete-button"
trackingProperties={{
action: 'delete_essay',
page: 'essay_editor',
status: essay.status
}}
>
<Trash2 className="mr-2 h-4 w-4" />
<AdaptiveText text="Deletar" isUpperCase={isUpperCase} />
</Button>
</>
)}
</div>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="md:col-span-3">
<Textarea
value={essay.content}
onChange={(e) => setEssay({ ...essay, content: e.target.value })}
className="min-h-[500px] font-mono resize-none p-4"
placeholder="Escreva sua redação aqui..."
/>
<div className="mt-2 text-sm">
<span className={cn(
"font-medium",
isWithinWordLimit ? "text-gray-600" : "text-red-600"
)}>
{wordCount} palavras
</span>
{!isWithinWordLimit && (
<span className="text-red-600">
{' '}(mínimo: {essay.essay_genre.requirements.min_words},
máximo: {essay.essay_genre.requirements.max_words})
</span>
)}
</div>
</div>
<Card className="h-fit">
<CardContent className="p-4">
<h3 className="font-semibold mb-4">
<AdaptiveText text="Requisitos do Gênero" isUpperCase={isUpperCase} />
</h3>
<div className="space-y-4">
<div className="bg-purple-50 p-4 rounded-lg border border-purple-100">
<div className="space-y-2 text-sm text-purple-800">
<p>
<AdaptiveText
text={`Mínimo: ${essay.essay_genre.requirements.min_words} palavras`}
isUpperCase={isUpperCase}
/>
</p>
<p>
<AdaptiveText
text={`Máximo: ${essay.essay_genre.requirements.max_words} palavras`}
isUpperCase={isUpperCase}
/>
</p>
<p className="mt-2 font-medium">
<AdaptiveText text="Elementos necessários:" isUpperCase={isUpperCase} />
</p>
<ul className="list-disc list-inside space-y-1">
{essay.essay_genre.requirements.required_elements.map((element, index) => (
<li key={index}>
<AdaptiveText text={element} isUpperCase={isUpperCase} />
</li>
))}
</ul>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</>
)}
</div>
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Você tem certeza?</AlertDialogTitle>
<AlertDialogTitle>
<AdaptiveText text="Você tem certeza?" isUpperCase={isUpperCase} />
</AlertDialogTitle>
<AlertDialogDescription>
Esta ação não pode ser desfeita. Isso excluirá permanentemente sua redação.
<AdaptiveText
text="Esta ação não pode ser desfeita. Isso excluirá permanentemente sua redação."
isUpperCase={isUpperCase}
/>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogCancel>
<AdaptiveText text="Cancelar" isUpperCase={isUpperCase} />
</AlertDialogCancel>
<AlertDialogAction onClick={deleteEssay} className="bg-red-600 hover:bg-red-700">
Deletar
<AdaptiveText text="Deletar" isUpperCase={isUpperCase} />
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>