mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-16 21:37:51 +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,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
PenTool
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
@ -69,6 +70,21 @@ export function StudentDashboardLayout() {
|
||||
{!isCollapsed && <span>Painel</span>}
|
||||
</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
|
||||
to="/aluno/conquistas"
|
||||
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 { PhonicsPage } from "./pages/student-dashboard/PhonicsPage";
|
||||
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 }) {
|
||||
return (
|
||||
@ -219,6 +223,27 @@ export const router = createBrowserRouter([
|
||||
{
|
||||
path: 'fonicos/progresso',
|
||||
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