From af8371ed84ab29ce59a0a7d39edeb8872d8276dd Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Sun, 28 Sep 2025 05:58:29 -0400 Subject: [PATCH] halfway commit to allow collaboration --- concord-client/bun.lock | 17 +- concord-client/package.json | 3 + concord-client/src/App.tsx | 33 +- .../src/components/direct-dropdown.tsx | 276 ++++++ .../src/components/layout/MemberList.tsx | 154 +++- .../src/components/layout/ServerSidebar.tsx | 83 +- .../src/components/layout/UserPanel.tsx | 32 +- .../components/modals/CreateChannelModal.tsx | 154 ++++ .../components/modals/CreateServerModal.tsx | 92 ++ .../components/modals/EditMessageModal.tsx | 121 +++ .../components/modals/MessageActionsModal.tsx | 101 +++ .../components/modals/PinnedMessagesModal.tsx | 155 ++++ .../src/components/modals/UserRoleModal.tsx | 262 ++++++ .../src/components/modals/UserStatusModal.tsx | 83 ++ .../src/components/server/ServerIcon.tsx | 332 ++++++- .../src/components/ui/dropdown-menu.tsx | 64 +- concord-client/src/hooks/useAuth.ts | 213 +++++ concord-client/src/hooks/useChannel.ts | 86 -- concord-client/src/hooks/useMessages.ts | 313 +++++++ concord-client/src/hooks/useServers.ts | 553 +++++------- concord-client/src/lib/api-client.ts | 351 ++++++++ concord-client/src/lib/auth-client.ts | 175 ++++ concord-client/src/pages/ChatPage.tsx | 834 ++++++++++++------ concord-client/src/pages/LoginPage.tsx | 61 +- concord-client/src/pages/SettingsPage.tsx | 737 +++++++++------- concord-client/src/types/database.ts | 7 +- concord-client/src/types/index.ts | 46 + concord-client/src/utils/permissions.ts | 198 +++++ concord-server/schema.prisma | 4 +- .../src/controller/userController.ts | 5 + concord-server/src/routes/authRoutes.ts | 26 +- concord-server/src/routes/userRoutes.ts | 33 + concord-server/src/services/userService.ts | 33 + .../src/validators/userValidator.ts | 4 + 34 files changed, 4418 insertions(+), 1223 deletions(-) create mode 100644 concord-client/src/components/direct-dropdown.tsx create mode 100644 concord-client/src/components/modals/CreateChannelModal.tsx create mode 100644 concord-client/src/components/modals/CreateServerModal.tsx create mode 100644 concord-client/src/components/modals/EditMessageModal.tsx create mode 100644 concord-client/src/components/modals/MessageActionsModal.tsx create mode 100644 concord-client/src/components/modals/PinnedMessagesModal.tsx create mode 100644 concord-client/src/components/modals/UserRoleModal.tsx create mode 100644 concord-client/src/components/modals/UserStatusModal.tsx create mode 100644 concord-client/src/hooks/useAuth.ts delete mode 100644 concord-client/src/hooks/useChannel.ts create mode 100644 concord-client/src/hooks/useMessages.ts create mode 100644 concord-client/src/lib/api-client.ts create mode 100644 concord-client/src/lib/auth-client.ts create mode 100644 concord-client/src/types/index.ts create mode 100644 concord-client/src/utils/permissions.ts diff --git a/concord-client/bun.lock b/concord-client/bun.lock index 9ea2ad0..dbd2a8c 100644 --- a/concord-client/bun.lock +++ b/concord-client/bun.lock @@ -18,6 +18,7 @@ "@tailwindcss/vite": "^4.1.13", "@tanstack/react-query": "^5.90.2", "@tanstack/react-query-devtools": "^5.90.2", + "bcrypt": "^6.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -34,6 +35,8 @@ "zustand": "^5.0.8", }, "devDependencies": { + "@types/bcrypt": "^6.0.0", + "@types/bun": "^1.2.22", "@types/react": "^18.2.64", "@types/react-dom": "^18.2.21", "@types/react-syntax-highlighter": "^15.5.13", @@ -378,6 +381,10 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/bcrypt": ["@types/bcrypt@6.0.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ=="], + + "@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], + "@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="], "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], @@ -488,6 +495,8 @@ "baseline-browser-mapping": ["baseline-browser-mapping@2.8.7", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-bxxN2M3a4d1CRoQC//IqsR5XrLh0IJ8TCv2x6Y9N0nckNz/rTjZB3//GGscZziZOxmjP55rzxg/ze7usFI9FqQ=="], + "bcrypt": ["bcrypt@6.0.0", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg=="], + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="], @@ -514,6 +523,8 @@ "builder-util-runtime": ["builder-util-runtime@9.2.4", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA=="], + "bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], + "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], "cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="], @@ -1030,7 +1041,9 @@ "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], - "node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], + "node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], + + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], "node-releases": ["node-releases@2.0.21", "", {}, "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw=="], @@ -1398,6 +1411,8 @@ "hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + "iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], + "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], diff --git a/concord-client/package.json b/concord-client/package.json index 0f6abb2..63a3cbf 100644 --- a/concord-client/package.json +++ b/concord-client/package.json @@ -24,6 +24,7 @@ "@tailwindcss/vite": "^4.1.13", "@tanstack/react-query": "^5.90.2", "@tanstack/react-query-devtools": "^5.90.2", + "bcrypt": "^6.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -40,6 +41,8 @@ "zustand": "^5.0.8" }, "devDependencies": { + "@types/bcrypt": "^6.0.0", + "@types/bun": "^1.2.22", "@types/react": "^18.2.64", "@types/react-dom": "^18.2.21", "@types/react-syntax-highlighter": "^15.5.13", diff --git a/concord-client/src/App.tsx b/concord-client/src/App.tsx index 0a2e230..8103b75 100644 --- a/concord-client/src/App.tsx +++ b/concord-client/src/App.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { BrowserRouter as Router, Routes, Route } from "react-router"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router"; +import { QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { Toaster } from "@/components/ui/sonner"; @@ -11,33 +11,22 @@ import ChatPage from "@/pages/ChatPage"; import SettingsPage from "@/pages/SettingsPage"; import NotFoundPage from "@/pages/NotFoundPage"; -// import { useAuthStore } from "@/stores/authStore"; -// import { useUiStore } from "@/stores/uiStore"; +import { queryClient } from "@/lib/api-client"; +import { useAuthStore } from "@/stores/authStore"; import ErrorBoundary from "@/components/common/ErrorBoundary"; import { Home } from "lucide-react"; -// Create a client -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 1000 * 60 * 5, // 5 minutes - refetchOnWindowFocus: false, - retry: (failureCount, error: any) => { - if (error?.status === 401) return false; - return failureCount < 3; - }, - }, - }, -}); - // Protected Route wrapper const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children, }) => { - // const { isAuthenticated } = useAuthStore(); - // if (!isAuthenticated) { - // return ; - // } + const { isAuthenticated } = useAuthStore(); + + // Enable this when you want to enforce authentication + if (!isAuthenticated) { + return ; + } + return <>{children}; }; diff --git a/concord-client/src/components/direct-dropdown.tsx b/concord-client/src/components/direct-dropdown.tsx new file mode 100644 index 0000000..5a08725 --- /dev/null +++ b/concord-client/src/components/direct-dropdown.tsx @@ -0,0 +1,276 @@ +import * as React from "react"; +import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; + +// Basic dropdown styles +const dropdownStyles = { + content: ` + min-w-[220px] + bg-white dark:bg-gray-800 + border border-gray-200 dark:border-gray-700 + rounded-md + p-1 + shadow-lg + z-50 + animate-in + fade-in-80 + data-[state=open]:animate-in + data-[state=closed]:animate-out + data-[state=closed]:fade-out-0 + data-[state=open]:fade-in-0 + data-[state=closed]:zoom-out-95 + data-[state=open]:zoom-in-95 + data-[side=bottom]:slide-in-from-top-2 + data-[side=left]:slide-in-from-right-2 + data-[side=right]:slide-in-from-left-2 + data-[side=top]:slide-in-from-bottom-2 + `, + item: ` + relative + flex + cursor-pointer + select-none + items-center + rounded-sm + px-2 + py-1.5 + text-sm + outline-none + transition-colors + hover:bg-gray-100 dark:hover:bg-gray-700 + focus:bg-gray-100 dark:focus:bg-gray-700 + data-[disabled]:pointer-events-none + data-[disabled]:opacity-50 + `, + separator: ` + -mx-1 + my-1 + h-px + bg-gray-200 dark:bg-gray-700 + `, + label: ` + px-2 + py-1.5 + text-sm + font-semibold + text-gray-500 dark:text-gray-400 + `, + destructive: ` + text-red-600 dark:text-red-400 + hover:bg-red-50 dark:hover:bg-red-900/20 + focus:bg-red-50 dark:focus:bg-red-900/20 + `, +}; + +// Utility to combine class names +const cn = (...classes: (string | undefined | false)[]) => { + return classes.filter(Boolean).join(" "); +}; + +// Root dropdown menu +export const DirectDropdownMenu = DropdownMenu.Root; +export const DirectDropdownMenuTrigger = DropdownMenu.Trigger; + +// Content component with styling +interface DirectDropdownMenuContentProps + extends React.ComponentProps { + className?: string; + sideOffset?: number; +} + +export const DirectDropdownMenuContent = React.forwardRef< + React.ElementRef, + DirectDropdownMenuContentProps +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +DirectDropdownMenuContent.displayName = "DirectDropdownMenuContent"; + +// Menu item component +interface DirectDropdownMenuItemProps + extends React.ComponentProps { + className?: string; + destructive?: boolean; +} + +export const DirectDropdownMenuItem = React.forwardRef< + React.ElementRef, + DirectDropdownMenuItemProps +>(({ className, destructive, ...props }, ref) => ( + +)); +DirectDropdownMenuItem.displayName = "DirectDropdownMenuItem"; + +// Separator component +interface DirectDropdownMenuSeparatorProps + extends React.ComponentProps { + className?: string; +} + +export const DirectDropdownMenuSeparator = React.forwardRef< + React.ElementRef, + DirectDropdownMenuSeparatorProps +>(({ className, ...props }, ref) => ( + +)); +DirectDropdownMenuSeparator.displayName = "DirectDropdownMenuSeparator"; + +// Label component +interface DirectDropdownMenuLabelProps + extends React.ComponentProps { + className?: string; +} + +export const DirectDropdownMenuLabel = React.forwardRef< + React.ElementRef, + DirectDropdownMenuLabelProps +>(({ className, ...props }, ref) => ( + +)); +DirectDropdownMenuLabel.displayName = "DirectDropdownMenuLabel"; + +// Checkbox item component +interface DirectDropdownMenuCheckboxItemProps + extends React.ComponentProps { + className?: string; + children: React.ReactNode; +} + +export const DirectDropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + DirectDropdownMenuCheckboxItemProps +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +DirectDropdownMenuCheckboxItem.displayName = "DirectDropdownMenuCheckboxItem"; + +// Sub menu components +export const DirectDropdownMenuSub = DropdownMenu.Sub; + +interface DirectDropdownMenuSubTriggerProps + extends React.ComponentProps { + className?: string; + children: React.ReactNode; +} + +export const DirectDropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + DirectDropdownMenuSubTriggerProps +>(({ className, children, ...props }, ref) => ( + + {children} + + +)); +DirectDropdownMenuSubTrigger.displayName = "DirectDropdownMenuSubTrigger"; + +interface DirectDropdownMenuSubContentProps + extends React.ComponentProps { + className?: string; +} + +export const DirectDropdownMenuSubContent = React.forwardRef< + React.ElementRef, + DirectDropdownMenuSubContentProps +>(({ className, ...props }, ref) => ( + +)); +DirectDropdownMenuSubContent.displayName = "DirectDropdownMenuSubContent"; + +// Example usage component to test the dropdown +export const DirectDropdownExample: React.FC = () => { + const [isOpen, setIsOpen] = React.useState(false); + + return ( +
+ + + + + + + My Account + + + console.log("Profile clicked")} + > + Profile + + + console.log("Settings clicked")} + > + Settings + + + + + console.log("Logout clicked")} + > + Log out + + + +
+ ); +}; diff --git a/concord-client/src/components/layout/MemberList.tsx b/concord-client/src/components/layout/MemberList.tsx index ce3df5a..cfd5422 100644 --- a/concord-client/src/components/layout/MemberList.tsx +++ b/concord-client/src/components/layout/MemberList.tsx @@ -1,11 +1,15 @@ -import React from "react"; +// src/components/layout/MemberList.tsx - Enhanced with role management +import React, { useState } from "react"; import { useParams } from "react-router"; import { Crown, Shield, UserIcon } from "lucide-react"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Button } from "@/components/ui/button"; import { Role } from "@/types/database"; import { useInstanceMembers } from "@/hooks/useServers"; +import { useAuthStore } from "@/stores/authStore"; import { User } from "@/types/database"; +import { UserRoleModal } from "@/components/modals/UserRoleModal"; // Status color utility const getStatusColor = (status: string) => { @@ -23,6 +27,9 @@ const getStatusColor = (status: string) => { interface MemberItemProps { member: User; + instanceId: string; + currentUserRole: string; + canManageRoles: boolean; isOwner?: boolean; } @@ -43,61 +50,117 @@ const getRoleInfo = (role: string) => { } }; -const MemberItem: React.FC = ({ member, isOwner = false }) => { - const { instanceId } = useParams(); +const MemberItem: React.FC = ({ + member, + instanceId, + currentUserRole, + canManageRoles, + isOwner = false, +}) => { + const [showUserModal, setShowUserModal] = useState(false); const userRole = getUserRoleForInstance(member.roles, instanceId || ""); const roleInfo = getRoleInfo(userRole); - return ( -
-
- - - - {member.username.slice(0, 2).toUpperCase()} - - - {/* Status indicator */} -
-
+ const handleMemberClick = () => { + if (canManageRoles && !member.admin) { + setShowUserModal(true); + } + }; -
-
- {isOwner && ( - - )} - {!isOwner && userRole !== "member" && ( - +
+ + + {/* User Role Modal */} + setShowUserModal(false)} + user={member} + instanceId={instanceId} + currentUserRole={currentUserRole} + canManageRoles={canManageRoles} + /> + ); }; const MemberList: React.FC = () => { const { instanceId } = useParams(); const { data: members, isLoading } = useInstanceMembers(instanceId); + const { user: currentUser } = useAuthStore(); + + // Check if current user can manage roles + const canManageRoles = React.useMemo(() => { + if (!currentUser || !instanceId) return false; + + // Global admins can manage roles + if (currentUser.admin) return true; + + // Check if user is admin or mod in this instance + const userRole = currentUser.roles.find( + (role) => role.instanceId === instanceId, + ); + return userRole && (userRole.role === "admin" || userRole.role === "mod"); + }, [currentUser, instanceId]); + + const currentUserRole = React.useMemo(() => { + if (!currentUser || !instanceId) return "member"; + if (currentUser.admin) return "admin"; + + const userRole = currentUser.roles.find( + (role) => role.instanceId === instanceId, + ); + return userRole?.role || "member"; + }, [currentUser, instanceId]); if (!instanceId) { return null; @@ -177,6 +240,9 @@ const MemberList: React.FC = () => { ))} diff --git a/concord-client/src/components/layout/ServerSidebar.tsx b/concord-client/src/components/layout/ServerSidebar.tsx index 0c20d3e..650a3d2 100644 --- a/concord-client/src/components/layout/ServerSidebar.tsx +++ b/concord-client/src/components/layout/ServerSidebar.tsx @@ -10,34 +10,45 @@ import { } from "@/components/ui/tooltip"; import { useServers } from "@/hooks/useServers"; import { useUiStore } from "@/stores/uiStore"; +import { useAuthStore } from "@/stores/authStore"; import ServerIcon from "@/components/server/ServerIcon"; +import { getAccessibleInstances, isGlobalAdmin } from "@/utils/permissions"; const ServerSidebar: React.FC = () => { const navigate = useNavigate(); const { instanceId } = useParams(); - const { data: servers, isLoading } = useServers(); + const { data: allServers = [], isLoading } = useServers(); const { openCreateServer, setActiveInstance, getSelectedChannelForInstance } = useUiStore(); + const { user: currentUser } = useAuthStore(); + + // Filter servers based on user permissions + const accessibleServers = getAccessibleInstances(currentUser, allServers); + const canCreateServer = isGlobalAdmin(currentUser); const handleServerClick = (serverId: string) => { setActiveInstance(serverId); const lastChannelId = getSelectedChannelForInstance(serverId); - console.log(servers); - if (lastChannelId) { navigate(`/channels/${serverId}/${lastChannelId}`); } else { // Fallback: navigate to the server, let the page component handle finding a channel - // A better UX would be to find and navigate to the first channel here. navigate(`/channels/${serverId}`); } }; + const handleHomeClick = () => { setActiveInstance(null); navigate("/channels/@me"); }; + const handleCreateServer = () => { + if (canCreateServer) { + openCreateServer(); + } + }; + return (
@@ -47,8 +58,10 @@ const ServerSidebar: React.FC = () => { -

Direct Messages

+

{isGlobalAdmin(currentUser) ? "Admin Dashboard" : "Home"}

@@ -69,8 +82,8 @@ const ServerSidebar: React.FC = () => {
- ) : ( - servers?.map((server) => ( + ) : accessibleServers.length > 0 ? ( + accessibleServers.map((server) => (
@@ -86,25 +99,43 @@ const ServerSidebar: React.FC = () => { )) - )} + ) : currentUser ? ( +
+
+ No servers available +
+ {canCreateServer && ( + + )} +
+ ) : null}
- {/* Add Server Button */} - - - - - -

Add a Server

-
-
+ {/* Add Server Button - Only show if user can create servers */} + {canCreateServer && ( + + + + + +

Add a Server

+
+
+ )}
); diff --git a/concord-client/src/components/layout/UserPanel.tsx b/concord-client/src/components/layout/UserPanel.tsx index 10b134a..252b940 100644 --- a/concord-client/src/components/layout/UserPanel.tsx +++ b/concord-client/src/components/layout/UserPanel.tsx @@ -18,7 +18,7 @@ import { import { useAuthStore } from "@/stores/authStore"; import { useUiStore } from "@/stores/uiStore"; -import { SAMPLE_USERS } from "@/hooks/useServers"; +import { useLogout } from "@/hooks/useAuth"; // Status color utility const getStatusColor = (status: string) => { @@ -46,6 +46,8 @@ const UserStatusDropdown: React.FC = ({ onStatusChange, children, }) => { + const { mutate: logout } = useLogout(); + const statusOptions = [ { value: "online", label: "Online", color: "bg-status-online" }, { value: "away", label: "Away", color: "bg-status-away" }, @@ -79,7 +81,7 @@ const UserStatusDropdown: React.FC = ({ useAuthStore.getState().logout()} + onClick={() => logout()} className="text-destructive focus:text-destructive" > Log Out @@ -212,12 +214,19 @@ const UserPanel: React.FC = () => { const [isMuted, setIsMuted] = useState(false); const [isDeafened, setIsDeafened] = useState(false); - const displayUser = user || SAMPLE_USERS.find((u) => u.id === "current"); - - if (!displayUser) { + // If no authenticated user, show login prompt + if (!user) { return ( -
-
No user data
+
+
+ +
); } @@ -225,6 +234,7 @@ const UserPanel: React.FC = () => { const handleStatusChange = (newStatus: string) => { console.log("Status change to:", newStatus); // TODO: Implement API call to update user status + // You can add a useUpdateUserStatus hook here }; const handleMuteToggle = () => setIsMuted(!isMuted); @@ -240,20 +250,20 @@ const UserPanel: React.FC = () => {
{/* User Info with Dropdown */} diff --git a/concord-client/src/components/modals/CreateChannelModal.tsx b/concord-client/src/components/modals/CreateChannelModal.tsx new file mode 100644 index 0000000..177d318 --- /dev/null +++ b/concord-client/src/components/modals/CreateChannelModal.tsx @@ -0,0 +1,154 @@ +import React, { useState } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Hash, Volume2 } from "lucide-react"; +import { useCreateChannel } from "@/hooks/useServers"; +import { CategoryWithChannels } from "@/types/api"; + +interface CreateChannelModalProps { + isOpen: boolean; + onClose: () => void; + categories: CategoryWithChannels[]; + defaultCategoryId?: string; +} + +export const CreateChannelModal: React.FC = ({ + isOpen, + onClose, + categories, + defaultCategoryId, +}) => { + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [type, setType] = useState<"text" | "voice">("text"); + const [categoryId, setCategoryId] = useState(defaultCategoryId || ""); + + const createChannelMutation = useCreateChannel(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!name.trim() || !categoryId) return; + + try { + await createChannelMutation.mutateAsync({ + name: name.trim(), + description: description.trim(), + type, + categoryId, + }); + + // Reset form + setName(""); + setDescription(""); + setType("text"); + setCategoryId(defaultCategoryId || ""); + onClose(); + } catch (error) { + console.error("Failed to create channel:", error); + } + }; + + return ( + + + + Create Channel + +
+
+ +
+ + +
+
+ +
+ + setName(e.target.value)} + placeholder="awesome-channel" + required + /> +
+ +
+ +