diff --git a/.env b/.env index 0855a36..a9475f5 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ # VITE_API_BASE_URL=https://api.yimaru.yaltopia.com/api/v1 -VITE_API_BASE_URL=http://localhost:8432/api/v1 +VITE_API_BASE_URL= https://api.yimaruacademy.com/api/v1 VITE_GOOGLE_CLIENT_ID= diff --git a/src/app/AppRoutes.tsx b/src/app/AppRoutes.tsx index 09cf9a8..1b396d9 100644 --- a/src/app/AppRoutes.tsx +++ b/src/app/AppRoutes.tsx @@ -17,6 +17,7 @@ import { AddVideoPage } from "../pages/content-management/AddVideoPage" import { AddPracticePage } from "../pages/content-management/AddPracticePage" import { NotFoundPage } from "../pages/NotFoundPage" import { NotificationsPage } from "../pages/notifications/NotificationsPage" +import { CreateNotificationPage } from "../pages/notifications/CreateNotificationPage" import { UserDetailPage } from "../pages/user-management/UserDetailPage" import { UserManagementLayout } from "../pages/user-management/UserManagementLayout" import { UsersListPage } from "../pages/user-management/UsersListPage" @@ -85,6 +86,7 @@ export function AppRoutes() { } /> + } /> } /> } /> } /> diff --git a/src/components/topbar/NotificationDropdown.tsx b/src/components/topbar/NotificationDropdown.tsx index 7b338eb..b0b8110 100644 --- a/src/components/topbar/NotificationDropdown.tsx +++ b/src/components/topbar/NotificationDropdown.tsx @@ -76,7 +76,6 @@ function NotificationItem({ type="button" className={cn( "group relative flex w-full gap-3 rounded-lg px-3 py-3 text-left transition-colors hover:bg-grayScale-100", - !notification.is_read && "bg-brand-100/30" )} onClick={() => { if (!notification.is_read) onMarkRead(notification.id) @@ -84,13 +83,13 @@ function NotificationItem({ > {/* Unread dot */} {!notification.is_read && ( - + )} {/* Type icon */} @@ -101,16 +100,16 @@ function NotificationItem({

- {getNotificationTitle(notification)} + {getNotificationTitle(notification) || "Notification"}

-

- {getNotificationMessage(notification)} +

+ {getNotificationMessage(notification) || "No preview text available."}

-

+

{formatTimestamp(notification.timestamp)}

@@ -170,14 +169,11 @@ export function NotificationDropdown() { {/* Bell button */} diff --git a/src/pages/issues/IssuesPage.tsx b/src/pages/issues/IssuesPage.tsx index 76c74f7..7dea494 100644 --- a/src/pages/issues/IssuesPage.tsx +++ b/src/pages/issues/IssuesPage.tsx @@ -115,7 +115,7 @@ function getIssueTypeConfig(type: string): { case "course": return { label: "Course", - classes: "bg-brand-50 text-brand-700 border-brand-200", + classes: "bg-brand-500 text-white border-brand-500", icon: BookOpen, }; case "account": diff --git a/src/pages/notifications/CreateNotificationPage.tsx b/src/pages/notifications/CreateNotificationPage.tsx new file mode 100644 index 0000000..1206264 --- /dev/null +++ b/src/pages/notifications/CreateNotificationPage.tsx @@ -0,0 +1,422 @@ +import { useEffect, useMemo, useState } from "react" +import { useNavigate } from "react-router-dom" +import { Bell, Loader2, Mail, MailOpen, Megaphone } from "lucide-react" +import { Card, CardContent } from "../../components/ui/card" +import { Button } from "../../components/ui/button" +import { Input } from "../../components/ui/input" +import { Textarea } from "../../components/ui/textarea" +import { FileUpload } from "../../components/ui/file-upload" +import { cn } from "../../lib/utils" +import { getTeamMembers } from "../../api/team.api" +import type { TeamMember } from "../../types/team.types" + +export function CreateNotificationPage() { + const navigate = useNavigate() + + const [composeChannels, setComposeChannels] = useState>(["push"]) + const [composeAudience, setComposeAudience] = useState<"all" | "selected">("all") + const [teamRecipients, setTeamRecipients] = useState([]) + const [recipientsLoading, setRecipientsLoading] = useState(false) + const [selectedRecipientIds, setSelectedRecipientIds] = useState([]) + const [composeTitle, setComposeTitle] = useState("") + const [composeMessage, setComposeMessage] = useState("") + const [sending, setSending] = useState(false) + const [, setComposeImage] = useState(null) + + useEffect(() => { + setRecipientsLoading(true) + getTeamMembers(1, 50) + .then((res) => { + setTeamRecipients(res.data.data ?? []) + }) + .catch(() => { + setTeamRecipients([]) + }) + .finally(() => { + setRecipientsLoading(false) + }) + }, []) + + const selectedRecipients = useMemo( + () => teamRecipients.filter((m) => selectedRecipientIds.includes(m.id)), + [teamRecipients, selectedRecipientIds], + ) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!composeTitle.trim() || !composeMessage.trim()) return + if (composeChannels.length === 0) return + setSending(true) + try { + // Hook up to backend send API here when available. + await new Promise((resolve) => setTimeout(resolve, 400)) + setComposeTitle("") + setComposeMessage("") + setComposeAudience("all") + setComposeChannels(["push"]) + setSelectedRecipientIds([]) + setComposeImage(null) + navigate("/notifications") + } finally { + setSending(false) + } + } + + return ( +
+ {/* Breadcrumb + Header */} +
+ + +
+
+

+ Notifications +

+

+ Create notification + + + Composer + +

+

+ Send a one-off push or SMS notification to your users. +

+
+ +
+
+ +
+ {/* Left: message setup */} +
+ + + {/* Channel & audience */} +
+
+

+ Channel +

+
+ + +
+
+ +
+

+ Audience +

+
+ + +
+
+
+ + {/* Title & message */} +
+
+ + setComposeTitle(e.target.value)} + /> +
+
+ +