observatory

This commit is contained in:
alma 2025-05-04 21:27:22 +02:00
parent 495706d7a9
commit e9142b28de
6 changed files with 692 additions and 0 deletions

27
app/announcement/page.tsx Normal file
View File

@ -0,0 +1,27 @@
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { redirect } from "next/navigation";
import { AnnouncementsPage } from "@/components/announcement/announcements-page";
export const metadata = {
title: "Announcements",
};
export default async function AnnouncementPage() {
const session = await getServerSession(authOptions);
if (!session) {
redirect("/signin");
}
// Get user role(s)
const userRole = session.user.role || [];
return (
<div className='min-h-screen bg-white'>
<div className='container mx-auto py-10'>
<AnnouncementsPage userRole={userRole} />
</div>
</div>
);
}

10
app/types/announcement.ts Normal file
View File

@ -0,0 +1,10 @@
export interface Announcement {
id: string;
title: string;
content: string;
createdAt: string;
updatedAt: string;
author: string;
authorId: string;
targetRoles: string[];
}

View File

@ -0,0 +1,218 @@
"use client";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle
} from "@/components/ui/card";
import { CheckIcon, Loader2 } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Announcement } from "@/app/types/announcement";
// Form schema
const formSchema = z.object({
title: z.string().min(5, { message: "Title must be at least 5 characters" }),
content: z.string().min(10, { message: "Content must be at least 10 characters" }),
targetRoles: z.array(z.string()).min(1, { message: "Select at least one target role" }),
});
interface AnnouncementFormProps {
userRole: string[];
}
export function AnnouncementForm({ userRole }: AnnouncementFormProps) {
const [selectedRoles, setSelectedRoles] = useState<string[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
// Initialize form
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
title: "",
content: "",
targetRoles: [],
},
});
// Available roles for selection
const availableRoles = [
{ id: "all", name: "All Users" },
{ id: "admin", name: "Administrators" },
{ id: "entrepreneurship", name: "Entrepreneurship" },
{ id: "communication", name: "Communication" },
{ id: "expression", name: "Expression" },
{ id: "coding", name: "Coding" },
{ id: "dataintelligence", name: "Data Intelligence" },
{ id: "mediation", name: "Mediation" },
];
// Handle role selection
const handleRoleToggle = (roleId: string) => {
if (roleId === "all") {
// If "all" is selected, clear other selections
setSelectedRoles(["all"]);
form.setValue("targetRoles", ["all"]);
} else {
// Remove "all" if it was previously selected
const newSelection = selectedRoles.filter(id => id !== "all");
// Toggle the selected role
if (newSelection.includes(roleId)) {
const updatedSelection = newSelection.filter(id => id !== roleId);
setSelectedRoles(updatedSelection);
form.setValue("targetRoles", updatedSelection);
} else {
const updatedSelection = [...newSelection, roleId];
setSelectedRoles(updatedSelection);
form.setValue("targetRoles", updatedSelection);
}
}
};
// Form submission
const onSubmit = async (data: z.infer<typeof formSchema>) => {
setIsSubmitting(true);
try {
// In a real implementation, this would be an API call
console.log("Announcement data:", data);
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 1000));
// Reset form and show success message
form.reset();
setSelectedRoles([]);
setIsSuccess(true);
// Hide success message after a delay
setTimeout(() => setIsSuccess(false), 3000);
} catch (error) {
console.error("Error submitting announcement:", error);
} finally {
setIsSubmitting(false);
}
};
return (
<Card>
<CardHeader>
<CardTitle>Create New Announcement</CardTitle>
<CardDescription>
Create an announcement to be displayed to specific user roles
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input placeholder="Enter announcement title" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormLabel>Content</FormLabel>
<FormControl>
<Textarea
placeholder="Enter announcement content"
rows={5}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="targetRoles"
render={() => (
<FormItem>
<FormLabel>Target Audience</FormLabel>
<FormControl>
<div className="flex flex-wrap gap-2">
{availableRoles.map(role => (
<Badge
key={role.id}
variant={selectedRoles.includes(role.id) ? "default" : "outline"}
className={`cursor-pointer ${
selectedRoles.includes(role.id)
? "bg-blue-600 hover:bg-blue-700"
: "hover:bg-gray-100"
}`}
onClick={() => handleRoleToggle(role.id)}
>
{role.name}
{selectedRoles.includes(role.id) && (
<CheckIcon className="ml-1 h-3 w-3" />
)}
</Badge>
))}
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={isSubmitting || isSuccess} className="w-full">
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Submitting...
</>
) : isSuccess ? (
<>
<CheckIcon className="mr-2 h-4 w-4" />
Announcement Created!
</>
) : (
"Create Announcement"
)}
</Button>
</form>
</Form>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,109 @@
"use client";
import { useState, useEffect } from "react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "@/components/ui/select";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Announcement } from "@/app/types/announcement";
// Mock data for demo purposes
const mockAnnouncements: Announcement[] = [
{
id: "1",
title: "System Maintenance",
content: "The system will be undergoing maintenance on Saturday from 2-4am.",
createdAt: "2023-06-01T10:00:00Z",
updatedAt: "2023-06-01T10:00:00Z",
author: "System Admin",
authorId: "admin1",
targetRoles: ["all"]
},
{
id: "2",
title: "New Feature Launch",
content: "We're excited to announce our new collaborative workspace feature launching next week!",
createdAt: "2023-06-02T14:30:00Z",
updatedAt: "2023-06-02T14:30:00Z",
author: "Product Team",
authorId: "product1",
targetRoles: ["admin", "entrepreneurship"]
}
];
export function AnnouncementsDropdown() {
const [announcements, setAnnouncements] = useState<Announcement[]>([]);
const [selectedAnnouncement, setSelectedAnnouncement] = useState<Announcement | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// In a real implementation, this would be an API call
// For now, using mock data
setAnnouncements(mockAnnouncements);
if (mockAnnouncements.length > 0) {
setSelectedAnnouncement(mockAnnouncements[0]);
}
setLoading(false);
}, []);
const handleAnnouncementChange = (announcementId: string) => {
const announcement = announcements.find(a => a.id === announcementId);
if (announcement) {
setSelectedAnnouncement(announcement);
}
};
return (
<div className="flex flex-col space-y-4">
{loading ? (
<div className="flex items-center justify-center h-40">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
</div>
) : announcements.length === 0 ? (
<div className="text-center py-10 text-gray-500">
No announcements available
</div>
) : (
<>
<div className="w-full max-w-md">
<Select
onValueChange={handleAnnouncementChange}
defaultValue={selectedAnnouncement?.id}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an announcement" />
</SelectTrigger>
<SelectContent>
{announcements.map(announcement => (
<SelectItem key={announcement.id} value={announcement.id}>
{announcement.title}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{selectedAnnouncement && (
<Card className="w-full">
<CardHeader>
<CardTitle>{selectedAnnouncement.title}</CardTitle>
<div className="text-sm text-gray-500">
Posted by {selectedAnnouncement.author} on {new Date(selectedAnnouncement.createdAt).toLocaleDateString()}
</div>
</CardHeader>
<CardContent>
<div className="prose">
{selectedAnnouncement.content}
</div>
</CardContent>
</Card>
)}
</>
)}
</div>
);
}

View File

@ -0,0 +1,264 @@
"use client";
import { useState, useEffect } from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Pencil,
Trash2,
Eye,
Search,
Plus,
Filter,
AlertTriangle
} from "lucide-react";
import { Input } from "@/components/ui/input";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter,
DialogTrigger
} from "@/components/ui/dialog";
import { Announcement } from "@/app/types/announcement";
// Mock data for demo purposes
const mockAnnouncements: Announcement[] = [
{
id: "1",
title: "System Maintenance",
content: "The system will be undergoing maintenance on Saturday from 2-4am.",
createdAt: "2023-06-01T10:00:00Z",
updatedAt: "2023-06-01T10:00:00Z",
author: "System Admin",
authorId: "admin1",
targetRoles: ["all"]
},
{
id: "2",
title: "New Feature Launch",
content: "We're excited to announce our new collaborative workspace feature launching next week!",
createdAt: "2023-06-02T14:30:00Z",
updatedAt: "2023-06-02T14:30:00Z",
author: "Product Team",
authorId: "product1",
targetRoles: ["admin", "entrepreneurship"]
},
{
id: "3",
title: "Team Meeting",
content: "There will be a team meeting on Monday at 10 AM to discuss the upcoming project milestones.",
createdAt: "2023-06-03T09:15:00Z",
updatedAt: "2023-06-03T09:15:00Z",
author: "Team Lead",
authorId: "lead1",
targetRoles: ["communication", "admin"]
}
];
interface AnnouncementsListProps {
userRole: string[];
}
export function AnnouncementsList({ userRole }: AnnouncementsListProps) {
const [announcements, setAnnouncements] = useState<Announcement[]>([]);
const [searchTerm, setSearchTerm] = useState("");
const [selectedAnnouncement, setSelectedAnnouncement] = useState<Announcement | null>(null);
const [isViewDialogOpen, setIsViewDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
// In a real implementation, this would be an API call
// For now, using mock data
setAnnouncements(mockAnnouncements);
setLoading(false);
}, []);
// Filter announcements based on search term
const filteredAnnouncements = announcements.filter(
announcement =>
announcement.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
announcement.content.toLowerCase().includes(searchTerm.toLowerCase())
);
// Handle viewing an announcement
const handleViewAnnouncement = (announcement: Announcement) => {
setSelectedAnnouncement(announcement);
setIsViewDialogOpen(true);
};
// Handle deleting an announcement
const handleDeleteClick = (announcement: Announcement) => {
setSelectedAnnouncement(announcement);
setIsDeleteDialogOpen(true);
};
const confirmDelete = () => {
if (selectedAnnouncement) {
// In a real implementation, this would be an API call
setAnnouncements(announcements.filter(a => a.id !== selectedAnnouncement.id));
setIsDeleteDialogOpen(false);
}
};
// Format roles for display
const formatRoles = (roles: string[]) => {
return roles.map(role => {
const roleName = role === "all"
? "All Users"
: role.charAt(0).toUpperCase() + role.slice(1);
return (
<Badge key={role} variant="outline" className="mr-1">
{roleName}
</Badge>
);
});
};
return (
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<div>
<CardTitle>All Announcements</CardTitle>
<CardDescription>
Manage announcements for different user roles
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
{/* Search and filter */}
<div className="mb-4 flex items-center space-x-2">
<div className="relative flex-1">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500" />
<Input
placeholder="Search announcements..."
className="pl-8"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
{/* Announcements table */}
{loading ? (
<div className="flex items-center justify-center h-40">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
</div>
) : filteredAnnouncements.length === 0 ? (
<div className="text-center py-10 text-gray-500">
No announcements found
</div>
) : (
<div className="border rounded-md">
<Table>
<TableHeader>
<TableRow>
<TableHead>Title</TableHead>
<TableHead>Created</TableHead>
<TableHead>Author</TableHead>
<TableHead>Target Roles</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredAnnouncements.map((announcement) => (
<TableRow key={announcement.id}>
<TableCell className="font-medium">{announcement.title}</TableCell>
<TableCell>{new Date(announcement.createdAt).toLocaleDateString()}</TableCell>
<TableCell>{announcement.author}</TableCell>
<TableCell>{formatRoles(announcement.targetRoles)}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => handleViewAnnouncement(announcement)}
>
<Eye className="h-4 w-4" />
<span className="sr-only">View</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDeleteClick(announcement)}
>
<Trash2 className="h-4 w-4" />
<span className="sr-only">Delete</span>
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
{/* View Announcement Dialog */}
<Dialog open={isViewDialogOpen} onOpenChange={setIsViewDialogOpen}>
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<DialogTitle>{selectedAnnouncement?.title}</DialogTitle>
<DialogDescription>
Posted by {selectedAnnouncement?.author} on {selectedAnnouncement && new Date(selectedAnnouncement.createdAt).toLocaleDateString()}
</DialogDescription>
</DialogHeader>
<div className="mt-4">
<div className="mb-2">
<span className="text-sm text-gray-500">Target Audience:</span>{" "}
{selectedAnnouncement && formatRoles(selectedAnnouncement.targetRoles)}
</div>
<p className="text-sm leading-6 text-gray-700">
{selectedAnnouncement?.content}
</p>
</div>
</DialogContent>
</Dialog>
{/* Delete Confirmation Dialog */}
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<AlertTriangle className="text-red-500 h-5 w-5" />
Confirm Deletion
</DialogTitle>
<DialogDescription>
Are you sure you want to delete the announcement "{selectedAnnouncement?.title}"? This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-4">
<Button variant="outline" onClick={() => setIsDeleteDialogOpen(false)}>
Cancel
</Button>
<Button variant="destructive" onClick={confirmDelete}>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,64 @@
"use client";
import { useState, useEffect } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { AnnouncementForm } from "./announcement-form";
import { AnnouncementsList } from "./announcements-list";
import { AnnouncementsDropdown } from "./announcements-dropdown";
interface AnnouncementsPageProps {
userRole: string | string[];
}
export function AnnouncementsPage({ userRole = [] }: AnnouncementsPageProps) {
const roles = Array.isArray(userRole) ? userRole : [userRole];
// Check if user has admin, entrepreneurship, or communication role
const hasAdminAccess = roles.some(role =>
["admin", "entrepreneurship", "communication"].includes(role)
);
if (!hasAdminAccess) {
// For regular users, just show the dropdown
return (
<div className="flex flex-col">
<h1 className="text-2xl font-bold mb-6">Announcements</h1>
<AnnouncementsDropdown />
</div>
);
}
// For users with admin access, show the full UI with tabs
return (
<div className="flex flex-col">
<h1 className="text-2xl font-bold mb-6">Announcements Management</h1>
<Tabs defaultValue="list">
<div className="flex justify-between items-center mb-8">
<TabsList className="bg-black/20 border-0">
<TabsTrigger
value="list"
className="data-[state=active]:bg-blue-600 data-[state=active]:text-white text-gray-400"
>
All Announcements
</TabsTrigger>
<TabsTrigger
value="create"
className="data-[state=active]:bg-blue-600 data-[state=active]:text-white text-gray-400"
>
Create Announcement
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="list">
<AnnouncementsList userRole={roles} />
</TabsContent>
<TabsContent value="create">
<AnnouncementForm userRole={roles} />
</TabsContent>
</Tabs>
</div>
);
}