@@ -92,8 +98,22 @@ export function Header({ activeTab, wsStatus, voiceStatus }: HeaderProps) {
- {/* Right: status badges */}
+ {/* Right: status badges + theme toggle */}
+ {/* Theme toggle */}
+
+
{/* WS Badge */}
=
- [
- { id: "messages", label: "Messages & Moderation", icon: MessageSquare },
- { id: "live", label: "Voice & Media", icon: Radio },
- { id: "dashboard", label: "Dashboard", icon: LayoutDashboard },
- ];
+const navItems: Array<{
+ id: DashboardTab;
+ label: string;
+ icon: typeof Radio;
+}> = [
+ { id: "messages", label: "Messages & Moderation", icon: MessageSquare },
+ { id: "live", label: "Voice & Media", icon: Radio },
+ { id: "dashboard", label: "Dashboard", icon: LayoutDashboard },
+ { id: "settings" as const, label: "Admin", icon: Settings },
+];
interface SidebarProps {
activeTab: DashboardTab;
@@ -21,6 +31,7 @@ interface SidebarProps {
recentMessages?: MessageRecord[];
guildId?: string;
channelId?: string;
+ notificationCount?: number;
}
export function Sidebar({
@@ -30,6 +41,7 @@ export function Sidebar({
recentMessages = [],
guildId,
channelId,
+ notificationCount = 0,
}: SidebarProps) {
const mascotChat = useMascotChat({
messageCount: recentMessages.length,
@@ -46,7 +58,7 @@ export function Sidebar({
{
+ const target = e.currentTarget;
+ target.style.display = "none";
+ }}
/>
{/* Mascot image — only when expanded */}
@@ -70,6 +86,10 @@ export function Sidebar({
src="https://raw.githubusercontent.com/IMPHNEN/imphnen-frontend-service/develop/apps/dimentorin/public/image/mascot-1.png"
alt="Mascot"
className="mt-4 h-auto w-[140px] object-contain drop-shadow-md"
+ onError={(e) => {
+ const target = e.currentTarget;
+ target.style.display = "none";
+ }}
/>
)}
@@ -95,6 +115,21 @@ export function Sidebar({
>
{!collapsed &&
{item.label}}
+ {!collapsed && item.id === "messages" &&
+ notificationCount !== undefined &&
+ notificationCount > 0 && (
+
+ {notificationCount > 99 ? "99+" : notificationCount}
+
+ )}
+ {/* Collapsed badge — top-right dot */}
+ {collapsed && item.id === "messages" &&
+ notificationCount !== undefined &&
+ notificationCount > 0 && (
+
+ {notificationCount > 9 ? "N" : notificationCount}
+
+ )}
);
})}
@@ -118,7 +153,7 @@ export function Sidebar({
onClose={() => mascotChat.setIsOpen(false)}
onSendMessage={mascotChat.handleSendMessage}
mascotName="IMPHNEN Mascot"
- className="fixed bottom-[170px] left-[80px] z-[9999]"
+ className="fixed bottom-[170px] left-[80px] z-[9999] md:bottom-4 md:left-4 md:right-auto"
/>
>
);
diff --git a/services/frontend/src/widgets/mascot/MascotChatbot.tsx b/services/frontend/src/widgets/mascot/MascotChatbot.tsx
index 26c22ca..6fe1faa 100644
--- a/services/frontend/src/widgets/mascot/MascotChatbot.tsx
+++ b/services/frontend/src/widgets/mascot/MascotChatbot.tsx
@@ -117,14 +117,14 @@ export function MascotChatbot({
)}
>
{/* Header */}
-
+
-
+
{mascotName}
-
+
{loading ? "Mengetik..." : "Online"}
@@ -134,7 +134,7 @@ export function MascotChatbot({
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
onClick={() => setIsMinimized(!isMinimized)}
- className="p-1.5 hover:bg-white/20 rounded-lg transition-colors"
+ className="p-1.5 hover:bg-primary-foreground/20 rounded-lg transition-colors"
title={isMinimized ? "Maximize" : "Minimize"}
>
{isMinimized ? (
@@ -149,7 +149,7 @@ export function MascotChatbot({
onClick={() => {
onClose?.();
}}
- className="p-1.5 hover:bg-white/20 rounded-lg transition-colors"
+ className="p-1.5 hover:bg-primary-foreground/20 rounded-lg transition-colors"
title="Close"
>
@@ -176,6 +176,10 @@ export function MascotChatbot({
src={mascotAvatar}
alt={mascotName}
className="w-6 h-6 rounded-full object-cover"
+ onError={(e) => {
+ const target = e.currentTarget;
+ target.style.display = "none";
+ }}
/>
)}
{
+ const target = e.currentTarget;
+ target.style.display = "none";
+ }}
/>
diff --git a/services/frontend/src/widgets/mascot/MascotImage.tsx b/services/frontend/src/widgets/mascot/MascotImage.tsx
index af53a3a..2f1eed4 100644
--- a/services/frontend/src/widgets/mascot/MascotImage.tsx
+++ b/services/frontend/src/widgets/mascot/MascotImage.tsx
@@ -38,6 +38,7 @@ export function MascotImage({
const sizeClass = sizeMap[size];
const chatSizeClass = chatSizeMap[size];
const [isVisible, setIsVisible] = useState(false);
+ const [imgError, setImgError] = useState(false);
useEffect(() => {
if (showChat && chatMessage) {
@@ -51,13 +52,20 @@ export function MascotImage({
return (
-
+ {imgError ? (
+
+
+
+ ) : (
+
setImgError(true)}
+ />
+ )}
{/* Floating Chat Bubble */}
{isVisible && chatMessage && (
@@ -72,7 +80,7 @@ export function MascotImage({
{/* Chat bubble */}
-
+
{chatMessage}
diff --git a/services/frontend/src/widgets/particles/ParticleBackground.tsx b/services/frontend/src/widgets/particles/ParticleBackground.tsx
index c6743c0..21f6638 100644
--- a/services/frontend/src/widgets/particles/ParticleBackground.tsx
+++ b/services/frontend/src/widgets/particles/ParticleBackground.tsx
@@ -20,11 +20,16 @@ export function ParticleBackground() {
style={{ zIndex: -1 }}
>
{/* Top-right glow orb */}
-
+
{/* Bottom-left glow orb */}
);
diff --git a/services/frontend/tailwind.config.js b/services/frontend/tailwind.config.js
index 44977b3..1785d4a 100644
--- a/services/frontend/tailwind.config.js
+++ b/services/frontend/tailwind.config.js
@@ -1,5 +1,6 @@
/** @type {import('tailwindcss').Config} */
export default {
+ darkMode: "class",
content: ["./index.html", "./src/**/*.{ts,tsx}"],
theme: {
extend: {
diff --git a/services/frontend/tsconfig.json b/services/frontend/tsconfig.json
index deefd85..9cd8dd5 100644
--- a/services/frontend/tsconfig.json
+++ b/services/frontend/tsconfig.json
@@ -6,11 +6,15 @@
"strict": true,
"jsx": "react-jsx",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
- "types": ["vite/client"],
+ "types": ["astro/client"],
"noEmit": true,
"skipLibCheck": true,
"esModuleInterop": true,
- "allowSyntheticDefaultImports": true
+ "allowSyntheticDefaultImports": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["src/*"]
+ }
},
- "include": ["src/**/*", "vite.config.ts"]
+ "include": ["src/**/*", "astro.config.mjs"]
}
diff --git a/services/frontend/vite.config.ts b/services/frontend/vite.config.ts
deleted file mode 100644
index 0699450..0000000
--- a/services/frontend/vite.config.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import react from "@vitejs/plugin-react";
-import { defineConfig } from "vite";
-
-export default defineConfig({
- plugins: [react()],
- build: {
- rolldownOptions: {
- checks: {
- pluginTimings: false,
- },
- },
- },
- server: {
- middlewareMode: false,
- },
- preview: {
- port: 3000,
- host: true,
- allowedHosts: [
- "imphnen.asepharyana.my.id",
- "imphnen.asepharyana.tech",
- "imphnen.asepharyana.web.id",
- ],
- },
-});