feat: implementa sistema de deleção de histórias

- Adiciona modal de confirmação de deleção
- Implementa limpeza em cascata de recursos
- Otimiza remoção de arquivos no storage
- Adiciona feedback visual do processo
- Melhora tratamento de erros
- Implementa navegação pós-deleção
This commit is contained in:
Lucas Santana 2024-12-31 06:40:48 -03:00
parent b008b4134b
commit e23914657f
4 changed files with 1158 additions and 219 deletions

View File

@ -1,31 +1,50 @@
# Changelog # Changelog
## [1.2.0] - 2024-03-21 ## [1.3.0] - 2024-03-21
### Adicionado ### Adicionado
- Implementada funcionalidade de deleção de histórias com confirmação
- Adicionado modal de confirmação para exclusão
- Integrado sistema de limpeza automática de recursos
### Técnico
- Implementada lógica de deleção em cascata para recursos relacionados
- Criado fluxo otimizado para remoção de arquivos no storage
- Adicionado tratamento de erros robusto para o processo de deleção
### Modificado
- Melhorada interface de gerenciamento de histórias
- Adicionado feedback visual durante processo de exclusão
- Implementada navegação automática após deleção bem-sucedida
## [1.2.0] - 2024-12-31
### Adicionado
- Implementado componente `WordHighlighter` para destacar palavras importantes durante a leitura - Implementado componente `WordHighlighter` para destacar palavras importantes durante a leitura
- Adicionado modal de detalhes da palavra com significado, sílabas e exemplos - Adicionado modal de detalhes da palavra com significado, sílabas e exemplos
- Integrado sistema de tracking de palavras difíceis por aluno - Integrado sistema de tracking de palavras difíceis por aluno
### Técnico ### Técnico
- Criado sistema de teste para o componente `WordHighlighter` - Criado sistema de teste para o componente `WordHighlighter`
- Implementada integração com Jest/Vitest para testes de componentes - Implementada integração com Jest/Vitest para testes de componentes
- Adicionado suporte a ARIA labels para acessibilidade - Adicionado suporte a ARIA labels para acessibilidade
- Configurado ambiente de teste com setup adequado - Configurado ambiente de teste com setup adequado
### Modificado ### Modificado
- Melhorada a experiência de leitura com destaque visual de palavras importantes - Melhorada a experiência de leitura com destaque visual de palavras importantes
- Implementado feedback visual para palavras difíceis e importantes - Implementado feedback visual para palavras difíceis e importantes
- Adicionado suporte a interatividade nas palavras destacadas - Adicionado suporte a interatividade nas palavras destacadas
## [1.1.1] - 2024-03-19 ## [1.2.1] - 2024-03-21
### Técnico ### Técnico
- Removida lógica de obtenção de URL pública redundante no componente `RecordingHistoryCard` - Corrigida ordem de deleção de histórias para evitar problemas de integridade
- Simplificada a reprodução de áudio usando diretamente a URL do Supabase Storage - Otimizado processo de limpeza de arquivos no storage
- Corrigido problema de CORS na reprodução de áudio - Melhorado tratamento de erros na deleção de histórias
- Mantida configuração original do cliente Supabase para melhor compatibilidade
### Modificado ### Modificado
- Melhorada a experiência de reprodução de áudio no histórico de gravações - Aprimorado fluxo de exclusão de histórias para garantir remoção completa de recursos
- Otimizado o carregamento de gravações reduzindo chamadas redundantes - Adicionada confirmação visual durante processo de deleção

