mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-16 21:37:51 +00:00
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:
parent
b008b4134b
commit
e23914657f
35
CHANGELOG.md
35
CHANGELOG.md
@ -1,31 +1,50 @@
|
||||
# Changelog
|
||||
|
||||
## [1.2.0] - 2024-03-21
|
||||
## [1.3.0] - 2024-03-21
|
||||
|
||||
### 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
|
||||
- Adicionado modal de detalhes da palavra com significado, sílabas e exemplos
|
||||
- Integrado sistema de tracking de palavras difíceis por aluno
|
||||
|
||||
### Técnico
|
||||
|
||||
- Criado sistema de teste para o componente `WordHighlighter`
|
||||
- Implementada integração com Jest/Vitest para testes de componentes
|
||||
- Adicionado suporte a ARIA labels para acessibilidade
|
||||
- Configurado ambiente de teste com setup adequado
|
||||
|
||||
### Modificado
|
||||
|
||||
- Melhorada a experiência de leitura com destaque visual de palavras importantes
|
||||
- Implementado feedback visual para palavras difíceis e importantes
|
||||
- Adicionado suporte a interatividade nas palavras destacadas
|
||||
|
||||
## [1.1.1] - 2024-03-19
|
||||
## [1.2.1] - 2024-03-21
|
||||
|
||||
### Técnico
|
||||
- Removida lógica de obtenção de URL pública redundante no componente `RecordingHistoryCard`
|
||||
- Simplificada a reprodução de áudio usando diretamente a URL do Supabase Storage
|
||||
- Corrigido problema de CORS na reprodução de áudio
|
||||
- Mantida configuração original do cliente Supabase para melhor compatibilidade
|
||||
- Corrigida ordem de deleção de histórias para evitar problemas de integridade
|
||||
- Otimizado processo de limpeza de arquivos no storage
|
||||
- Melhorado tratamento de erros na deleção de histórias
|
||||
|
||||
### Modificado
|
||||
- Melhorada a experiência de reprodução de áudio no histórico de gravações
|
||||
- Otimizado o carregamento de gravações reduzindo chamadas redundantes
|
||||
- Aprimorado fluxo de exclusão de histórias para garantir remoção completa de recursos
|
||||
- Adicionada confirmação visual durante processo de deleção
|
||||
|
||||
1202
package-lock.json
generated
1202
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -36,6 +36,7 @@
|
||||
"react-router-dom": "^6.28.0",
|
||||
"recharts": "^2.15.0",
|
||||
"resend": "^3.2.0",
|
||||
"shadcn-ui": "^0.9.4",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"uuid": "^11.0.3",
|
||||
"vitest": "^2.1.8"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { supabase } from '../../lib/supabase';
|
||||
import { AudioRecorder } from '../../components/story/AudioRecorder';
|
||||
@ -8,6 +8,7 @@ import { StoryMetrics } from '../../components/story/StoryMetrics';
|
||||
import type { MetricsData } from '../../components/story/StoryMetrics';
|
||||
import { getOptimizedImageUrl } from '../../lib/imageUtils';
|
||||
import { convertWebmToMp3 } from '../../utils/audioConverter';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
|
||||
interface StoryRecording {
|
||||
id: string;
|
||||
@ -385,6 +386,8 @@ export function StoryPage() {
|
||||
const [loadingRecordings, setLoadingRecordings] = React.useState(true);
|
||||
const [metrics, setMetrics] = React.useState<MetricsData | null>(null);
|
||||
const [loadingMetrics, setLoadingMetrics] = React.useState(true);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchStory = async () => {
|
||||
@ -431,28 +434,7 @@ export function StoryPage() {
|
||||
fetchStory();
|
||||
}, [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(() => {
|
||||
const fetchRecordings = async () => {
|
||||
@ -534,6 +516,67 @@ export function StoryPage() {
|
||||
}
|
||||
}, [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) {
|
||||
return (
|
||||
<div className="animate-pulse">
|
||||
@ -681,6 +724,58 @@ export function StoryPage() {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user