diff --git a/frontend/package.json b/frontend/package.json index e3e846ed..0b34c510 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -51,10 +51,11 @@ "react-hotkeys-hook": "^5.1.0", "react-i18next": "^15.7.3", "react-router-dom": "^6.8.1", + "react-use-websocket": "^4.7.0", "react-virtuoso": "^4.14.0", "react-window": "^1.8.11", "rfc6902": "^5.1.2", - "react-use-websocket": "^4.7.0", + "simple-icons": "^15.16.0", "tailwind-merge": "^2.2.0", "tailwindcss-animate": "^1.0.7", "vibe-kanban-web-companion": "^0.0.4", diff --git a/frontend/src/components/layout/navbar.tsx b/frontend/src/components/layout/navbar.tsx index 03b6842a..67f724e0 100644 --- a/frontend/src/components/layout/navbar.tsx +++ b/frontend/src/components/layout/navbar.tsx @@ -1,5 +1,6 @@ import { Link, useLocation } from 'react-router-dom'; -import { useCallback } from 'react'; +import { useCallback, useEffect, useState } from 'react'; +import { siDiscord } from 'simple-icons'; import { Button } from '@/components/ui/button'; import { DropdownMenu, @@ -13,6 +14,7 @@ import { Settings, BookOpen, MessageCircleQuestion, + MessageCircle, Menu, Plus, } from 'lucide-react'; @@ -24,6 +26,8 @@ import { useProject } from '@/contexts/project-context'; import { showProjectForm } from '@/lib/modals'; import { useOpenProjectInEditor } from '@/hooks/useOpenProjectInEditor'; +const DISCORD_GUILD_ID = '1423630976524877857'; + const INTERNAL_NAV = [ { label: 'Projects', icon: FolderOpen, to: '/projects' }, { label: 'Settings', icon: Settings, to: '/settings' }, @@ -40,6 +44,11 @@ const EXTERNAL_LINKS = [ icon: MessageCircleQuestion, href: 'https://github.com/BloopAI/vibe-kanban/issues', }, + { + label: 'Discord', + icon: MessageCircle, + href: 'https://discord.gg/AC4nwVtJM3', + }, ]; export function Navbar() { @@ -47,6 +56,36 @@ export function Navbar() { const { projectId, project } = useProject(); const { query, setQuery, active, clear, registerInputRef } = useSearch(); const handleOpenInEditor = useOpenProjectInEditor(project || null); + const [onlineCount, setOnlineCount] = useState(null); + + useEffect(() => { + let cancelled = false; + + const fetchCount = async () => { + try { + const res = await fetch( + `https://discord.com/api/guilds/${DISCORD_GUILD_ID}/widget.json`, + { cache: 'no-store' } + ); + if (!res.ok) return; // Widget disabled or temporary error; keep previous value + const data = await res.json(); + if (!cancelled && typeof data?.presence_count === 'number') { + setOnlineCount(data.presence_count); + } + } catch { + // Network error; ignore and keep previous value + } + }; + + // Initial fetch + refresh every 60s + fetchCount(); + const interval = setInterval(fetchCount, 60_000); + + return () => { + cancelled = true; + clearInterval(interval); + }; + }, []); const setSearchBarRef = useCallback( (node: HTMLInputElement | null) => { @@ -78,10 +117,36 @@ export function Navbar() {
-
+