File "LatestNotices.tsx"
Full Path: /home/trinadezambia/public_html/student_panel/src/components/ui/pages/dashboard/LatestNotices.tsx
File size: 9.31 KB
MIME-type: text/x-java
Charset: utf-8
'use client';
import { useState } from 'react';
import {
Dialog,
DialogClose,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { BiX } from 'react-icons/bi';
import { Download, ExternalLink, Loader2 } from 'lucide-react';
import { useGetAnnouncements } from '@/lib/api/student/queryHooks';
import type { Announcement } from '@/lib/api/student/functions';
import { useTranslate } from '@/components/hooks/useTranslate';
export default function LatestNotices() {
const translate = useTranslate();
// State to manage which notice modal is open
const [selectedNotice, setSelectedNotice] = useState<Announcement | null>(
null
);
// Fetch announcements from API with type 'class'
// This will automatically handle loading states, caching, and error handling
const { data, isLoading, error } = useGetAnnouncements({ type: 'class' });
// Handler for opening modal with selected announcement
const handleViewDetails = (announcement: Announcement) => {
setSelectedNotice(announcement);
};
// Handler for closing modal
const handleCloseModal = () => {
setSelectedNotice(null);
};
// Handler for downloading attachment files or opening links
const handleDownloadAttachment = (fileUrl: string) => {
// Open the file or link in a new tab
window.open(fileUrl, '_blank');
};
// Helper function to check if attachment is a link (YouTube, website, etc.)
// Links have type "4" or type_detail "Other Link"
const isLink = (file: { type: string; type_detail: string }) => {
return file.type === '4' || file.type_detail === 'Other Link';
};
// Get announcements array from API response
const announcements = data?.data?.data || [];
return (
<div className="bg-white rounded-[12px] border border-gray-200">
<h2 className="text-lg sm:text-xl font-medium text-gray-800 border-b border-gray-200 p-3 sm:p-4 md:p-6 leading-relaxed">
{translate('latestNotices')}
</h2>
{/* Loading State - Show spinner while fetching data */}
{isLoading && (
<div className="flex items-center justify-center py-8">
<Loader2 className="w-6 h-6 animate-spin text-[var(--primary-color)]" />
</div>
)}
{/* Error State - Show error message if API fails */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 m-3 sm:m-4 md:m-6">
<p className="text-red-600 text-sm">
{translate('failedToLoadAnnouncements')}
</p>
</div>
)}
{/* Empty State - Show message when no announcements are available */}
{!isLoading && !error && announcements.length === 0 && (
<div className="bg-gray-50 border border-gray-200 rounded-lg p-6 m-3 sm:m-4 md:m-6 text-center">
<p className="text-gray-600 text-sm">
{translate('noAnnouncementsAvailable')}
</p>
</div>
)}
{/* Announcements List - Show when data is loaded successfully */}
{!isLoading && !error && announcements.length > 0 && (
<div className="space-y-3 sm:space-y-4 p-3 sm:p-4 md:p-6">
{announcements.slice(0, 6).map((announcement) => (
<div
key={announcement.id}
className="bg-[var(--light-primary-color)] p-3 sm:p-4 rounded-[12px] border border-gray-200"
>
<div className="flex flex-wrap flex-col lg:flex-row lg:justify-between lg:items-center gap-3 lg:gap-4">
<div className="flex-1">
<h3 className="text-sm sm:text-base font-medium text-gray-800 mb-1 leading-relaxed">
{announcement.title}
</h3>
<p className="text-xs sm:text-sm font-normal text-gray-600 leading-relaxed line-clamp-1 sm:line-clamp-1">
{announcement.description}
</p>
</div>
<button
onClick={() => handleViewDetails(announcement)}
className="bg-(--primary-color) leading-relaxed text-white px-3 sm:px-4 py-2 sm:py-2 rounded-[8px] text-xs sm:text-sm font-normal hover:bg-(--primary-color)/80 transition-colors whitespace-nowrap w-full sm:w-auto"
>
{translate('viewDetails')}
</button>
</div>
</div>
))}
</div>
)}
{/* Modal for viewing notice details */}
<Dialog open={selectedNotice !== null} onOpenChange={handleCloseModal}>
<DialogContent
className="sm:max-w-[600px] max-w-[95vw] max-h-[90vh] p-0 overflow-hidden"
showCloseButton={false}
>
{selectedNotice && (
<>
{/* Modal Header - Mobile responsive with better spacing */}
<DialogHeader className="flex flex-row items-start justify-between border-b p-3 sm:p-6">
<DialogTitle className="text-sm sm:text-base text-left font-bold text-gray-900 pr-2 sm:pr-4 leading-tight flex-1">
{selectedNotice.title}
</DialogTitle>
<DialogClose
onClick={handleCloseModal}
className="flex-shrink-0"
>
<BiX className="w-5 h-5 sm:w-8 sm:h-8 cursor-pointer text-gray-400 bg-[var(--light-primary-color)] border border-gray-200 rounded-[4px] p-1" />
</DialogClose>
</DialogHeader>
{/* Modal Content - Mobile responsive with proper scrolling */}
<div className="p-3 sm:p-6 !pt-0 space-y-3 sm:space-y-4 overflow-y-auto max-h-[calc(90vh-80px)]">
{/* Date Tag - Mobile responsive sizing */}
<div className="flex items-start">
<span className="bg-(--primary-color) text-white text-sm sm:text-base font-normal px-2 py-1 sm:px-3 sm:py-2 rounded-[4px]">
{selectedNotice.created_at}
</span>
</div>
{/* File Attachments (if exist) - Mobile responsive layout */}
{selectedNotice.file && selectedNotice.file.length > 0 && (
<div className="space-y-2">
{selectedNotice.file.map((file) => {
// Check if this is a link (YouTube, website, etc.) or a file
const isLinkAttachment = isLink(file);
return (
<div
key={file.id}
className="bg-[var(--light-primary-color)] rounded-lg p-2 sm:p-3 flex items-center justify-between border border-gray-200 gap-2"
>
<div className="flex-1 min-w-0 max-w-[200px] sm:max-w-[300px]">
<span className="text-gray-900 text-xs sm:text-sm font-medium block break-all">
{isLinkAttachment
? file.file_url
: file.file_name || translate('attachment')}
</span>
<div className="flex items-center gap-2 mt-0.5">
{/* Show file extension for files, or "Link" badge for links */}
{isLinkAttachment ? (
<span className="text-[var(--primary-color)] text-xs font-medium">
{file.type_detail || translate('link')}
</span>
) : (
file.file_extension && (
<span className="text-gray-500 text-xs uppercase">
{file.file_extension}
</span>
)
)}
</div>
</div>
{/* Show link icon for links, download icon for files */}
<button
onClick={() =>
handleDownloadAttachment(file.file_url)
}
className="bg-(--primary-color) text-white p-1.5 sm:p-2 rounded hover:bg-(--primary-color)/90 transition-colors flex-shrink-0"
title={
isLinkAttachment
? translate('openLink')
: translate('downloadFile')
}
>
{isLinkAttachment ? (
<ExternalLink className="w-3 h-3 sm:w-4 sm:h-4" />
) : (
<Download className="w-3 h-3 sm:w-4 sm:h-4" />
)}
</button>
</div>
);
})}
</div>
)}
{/* Description - Mobile responsive text sizing */}
<div className="bg-[var(--light-primary-color)] rounded-lg p-3 sm:p-4">
<p className="text-[var(--primary-color)] text-sm sm:text-base font-normal leading-relaxed">
{selectedNotice.description}
</p>
</div>
</div>
</>
)}
</DialogContent>
</Dialog>
</div>
);
}