1202
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -36,6 +36,7 @@
"react-router-dom": "^6.28.0", "react-router-dom": "^6.28.0",
"recharts": "^2.15.0", "recharts": "^2.15.0",
"resend": "^3.2.0", "resend": "^3.2.0",
"shadcn-ui": "^0.9.4",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"uuid": "^11.0.3", "uuid": "^11.0.3",
"vitest": "^2.1.8" "vitest": "^2.1.8"

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { ArrowLeft, ArrowRight, Mic, Volume2, Share2, Save, ChevronDown, ChevronUp, Loader2, Pause, Play, Download, RefreshCw } from 'lucide-react'; import { ArrowLeft, ArrowRight, Mic, Volume2, Share2, Save, ChevronDown, ChevronUp, Loader2, Pause, Play, Download, RefreshCw, Trash2 } from 'lucide-react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { supabase } from '../../lib/supabase'; import { supabase } from '../../lib/supabase';
import { AudioRecorder } from '../../components/story/AudioRecorder'; import { AudioRecorder } from '../../components/story/AudioRecorder';
@ -8,6 +8,7 @@ import { StoryMetrics } from '../../components/story/StoryMetrics';
import type { MetricsData } from '../../components/story/StoryMetrics'; import type { MetricsData } from '../../components/story/StoryMetrics';
import { getOptimizedImageUrl } from '../../lib/imageUtils'; import { getOptimizedImageUrl } from '../../lib/imageUtils';
import { convertWebmToMp3 } from '../../utils/audioConverter'; import { convertWebmToMp3 } from '../../utils/audioConverter';
import * as Dialog from '@radix-ui/react-dialog';
interface StoryRecording { interface StoryRecording {
id: string; id: string;
@ -385,6 +386,8 @@ export function StoryPage() {
const [loadingRecordings, setLoadingRecordings] = React.useState(true); const [loadingRecordings, setLoadingRecordings] = React.useState(true);
const [metrics, setMetrics] = React.useState<MetricsData | null>(null); const [metrics, setMetrics] = React.useState<MetricsData | null>(null);
const [loadingMetrics, setLoadingMetrics] = React.useState(true); const [loadingMetrics, setLoadingMetrics] = React.useState(true);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
React.useEffect(() => { React.useEffect(() => {
const fetchStory = async () => { const fetchStory = async () => {
@ -431,28 +434,7 @@ export function StoryPage() {
fetchStory(); fetchStory();
}, [id]); }, [id]);
React.useEffect(() => {
const fetchMetrics = async () => {
if (!story?.id) return;
try {
const { data, error } = await supabase
.from('reading_metrics')
.select('*')
.eq('story_id', story.id)
.single();
if (error) throw error;
setMetrics(data);
} catch (err) {
console.error('Erro ao carregar métricas:', err);
} finally {
setLoadingMetrics(false);
}
};
fetchMetrics();
}, [story?.id]);
React.useEffect(() => { React.useEffect(() => {
const fetchRecordings = async () => { const fetchRecordings = async () => {
@ -534,6 +516,67 @@ export function StoryPage() {
} }
}, [currentPage, story]); }, [currentPage, story]);
const handleDeleteStory = async () => {
if (!story?.id) return;
setIsDeleting(true);
try {
// 1. Primeiro buscar todas as gravações para obter as URLs dos arquivos
const { data: recordings } = await supabase
.from('story_recordings')
.select('audio_url')
.eq('story_id', story.id);
// 2. Deletar arquivos do storage
if (recordings?.length) {
const deletePromises = recordings.map(recording => {
if (!recording.audio_url) return null;
const urlParts = recording.audio_url.split('/recordings/');
if (urlParts.length !== 2) return null;
return supabase.storage
.from('recordings')
.remove([urlParts[1]])
.then(({ error }) => {
if (error) console.error('Erro ao deletar arquivo:', recording.audio_url, error);
});
});
await Promise.all(deletePromises.filter(Boolean));
}
// 3. Deletar as gravações
const { error: recordingsDeleteError } = await supabase
.from('story_recordings')
.delete()
.eq('story_id', story.id);
if (recordingsDeleteError) {
console.error('Erro ao deletar gravações:', recordingsDeleteError);
throw recordingsDeleteError;
}
// 4. Finalmente deletar a história
const { error: storyDeleteError } = await supabase
.from('stories')
.delete()
.eq('id', story.id);
if (storyDeleteError) throw storyDeleteError;
// 5. Redirecionar
navigate('/aluno/historias', { replace: true });
} catch (err) {
console.error('Erro ao deletar história:', err);
setError('Não foi possível deletar a história. Tente novamente.');
} finally {
setIsDeleting(false);
setShowDeleteModal(false);
}
};
if (loading) { if (loading) {
return ( return (
<div className="animate-pulse"> <div className="animate-pulse">
@ -681,6 +724,58 @@ export function StoryPage() {
</div> </div>
</div> </div>
)} )}
{/* Botão e Modal de Deletar História */}
<div className="mt-12 border-t border-gray-200 pt-8">
<button
onClick={() => setShowDeleteModal(true)}
className="flex items-center gap-2 px-4 py-2 text-red-600 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors"
>
<Trash2 className="h-5 w-5" />
Deletar História
</button>
<Dialog.Root open={showDeleteModal} onOpenChange={setShowDeleteModal}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
<Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 bg-white rounded-lg max-w-md w-full p-6">
<Dialog.Title className="text-xl font-bold text-gray-900 mb-4">
Confirmar exclusão
</Dialog.Title>
<p className="text-gray-600 mb-6">
Tem certeza que deseja deletar esta história?
Esta ação não pode ser desfeita e todas as gravações
relacionadas serão removidas.
</p>
<div className="flex justify-end gap-4">
<Dialog.Close className="px-4 py-2 text-gray-600 hover:text-gray-900" disabled={isDeleting}>
Cancelar
</Dialog.Close>
<button
onClick={handleDeleteStory}
disabled={isDeleting}
className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50"
>
{isDeleting ? (
<>
<Loader2 className="h-5 w-5 animate-spin" />
Deletando...
</>
) : (
<>
<Trash2 className="h-5 w-5" />
Deletar
</>
)}
</button>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</div>
</div> </div>
); );
} }