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, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog"; } 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 { interface Essay {
id: string; id: string;
@ -48,6 +53,8 @@ export function EssayPage() {
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [wordCount, setWordCount] = useState(0); const [wordCount, setWordCount] = useState(0);
const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const { session } = useSession();
const { isUpperCase, toggleUppercase, isLoading: isUppercaseLoading } = useUppercasePreference(session?.user?.id);
useEffect(() => { useEffect(() => {
if (id) { if (id) {
@ -157,11 +164,11 @@ export function EssayPage() {
return ( return (
<div className="container mx-auto p-6"> <div className="container mx-auto p-6">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center gap-4 mb-6">
<div className="flex items-center gap-4">
<Button <Button
variant="ghost" variant="ghost"
onClick={() => navigate('/aluno/redacoes')} onClick={() => navigate('/aluno/redacoes')}
className="text-gray-600 hover:text-gray-900"
trackingId="essay-back-to-list-button" trackingId="essay-back-to-list-button"
trackingProperties={{ trackingProperties={{
action: 'back_to_essays_list', action: 'back_to_essays_list',
@ -169,26 +176,55 @@ export function EssayPage() {
}} }}
> >
<ArrowLeft className="mr-2 h-4 w-4" /> <ArrowLeft className="mr-2 h-4 w-4" />
Voltar <AdaptiveText text="Voltar para redações" isUpperCase={isUpperCase} />
</Button> </Button>
<div> <TextCaseToggle
isUpperCase={isUpperCase}
onToggle={toggleUppercase}
isLoading={isUppercaseLoading}
className="ml-auto"
/>
</div>
<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>
) : !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 <Input
value={essay.title} value={essay.title}
onChange={(e) => setEssay({ ...essay, title: e.target.value })} onChange={(e) => setEssay({ ...essay, title: e.target.value })}
className="text-2xl font-bold border-none focus:border-none" className="text-2xl font-bold border-none focus:border-none bg-transparent px-0"
placeholder="Título da redação" placeholder="Título da redação"
/> />
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <div className="flex items-center gap-2 text-sm text-gray-600 mt-1">
<span>{essay.essay_type.title}</span> <AdaptiveText text={essay.essay_type?.title} isUpperCase={isUpperCase} />
<span></span> <span></span>
<span>{essay.essay_genre.title}</span> <AdaptiveText text={essay.essay_genre?.title} isUpperCase={isUpperCase} />
<span></span> <span></span>
<Badge variant={essay.status === 'draft' ? 'secondary' : 'default'}> <Badge variant={essay.status === 'draft' ? 'secondary' : 'default'}>
{essay.status === 'draft' ? 'Rascunho' : 'Enviada'} <AdaptiveText
text={essay.status === 'draft' ? 'Rascunho' : 'Enviada'}
isUpperCase={isUpperCase}
/>
</Badge> </Badge>
</div> </div>
</div> </div>
</div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
variant="outline" variant="outline"
@ -202,8 +238,12 @@ export function EssayPage() {
}} }}
> >
<Save className="mr-2 h-4 w-4" /> <Save className="mr-2 h-4 w-4" />
{saving ? 'Salvando...' : 'Salvar'} <AdaptiveText
text={saving ? 'Salvando...' : 'Salvar'}
isUpperCase={isUpperCase}
/>
</Button> </Button>
{essay.status === 'draft' && ( {essay.status === 'draft' && (
<> <>
<Button <Button
@ -219,8 +259,9 @@ export function EssayPage() {
}} }}
> >
<Send className="mr-2 h-4 w-4" /> <Send className="mr-2 h-4 w-4" />
Enviar para análise <AdaptiveText text="Enviar para análise" isUpperCase={isUpperCase} />
</Button> </Button>
<Button <Button
variant="destructive" variant="destructive"
onClick={() => setShowDeleteDialog(true)} onClick={() => setShowDeleteDialog(true)}
@ -232,7 +273,7 @@ export function EssayPage() {
}} }}
> >
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
Deletar <AdaptiveText text="Deletar" isUpperCase={isUpperCase} />
</Button> </Button>
</> </>
)} )}
@ -244,13 +285,18 @@ export function EssayPage() {
<Textarea <Textarea
value={essay.content} value={essay.content}
onChange={(e) => setEssay({ ...essay, content: e.target.value })} onChange={(e) => setEssay({ ...essay, content: e.target.value })}
className="min-h-[500px] font-mono" className="min-h-[500px] font-mono resize-none p-4"
placeholder="Escreva sua redação aqui..." placeholder="Escreva sua redação aqui..."
/> />
<div className="mt-2 text-sm text-muted-foreground"> <div className="mt-2 text-sm">
<span className={cn(
"font-medium",
isWithinWordLimit ? "text-gray-600" : "text-red-600"
)}>
{wordCount} palavras {wordCount} palavras
</span>
{!isWithinWordLimit && ( {!isWithinWordLimit && (
<span className="text-destructive"> <span className="text-red-600">
{' '}(mínimo: {essay.essay_genre.requirements.min_words}, {' '}(mínimo: {essay.essay_genre.requirements.min_words},
máximo: {essay.essay_genre.requirements.max_words}) máximo: {essay.essay_genre.requirements.max_words})
</span> </span>
@ -258,35 +304,65 @@ export function EssayPage() {
</div> </div>
</div> </div>
<Card> <Card className="h-fit">
<CardContent className="p-4"> <CardContent className="p-4">
<h3 className="font-semibold mb-2">Requisitos do Gênero</h3> <h3 className="font-semibold mb-4">
<div className="text-sm text-muted-foreground"> <AdaptiveText text="Requisitos do Gênero" isUpperCase={isUpperCase} />
<p>Mínimo: {essay.essay_genre.requirements.min_words} palavras</p> </h3>
<p>Máximo: {essay.essay_genre.requirements.max_words} palavras</p> <div className="space-y-4">
<p className="mt-2">Elementos necessários:</p> <div className="bg-purple-50 p-4 rounded-lg border border-purple-100">
<ul className="list-disc list-inside"> <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) => ( {essay.essay_genre.requirements.required_elements.map((element, index) => (
<li key={index}>{element}</li> <li key={index}>
<AdaptiveText text={element} isUpperCase={isUpperCase} />
</li>
))} ))}
</ul> </ul>
</div> </div>
</div>
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
</>
)}
</div>
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}> <AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Você tem certeza?</AlertDialogTitle> <AlertDialogTitle>
<AdaptiveText text="Você tem certeza?" isUpperCase={isUpperCase} />
</AlertDialogTitle>
<AlertDialogDescription> <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> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel> <AlertDialogCancel>
<AdaptiveText text="Cancelar" isUpperCase={isUpperCase} />
</AlertDialogCancel>
<AlertDialogAction onClick={deleteEssay} className="bg-red-600 hover:bg-red-700"> <AlertDialogAction onClick={deleteEssay} className="bg-red-600 hover:bg-red-700">
Deletar <AdaptiveText text="Deletar" isUpperCase={isUpperCase} />
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>