mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-17 05:47:52 +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
|
# 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
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",
|
"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"
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user