mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-18 06:17:56 +00:00
feat: Implementando páginas de essays
This commit is contained in:
parent
f602f4c666
commit
d1e44f84b7
24
src/components/ui/textarea.tsx
Normal file
24
src/components/ui/textarea.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export interface TextareaProps
|
||||||
|
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||||
|
|
||||||
|
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Textarea.displayName = "Textarea"
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
@ -12,6 +12,7 @@ import {
|
|||||||
X,
|
X,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
PenTool
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAuth } from '../../hooks/useAuth';
|
import { useAuth } from '../../hooks/useAuth';
|
||||||
import * as Dialog from '@radix-ui/react-dialog';
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
@ -69,6 +70,21 @@ export function StudentDashboardLayout() {
|
|||||||
{!isCollapsed && <span>Painel</span>}
|
{!isCollapsed && <span>Painel</span>}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
|
<NavLink
|
||||||
|
to="/aluno/redacoes"
|
||||||
|
onClick={handleNavigation}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`flex items-center gap-2 px-4 py-2 rounded-lg text-sm ${
|
||||||
|
isActive
|
||||||
|
? 'bg-purple-50 text-purple-700'
|
||||||
|
: 'text-gray-600 hover:bg-gray-50'
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PenTool className="h-5 w-5" />
|
||||||
|
{!isCollapsed && <span>Redações</span>}
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
{/* <NavLink
|
{/* <NavLink
|
||||||
to="/aluno/conquistas"
|
to="/aluno/conquistas"
|
||||||
onClick={handleNavigation}
|
onClick={handleNavigation}
|
||||||
|
|||||||
214
src/pages/student-dashboard/essays/[id].tsx
Normal file
214
src/pages/student-dashboard/essays/[id].tsx
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { supabase } from '@/lib/supabase';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { ArrowLeft, Save, Send } from 'lucide-react';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
|
||||||
|
interface Essay {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
type_id: string;
|
||||||
|
genre_id: string;
|
||||||
|
status: 'draft' | 'submitted' | 'analyzed';
|
||||||
|
essay_type: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
essay_genre: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
requirements: {
|
||||||
|
min_words: number;
|
||||||
|
max_words: number;
|
||||||
|
required_elements: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EssayPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { id } = router.query;
|
||||||
|
const { supabase } = supabase();
|
||||||
|
const [essay, setEssay] = useState<Essay | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [wordCount, setWordCount] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
loadEssay();
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (essay?.content) {
|
||||||
|
const words = essay.content.trim().split(/\s+/).length;
|
||||||
|
setWordCount(words);
|
||||||
|
}
|
||||||
|
}, [essay?.content]);
|
||||||
|
|
||||||
|
async function loadEssay() {
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('student_essays')
|
||||||
|
.select(`
|
||||||
|
*,
|
||||||
|
essay_type:essay_types(*),
|
||||||
|
essay_genre:essay_genres(*)
|
||||||
|
`)
|
||||||
|
.eq('id', id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
setEssay(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar redação:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveEssay() {
|
||||||
|
if (!essay) return;
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('student_essays')
|
||||||
|
.update({
|
||||||
|
title: essay.title,
|
||||||
|
content: essay.content
|
||||||
|
})
|
||||||
|
.eq('id', essay.id);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao salvar redação:', error);
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitForAnalysis() {
|
||||||
|
if (!essay) return;
|
||||||
|
try {
|
||||||
|
// Primeiro atualiza o status
|
||||||
|
const { error: updateError } = await supabase
|
||||||
|
.from('student_essays')
|
||||||
|
.update({ status: 'submitted' })
|
||||||
|
.eq('id', essay.id);
|
||||||
|
|
||||||
|
if (updateError) throw updateError;
|
||||||
|
|
||||||
|
// Chama a Edge Function para análise
|
||||||
|
const { error: analysisError } = await supabase.functions.invoke('analyze-essay', {
|
||||||
|
body: {
|
||||||
|
essay_id: essay.id,
|
||||||
|
content: essay.content,
|
||||||
|
type_id: essay.type_id,
|
||||||
|
genre_id: essay.genre_id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (analysisError) throw analysisError;
|
||||||
|
|
||||||
|
// Redireciona para a página de análise
|
||||||
|
router.push(`/student-dashboard/essays/${essay.id}/analysis`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao enviar para análise:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) return <div>Carregando...</div>;
|
||||||
|
if (!essay) return <div>Redação não encontrada</div>;
|
||||||
|
|
||||||
|
const isWithinWordLimit =
|
||||||
|
wordCount >= (essay.essay_genre.requirements.min_words || 0) &&
|
||||||
|
wordCount <= (essay.essay_genre.requirements.max_words || Infinity);
|
||||||
|
|
||||||
|
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={() => router.push('/student-dashboard/essays')}>
|
||||||
|
<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' : 'primary'}>
|
||||||
|
{essay.status === 'draft' ? 'Rascunho' : 'Enviada'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" onClick={saveEssay} disabled={saving}>
|
||||||
|
<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' : ''}
|
||||||
|
>
|
||||||
|
<Send className="mr-2 h-4 w-4" />
|
||||||
|
Enviar para análise
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
238
src/pages/student-dashboard/essays/[id]/analysis.tsx
Normal file
238
src/pages/student-dashboard/essays/[id]/analysis.tsx
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { supabase } from '@/lib/supabase';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { ArrowLeft, CheckCircle2, XCircle } from 'lucide-react';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
|
||||||
|
interface EssayAnalysis {
|
||||||
|
id: string;
|
||||||
|
essay_id: string;
|
||||||
|
overall_score: number;
|
||||||
|
feedback: {
|
||||||
|
structure: string;
|
||||||
|
content: string;
|
||||||
|
language: string;
|
||||||
|
};
|
||||||
|
strengths: string[];
|
||||||
|
improvements: string[];
|
||||||
|
suggestions: string;
|
||||||
|
criteria_scores: {
|
||||||
|
adequacy: number;
|
||||||
|
coherence: number;
|
||||||
|
cohesion: number;
|
||||||
|
vocabulary: number;
|
||||||
|
grammar: number;
|
||||||
|
};
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Essay {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
essay_type: {
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
essay_genre: {
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EssayAnalysisPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { id } = router.query;
|
||||||
|
const { supabase } = supabase();
|
||||||
|
const [analysis, setAnalysis] = useState<EssayAnalysis | null>(null);
|
||||||
|
const [essay, setEssay] = useState<Essay | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
loadEssayAndAnalysis();
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
async function loadEssayAndAnalysis() {
|
||||||
|
try {
|
||||||
|
// Carregar redação
|
||||||
|
const { data: essayData, error: essayError } = await supabase
|
||||||
|
.from('student_essays')
|
||||||
|
.select(`
|
||||||
|
*,
|
||||||
|
essay_type:essay_types(title),
|
||||||
|
essay_genre:essay_genres(title)
|
||||||
|
`)
|
||||||
|
.eq('id', id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (essayError) throw essayError;
|
||||||
|
setEssay(essayData);
|
||||||
|
|
||||||
|
// Carregar análise
|
||||||
|
const { data: analysisData, error: analysisError } = await supabase
|
||||||
|
.from('essay_analyses')
|
||||||
|
.select('*')
|
||||||
|
.eq('essay_id', id)
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (analysisError) throw analysisError;
|
||||||
|
setAnalysis(analysisData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar dados:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) return <div>Carregando...</div>;
|
||||||
|
if (!essay || !analysis) return <div>Análise não encontrada</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
<div className="flex items-center gap-4 mb-6">
|
||||||
|
<Button variant="ghost" onClick={() => router.push(`/student-dashboard/essays/${id}`)}>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Voltar para redação
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">{essay.title}</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{essay.essay_type.title} • {essay.essay_genre.title}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
{/* Pontuação Geral */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Pontuação Geral</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<div className="text-6xl font-bold text-primary">
|
||||||
|
{analysis.overall_score}
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl ml-1">/100</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Pontos Fortes */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="h-5 w-5 text-success" />
|
||||||
|
Pontos Fortes
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ul className="list-disc list-inside space-y-2">
|
||||||
|
{analysis.strengths.map((strength, index) => (
|
||||||
|
<li key={index} className="text-success">{strength}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Pontos a Melhorar */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<XCircle className="h-5 w-5 text-destructive" />
|
||||||
|
Pontos a Melhorar
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ul className="list-disc list-inside space-y-2">
|
||||||
|
{analysis.improvements.map((improvement, index) => (
|
||||||
|
<li key={index} className="text-destructive">{improvement}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Feedback Detalhado */}
|
||||||
|
<Card className="md:col-span-2">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Feedback Detalhado</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2">Estrutura</h4>
|
||||||
|
<p className="text-muted-foreground">{analysis.feedback.structure}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2">Conteúdo</h4>
|
||||||
|
<p className="text-muted-foreground">{analysis.feedback.content}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2">Linguagem</h4>
|
||||||
|
<p className="text-muted-foreground">{analysis.feedback.language}</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Critérios de Avaliação */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Critérios de Avaliação</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between mb-1">
|
||||||
|
<span>Adequação ao Gênero</span>
|
||||||
|
<span>{analysis.criteria_scores.adequacy}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={analysis.criteria_scores.adequacy} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between mb-1">
|
||||||
|
<span>Coerência</span>
|
||||||
|
<span>{analysis.criteria_scores.coherence}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={analysis.criteria_scores.coherence} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between mb-1">
|
||||||
|
<span>Coesão</span>
|
||||||
|
<span>{analysis.criteria_scores.cohesion}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={analysis.criteria_scores.cohesion} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between mb-1">
|
||||||
|
<span>Vocabulário</span>
|
||||||
|
<span>{analysis.criteria_scores.vocabulary}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={analysis.criteria_scores.vocabulary} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between mb-1">
|
||||||
|
<span>Gramática</span>
|
||||||
|
<span>{analysis.criteria_scores.grammar}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={analysis.criteria_scores.grammar} />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Sugestões */}
|
||||||
|
<Card className="md:col-span-3">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Sugestões para Melhoria</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-muted-foreground whitespace-pre-line">
|
||||||
|
{analysis.suggestions}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
113
src/pages/student-dashboard/essays/index.tsx
Normal file
113
src/pages/student-dashboard/essays/index.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { supabase } from '@/lib/supabase';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { PlusCircle } from 'lucide-react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
|
||||||
|
interface Essay {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
type_id: string;
|
||||||
|
genre_id: string;
|
||||||
|
status: 'draft' | 'submitted' | 'analyzed';
|
||||||
|
created_at: string;
|
||||||
|
essay_type: {
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
essay_genre: {
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EssaysPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { supabase } = supabase();
|
||||||
|
const [essays, setEssays] = useState<Essay[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadEssays();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function loadEssays() {
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('student_essays')
|
||||||
|
.select(`
|
||||||
|
*,
|
||||||
|
essay_type:essay_types(title),
|
||||||
|
essay_genre:essay_genres(title)
|
||||||
|
`)
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
setEssays(data || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar redações:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusBadge(status: Essay['status']) {
|
||||||
|
const statusMap = {
|
||||||
|
draft: { label: 'Rascunho', variant: 'secondary' },
|
||||||
|
submitted: { label: 'Enviada', variant: 'primary' },
|
||||||
|
analyzed: { label: 'Analisada', variant: 'success' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const { label, variant } = statusMap[status];
|
||||||
|
return <Badge variant={variant as any}>{label}</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-3xl font-bold">Minhas Redações</h1>
|
||||||
|
<Button onClick={() => router.push('/student-dashboard/essays/new')}>
|
||||||
|
<PlusCircle className="mr-2 h-4 w-4" />
|
||||||
|
Nova Redação
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div>Carregando...</div>
|
||||||
|
) : essays.length === 0 ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="flex flex-col items-center justify-center p-6">
|
||||||
|
<p className="text-muted-foreground mb-4">Você ainda não tem nenhuma redação</p>
|
||||||
|
<Button onClick={() => router.push('/student-dashboard/essays/new')}>
|
||||||
|
Criar Primeira Redação
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{essays.map((essay) => (
|
||||||
|
<Card key={essay.id} className="cursor-pointer hover:shadow-lg transition-shadow"
|
||||||
|
onClick={() => router.push(`/student-dashboard/essays/${essay.id}`)}>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<CardTitle>{essay.title || 'Sem título'}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{essay.essay_type?.title} - {essay.essay_genre?.title}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
{getStatusBadge(essay.status)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Criada em {new Date(essay.created_at).toLocaleDateString('pt-BR')}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
183
src/pages/student-dashboard/essays/new.tsx
Normal file
183
src/pages/student-dashboard/essays/new.tsx
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { supabase } from '@/lib/supabase';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { ArrowLeft } from 'lucide-react';
|
||||||
|
|
||||||
|
interface EssayType {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EssayGenre {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
requirements: {
|
||||||
|
min_words: number;
|
||||||
|
max_words: number;
|
||||||
|
required_elements: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NewEssayPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { supabase } = supabase();
|
||||||
|
const [step, setStep] = useState<'type' | 'genre'>('type');
|
||||||
|
const [selectedType, setSelectedType] = useState<EssayType | null>(null);
|
||||||
|
const [types, setTypes] = useState<EssayType[]>([]);
|
||||||
|
const [genres, setGenres] = useState<EssayGenre[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Carregar tipos textuais
|
||||||
|
async function loadTypes() {
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('essay_types')
|
||||||
|
.select('*')
|
||||||
|
.eq('active', true);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
setTypes(data || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar tipos:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carregar gêneros do tipo selecionado
|
||||||
|
async function loadGenres(typeId: string) {
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('essay_genres')
|
||||||
|
.select('*')
|
||||||
|
.eq('type_id', typeId)
|
||||||
|
.eq('active', true);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
setGenres(data || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar gêneros:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Criar nova redação
|
||||||
|
async function createEssay(genreId: string) {
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('student_essays')
|
||||||
|
.insert({
|
||||||
|
type_id: selectedType!.id,
|
||||||
|
genre_id: genreId,
|
||||||
|
status: 'draft',
|
||||||
|
title: 'Nova Redação',
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
router.push(`/student-dashboard/essays/${data.id}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao criar redação:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renderizar seleção de tipo textual
|
||||||
|
function renderTypeSelection() {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{types.map((type) => (
|
||||||
|
<Card
|
||||||
|
key={type.id}
|
||||||
|
className={`cursor-pointer hover:shadow-lg transition-shadow ${
|
||||||
|
selectedType?.id === type.id ? 'ring-2 ring-primary' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedType(type);
|
||||||
|
loadGenres(type.id);
|
||||||
|
setStep('genre');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<span>{type.icon}</span>
|
||||||
|
{type.title}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>{type.description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renderizar seleção de gênero textual
|
||||||
|
function renderGenreSelection() {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{genres.map((genre) => (
|
||||||
|
<Card
|
||||||
|
key={genre.id}
|
||||||
|
className="cursor-pointer hover:shadow-lg transition-shadow"
|
||||||
|
onClick={() => createEssay(genre.id)}
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<span>{genre.icon}</span>
|
||||||
|
{genre.title}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>{genre.description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
<p>Mínimo: {genre.requirements.min_words} palavras</p>
|
||||||
|
<p>Máximo: {genre.requirements.max_words} palavras</p>
|
||||||
|
<p className="mt-2">Elementos necessários:</p>
|
||||||
|
<ul className="list-disc list-inside">
|
||||||
|
{genre.requirements.required_elements.map((element, index) => (
|
||||||
|
<li key={index}>{element}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
<div className="flex items-center gap-4 mb-6">
|
||||||
|
{step === 'genre' && (
|
||||||
|
<Button variant="ghost" onClick={() => setStep('type')}>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Voltar
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">Nova Redação</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{step === 'type'
|
||||||
|
? 'Selecione o tipo textual'
|
||||||
|
: 'Selecione o gênero textual'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div>Carregando...</div>
|
||||||
|
) : step === 'type' ? (
|
||||||
|
renderTypeSelection()
|
||||||
|
) : (
|
||||||
|
renderGenreSelection()
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -33,6 +33,10 @@ import { EvidenceBased } from './pages/landing/EvidenceBased';
|
|||||||
import { TextSalesLetter } from './pages/landing/TextSalesLetter';
|
import { TextSalesLetter } from './pages/landing/TextSalesLetter';
|
||||||
import { PhonicsPage } from "./pages/student-dashboard/PhonicsPage";
|
import { PhonicsPage } from "./pages/student-dashboard/PhonicsPage";
|
||||||
import { PhonicsProgressPage } from "./pages/student-dashboard/PhonicsProgressPage";
|
import { PhonicsProgressPage } from "./pages/student-dashboard/PhonicsProgressPage";
|
||||||
|
import { EssaysPage } from './pages/student-dashboard/essays';
|
||||||
|
import { NewEssayPage } from './pages/student-dashboard/essays/new';
|
||||||
|
import { EssayPage } from './pages/student-dashboard/essays/[id]';
|
||||||
|
import { EssayAnalysisPage } from './pages/student-dashboard/essays/[id]/analysis';
|
||||||
|
|
||||||
function RootLayout({ children }: { children: React.ReactNode }) {
|
function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
@ -219,6 +223,27 @@ export const router = createBrowserRouter([
|
|||||||
{
|
{
|
||||||
path: 'fonicos/progresso',
|
path: 'fonicos/progresso',
|
||||||
element: <PhonicsProgressPage />,
|
element: <PhonicsProgressPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'redacoes',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
element: <EssaysPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'nova',
|
||||||
|
element: <NewEssayPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id',
|
||||||
|
element: <EssayPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id/analise',
|
||||||
|
element: <EssayAnalysisPage />,
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user