@@ -184,7 +208,7 @@ export function MaterialDetailModal({
{/* Botones de acción */}
}
onPress={() => {
downloadMaterial.mutate(material.id, {
@@ -205,6 +229,7 @@ export function MaterialDetailModal({
{isOwner && (
<>
}
onPress={() => onEdit?.(material)}
@@ -212,8 +237,8 @@ export function MaterialDetailModal({
Editar
}
onPress={() => onDelete?.(material.id)}
>
diff --git a/app/components/material-stats-modal.tsx b/app/components/material-stats-modal.tsx
index 0a51772..cfc32a7 100644
--- a/app/components/material-stats-modal.tsx
+++ b/app/components/material-stats-modal.tsx
@@ -8,22 +8,17 @@ import {
Card,
CardBody,
Chip,
- Select,
- SelectItem,
+ Spinner,
Tab,
Tabs,
} from '@heroui/react';
-import {
- BarChart3,
- Download,
- Eye,
- FileDown,
- MessageCircle,
- Star,
- TrendingUp,
- X,
-} from 'lucide-react';
+import { Download, Eye, FileDown, MessageCircle, Star, X } from 'lucide-react';
import { useState } from 'react';
+import {
+ useMaterialComments,
+ useMaterialRatingSummary,
+} from '~/lib/hooks/useMaterials';
+import { materialsService } from '~/lib/services/materials.service';
import type { Material } from '~/lib/types/api.types';
interface MaterialStatsModalProps {
@@ -32,72 +27,51 @@ interface MaterialStatsModalProps {
onClose: () => void;
}
-interface Comment {
- id: string;
- userName: string;
- content: string;
- rating: number;
- date: string;
-}
-
export function MaterialStatsModal({
material,
isOpen,
onClose,
}: MaterialStatsModalProps) {
- const [timeRange, setTimeRange] = useState('30d');
+ const [_timeRange, _setTimeRange] = useState('30d');
const [activeTab, setActiveTab] = useState('overview');
- if (!isOpen || !material) return null;
+ // Obtener datos reales de estadísticas y comentarios
+ const { data: ratingSummary, isLoading } = useMaterialRatingSummary(
+ material?.id || '',
+ );
+ const { data: comments = [], isLoading: isLoadingComments } =
+ useMaterialComments(material?.id || '');
- // Datos mock para estadísticas
- const stats = {
- totalDescargas: 1247,
- totalVistas: 3891,
- calificacionPromedio: 4.3,
- totalComentarios: 23,
- tendenciaDescargas: [
- { fecha: '2024-01-01', descargas: 45 },
- { fecha: '2024-01-02', descargas: 52 },
- { fecha: '2024-01-03', descargas: 38 },
- { fecha: '2024-01-04', descargas: 67 },
- { fecha: '2024-01-05', descargas: 71 },
- ],
- };
+ if (!isOpen || !material) return null;
- const comentarios: Comment[] = [
- {
- id: '1',
- userName: 'Ana García',
- content: 'Excelente material, muy bien explicado y útil para el curso.',
- rating: 5,
- date: '2024-01-15',
- },
- {
- id: '2',
- userName: 'Carlos López',
- content: 'Buen contenido, aunque podría tener más ejemplos prácticos.',
- rating: 4,
- date: '2024-01-14',
- },
- {
- id: '3',
- userName: 'María Rodríguez',
- content: 'Material muy completo y actualizado. Lo recomiendo.',
- rating: 5,
- date: '2024-01-13',
- },
- ];
+ // Usar datos reales si están disponibles, sino datos por defecto
+ const stats = ratingSummary
+ ? {
+ totalDescargas: ratingSummary.totalDescargas,
+ totalVistas: ratingSummary.totalVistas,
+ calificacionPromedio: ratingSummary.calificacionPromedio,
+ totalComentarios: ratingSummary.totalCalificaciones,
+ }
+ : {
+ totalDescargas: 0,
+ totalVistas: 0,
+ calificacionPromedio: 0,
+ totalComentarios: 0,
+ };
- const handleExportData = (format: 'csv' | 'pdf') => {
- // Simular exportación
- const fileName = `estadisticas_${material.nombre.replace(/\s+/g, '_')}.${format}`;
- console.log(`Exportando estadísticas como ${format}: ${fileName}`);
- alert(`Descargando reporte en formato ${format.toUpperCase()}`);
+ const handleExportData = async (format: 'pdf') => {
+ try {
+ if (format === 'pdf') {
+ await materialsService.exportStatsPdf(material.id);
+ }
+ } catch (error) {
+ console.error('Error al exportar PDF:', error);
+ alert('Error al descargar el PDF. Intenta nuevamente.');
+ }
};
return (
-
+
{/* Header */}
@@ -122,171 +96,161 @@ export function MaterialStatsModal({
variant="underlined"
>
-
{/* Content */}
- {activeTab === 'overview' && (
-
- {/* Métricas principales */}
-
-
-
-
-
-
-
- {stats.totalDescargas.toLocaleString()}
-
- Total Descargas
-
-
-
-
-
-
-
-
-
- {stats.totalVistas.toLocaleString()}
-
- Total Vistas
-
-
-
-
-
-
-
-
-
- {stats.calificacionPromedio}
-
-
- Calificación Promedio
-
-
-
-
-
-
-
-
-
-
- {stats.totalComentarios}
-
- Comentarios
-
-
-
-
- {/* Acciones de exportación */}
-
-
Exportar Datos
-
- }
- onPress={() => handleExportData('csv')}
- >
- Exportar CSV
-
- }
- onPress={() => handleExportData('pdf')}
- >
- Exportar PDF
-
-
-
+ {isLoading ? (
+
+
- )}
+ ) : (
+ <>
+ {activeTab === 'overview' && (
+
+ {/* Métricas principales */}
+
+
+
+
+
+
+
+ {stats.totalDescargas.toLocaleString()}
+
+
+ Total Descargas
+
+
+
+
+
+
+
+
+
+
+ {stats.totalVistas.toLocaleString()}
+
+ Total Vistas
+
+
- {activeTab === 'trends' && (
-
- {/* Selector de rango */}
-
-
-
+
+
+
+
+
+
+ {stats.calificacionPromedio}
+
+
+ Calificación Promedio
+
+
+
- {/* Gráfico simulado */}
-
-
-
-
-
Tendencia de Descargas
+
+
+
+
+
+
+ {stats.totalComentarios}
+
+ Comentarios
+
+
-
-
-
-
Gráfico de tendencias
-
- Se mostrará cuando esté conectado al backend
-
+
+ {/* Acciones de exportación */}
+
+
Exportar Datos
+
+ }
+ onPress={() => handleExportData('pdf')}
+ >
+ Exportar PDF
+
-
-
-
- )}
+
+ )}
- {activeTab === 'comments' && (
-
-
-
Comentarios Recientes
-
- {comentarios.length} comentarios
-
-
+ {activeTab === 'comments' && (
+
+ {isLoadingComments ? (
+
+
+
+ ) : (
+ <>
+
+
+ Comentarios Recientes
+
+
+ {comments.length} comentarios
+
+
-
- {comentarios.map((comment) => (
-
-
-
-
-
- {comment.userName}
-
-
- {new Date(comment.date).toLocaleDateString()}
-
+ {comments.length === 0 ? (
+
+
+
No hay comentarios aún
-
- {[...Array(5)].map((_, i) => (
-
+ ) : (
+
+ {comments.map((comment) => (
+
+
+
+
+
+ {comment.usuarioNombre || 'Usuario'}
+
+
+ {new Date(
+ comment.createdAt,
+ ).toLocaleDateString()}
+
+
+
+ {[...Array(5)].map((_, i) => (
+
+ ))}
+
+
+ {comment.comentario && (
+
+ {comment.comentario}
+
+ )}
+
+
))}
-
-
{comment.content}
-
-
- ))}
-
-
+ )}
+ >
+ )}
+
+ )}
+ >
)}
diff --git a/app/components/materials/PreviewModal.tsx b/app/components/materials/PreviewModal.tsx
index 81f35fa..46070d7 100644
--- a/app/components/materials/PreviewModal.tsx
+++ b/app/components/materials/PreviewModal.tsx
@@ -6,47 +6,29 @@ import {
ModalContent,
ModalHeader,
} from '@heroui/react';
-import { Download, FileText, Flag, MessageSquare, Share2 } from 'lucide-react';
-import { useState } from 'react';
-import CommentsSection from './CommentsSection';
+import { Download, FileText, MessageSquare, Share2, Star } from 'lucide-react';
import RatingStars from './ratingStars';
import type { Material } from './types';
interface PreviewModalProps {
material: Material | null;
isOpen: boolean;
- userRating: number;
onClose: () => void;
onDownload: (materialId: string) => void;
onShare: (material: Material) => void;
- onReport: (material: Material) => void;
- onRate: (material: Material, rating: number) => void;
- onComment: (material: Material) => void;
- onRatingChange: (rating: number) => void;
- onAddComment: (materialId: string, content: string) => void;
+ onOpenRatingModal: (material: Material) => void;
}
export default function PreviewModal({
material,
isOpen,
- userRating,
onClose,
onDownload,
onShare,
- onReport,
- onRate,
- onComment,
- onRatingChange,
- onAddComment,
+ onOpenRatingModal,
}: PreviewModalProps) {
- const [showComments, setShowComments] = useState(false);
-
if (!material) return null;
- const handleAddComment = (content: string) => {
- onAddComment(material.id, content);
- };
-
return (
@@ -66,6 +48,17 @@ export default function PreviewModal({
+ {/* Tags del material */}
+ {material.tags && material.tags.length > 0 && (
+
+ {material.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ )}
+
{/* Información del archivo */}
@@ -120,25 +113,48 @@ export default function PreviewModal({
{/* Área de vista previa */}
-
-
-
- {material.fileType} • {material.title}
-
-
- La vista previa completa del documento se mostrará aquí. Por
- ahora, puedes descargar el archivo para verlo.
-
-
}
- onClick={() => onDownload(material.id)}
- type="button"
- >
- Descargar {material.fileType}
-
-
-
+ {material && (
+
+ {material.fileType === 'PDF' && (
+
+ {/* Viewer alternativo con Google Docs */}
+
+
+
+
+
+ Vista previa del documento. Descarga para ver en mejor
+ calidad.
+
+
+
+ )}
+ {material.fileType !== 'PDF' && (
+
+
+
+ {material.fileType} • {material.title}
+
+
+ Vista previa no disponible para este tipo de archivo.
+
+
}
+ onClick={() => onDownload(material.id)}
+ type="button"
+ >
+ Descargar {material.fileType}
+
+
+ )}
+
+ )}
{/* Descripción */}
Descripción
@@ -146,57 +162,17 @@ export default function PreviewModal({
{/* Valoración */}
+ {/* Botón para valorar y comentar */}
-
- Valorar este material
-
-
-
-
- {userRating > 0
- ? `Tu valoración: ${userRating} estrellas`
- : 'Selecciona tu valoración'}
-
-
-
-
-
- }
- onClick={() => setShowComments(!showComments)}
- type="button"
- >
- {showComments ? 'Ocultar comentarios' : 'Ver comentarios'}
-
- }
- onClick={() => onReport(material)}
- type="button"
- >
- Reportar
-
-
+
}
+ onClick={() => onOpenRatingModal(material)}
+ type="button"
+ >
+ Valorar y comentar este material
+
-
- {/* SECCIÓN: Comentarios */}
-
setShowComments(false)}
- />
diff --git a/app/components/materials/RatingAndCommentsModal.tsx b/app/components/materials/RatingAndCommentsModal.tsx
new file mode 100644
index 0000000..6ef9135
--- /dev/null
+++ b/app/components/materials/RatingAndCommentsModal.tsx
@@ -0,0 +1,198 @@
+import {
+ Button,
+ Card,
+ CardBody,
+ Modal,
+ ModalBody,
+ ModalContent,
+ ModalHeader,
+ Spinner,
+ Textarea,
+} from '@heroui/react';
+import { MessageCircle, Star } from 'lucide-react';
+import { useState } from 'react';
+import { useAuth } from '~/contexts/auth-context';
+import { useMaterialComments, useRateMaterial } from '~/lib/hooks/useMaterials';
+import type { Material, MaterialRating } from '~/lib/types/api.types';
+import type { Material as MaterialCardType } from './types';
+
+interface RatingAndCommentsModalProps {
+ material: (MaterialCardType & Partial) | null;
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+export default function RatingAndCommentsModal({
+ material,
+ isOpen,
+ onClose,
+}: RatingAndCommentsModalProps) {
+ const { user } = useAuth();
+ const [userRating, setUserRating] = useState(0);
+ const [newComment, setNewComment] = useState('');
+ const [hoveredRating, setHoveredRating] = useState(0);
+
+ const { data: comments = [], isLoading: isLoadingComments } =
+ useMaterialComments(material?.id || '');
+ const rateMaterial = useRateMaterial();
+
+ if (!material) return null;
+
+ const handleSubmitRating = async () => {
+ if (userRating > 0 && user?.id && newComment.trim()) {
+ try {
+ await rateMaterial.mutateAsync({
+ id: material.id,
+ rating: userRating,
+ userId: user.id,
+ comentario: newComment.trim(),
+ });
+ setUserRating(0);
+ setNewComment('');
+ } catch (error) {
+ console.error('Error al calificar:', error);
+ }
+ }
+ };
+
+ return (
+
+
+
+ {material.title}
+
+ Valoriza y comenta este material
+
+
+
+
+ {/* Sección de Valoración */}
+
+
+
+ Tu Valoración
+
+
+
+
+ {[1, 2, 3, 4, 5].map((star) => (
+
+ ))}
+
+ {userRating > 0 && (
+
+ {userRating} de 5 estrellas
+
+ )}
+
+
+ {/* Área de comentario obligatorio */}
+
+
+
+
+
+
+
+
+ {/* Sección de Comentarios Existentes */}
+
+
+
+ Comentarios ({comments?.length || 0})
+
+
+ {/* Lista de comentarios */}
+ {isLoadingComments ? (
+
+
+
+ ) : comments && comments.length > 0 ? (
+
+ {comments.map((comment: MaterialRating) => (
+
+
+
+
+
+ {comment.usuarioNombre || 'Usuario Anónimo'}
+
+
+ {new Date(comment.createdAt).toLocaleDateString(
+ 'es-ES',
+ )}
+
+
+
+ {[...Array(5)].map((_, i) => (
+
+ ))}
+
+
+ {comment.comentario && (
+
+ {comment.comentario}
+
+ )}
+
+
+ ))}
+
+ ) : (
+
+
+
No hay comentarios aún. Sé el primero en comentar.
+
+ )}
+
+
+
+
+ );
+}
diff --git a/app/components/materials/filtersPanel.tsx b/app/components/materials/filtersPanel.tsx
index 1d921e5..de8d652 100644
--- a/app/components/materials/filtersPanel.tsx
+++ b/app/components/materials/filtersPanel.tsx
@@ -1,50 +1,97 @@
-import { Card, CardBody, Select, SelectItem } from '@heroui/react';
-import { semesters, subjects } from './types';
+import { Button, Card, CardBody, Chip, Input } from '@heroui/react';
+import { X } from 'lucide-react';
+import { useState } from 'react';
interface FiltersPanelProps {
isOpen: boolean;
- selectedSubject: string;
- selectedSemester: string;
- onSubjectChange: (subject: string) => void;
- onSemesterChange: (semester: string) => void;
+ selectedTags: string[];
+ onTagsChange: (tags: string[]) => void;
}
export default function FiltersPanel({
isOpen,
- selectedSubject,
- selectedSemester,
- onSubjectChange,
- onSemesterChange,
+ selectedTags,
+ onTagsChange,
}: FiltersPanelProps) {
+ const [tagInput, setTagInput] = useState('');
+
+ const handleAddTag = () => {
+ if (tagInput.trim() && !selectedTags.includes(tagInput.trim())) {
+ onTagsChange([...selectedTags, tagInput.trim()]);
+ setTagInput('');
+ }
+ };
+
+ const handleRemoveTag = (tagToRemove: string) => {
+ onTagsChange(selectedTags.filter((tag) => tag !== tagToRemove));
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ handleAddTag();
+ }
+ };
+
if (!isOpen) return null;
return (
-
-
-
-
+
+
+
+
+
+
+
+
+
+ {selectedTags.length > 0 && (
+
+
+ Tags seleccionados:
+
+
+ {selectedTags.map((tag) => (
+ handleRemoveTag(tag)}
+ className="ml-2 hover:opacity-70"
+ >
+
+
+ }
+ variant="flat"
+ color="primary"
+ >
+ {tag}
+
+ ))}
+
+
+ )}
diff --git a/app/components/materials/materialCard.tsx b/app/components/materials/materialCard.tsx
index 14f6e36..788319a 100644
--- a/app/components/materials/materialCard.tsx
+++ b/app/components/materials/materialCard.tsx
@@ -8,8 +8,7 @@ interface MaterialCardProps {
viewMode: 'grid' | 'list';
onPreview: (material: Material) => void;
onDownload: (materialId: string) => void;
- onRate: (material: Material) => void;
- onComment: (material: Material) => void;
+ onInteract: (material: Material) => void;
}
export default function MaterialCard({
@@ -17,8 +16,7 @@ export default function MaterialCard({
viewMode,
onPreview,
onDownload,
- onRate,
- onComment,
+ onInteract,
}: MaterialCardProps) {
if (viewMode === 'grid') {
return (
@@ -39,19 +37,29 @@ export default function MaterialCard({
{material.title}
-
+
{material.author}
-
- {material.subject} • {material.semester}
-
{material.date}
-
+ {material.tags && material.tags.length > 0 && (
+
+ {material.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ )}
{material.rating}
- ({material.ratingsCount})
@@ -85,26 +93,15 @@ export default function MaterialCard({
-
- }
- onClick={() => onRate(material)}
- type="button"
- >
- Valorar
-
- }
- onClick={() => onComment(material)}
- type="button"
- >
- Comentar
-
-
+
}
+ onClick={() => onInteract(material)}
+ type="button"
+ >
+ Valorar y comentar
+
);
@@ -123,21 +120,32 @@ export default function MaterialCard({
{material.title}
-
+
{material.author}
•
- {material.subject}
- •
- {material.semester}
- •
{material.date}
+ {material.tags && material.tags.length > 0 && (
+
+ {material.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ )}
+
{material.rating}
- ({material.ratingsCount})
@@ -151,19 +159,12 @@ export default function MaterialCard({
-
}
- onClick={() => onRate(material)}
- >
- Valorar
-
}
- onClick={() => onComment(material)}
+ onClick={() => onInteract(material)}
>
- Comentar
+ Valorar y comentar