ui: consistency across pages and better autofocus
- Consistency achieved across all pages on header items, icons, and margins / positions. Kinda hacked together quite a bit, may need to focus on a consistent style guide across components via utility classes - Autofocus on MessageInput. Refs were giving problems, ended up using DOM queries and focus is now achieved on ChatPage load and when replying to a user. Not ideal and should be changed to Refs again at some point - CreateChannelModal now loads categories, still fails the actual POST request though - Removed extrenuous console.log statements - Laid groundwork for permissions system - New more consistent look and feel overall
This commit is contained in:
@@ -38,11 +38,6 @@ const ChannelItem: React.FC<ChannelItemProps> = ({ channel }) => {
|
|||||||
if (isConnectedToThisChannel) {
|
if (isConnectedToThisChannel) {
|
||||||
leaveChannel();
|
leaveChannel();
|
||||||
} else if (currentUser && token) {
|
} else if (currentUser && token) {
|
||||||
console.log({
|
|
||||||
channelId: channel.id,
|
|
||||||
currentUser: currentUser.id,
|
|
||||||
token: token,
|
|
||||||
});
|
|
||||||
joinChannel(channel.id, currentUser.id, token);
|
joinChannel(channel.id, currentUser.id, token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const CategoryHeader: React.FC<CategoryHeaderProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="w-full justify-between p-4 h-6 text-md text-primary-foreground font-semibold interactive-hover uppercase tracking-wide group"
|
className="w-full justify-between p-4 h-6 text-md text-concord-primary font-semibold interactive-hover uppercase tracking-wide group"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onToggle();
|
onToggle();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Component, ErrorInfo, ReactNode } from "react";
|
|||||||
import { AlertTriangle, RotateCcw } from "lucide-react";
|
import { AlertTriangle, RotateCcw } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
|
import { Card } from "../ui/card";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -48,8 +49,8 @@ class ErrorBoundary extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
<div className="min-h-screen bg-concord-primary flex items-center justify-center p-4">
|
||||||
<div className="max-w-md w-full space-y-4">
|
<Card className="max-w-md bg-concord-secondary border-concord w-full space-y-4">
|
||||||
<Alert className="border-red-500 bg-red-950/50">
|
<Alert className="border-red-500 bg-red-950/50">
|
||||||
<AlertTriangle className="h-4 w-4 text-red-500" />
|
<AlertTriangle className="h-4 w-4 text-red-500" />
|
||||||
<AlertTitle className="text-red-400">
|
<AlertTitle className="text-red-400">
|
||||||
@@ -82,7 +83,7 @@ class ErrorBoundary extends Component<Props, State> {
|
|||||||
|
|
||||||
{/* Error details in development */}
|
{/* Error details in development */}
|
||||||
{process.env.NODE_ENV === "development" && this.state.error && (
|
{process.env.NODE_ENV === "development" && this.state.error && (
|
||||||
<details className="mt-4 p-3 bg-gray-800 rounded-lg text-sm">
|
<details className="mt-4 p-3 bg-concord-secondary rounded-lg text-sm">
|
||||||
<summary className="cursor-pointer text-red-400 font-medium mb-2">
|
<summary className="cursor-pointer text-red-400 font-medium mb-2">
|
||||||
Error Details (Development)
|
Error Details (Development)
|
||||||
</summary>
|
</summary>
|
||||||
@@ -107,7 +108,7 @@ class ErrorBoundary extends Component<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const AppLayout: React.FC = () => {
|
|||||||
<VoiceConnectionManager />
|
<VoiceConnectionManager />
|
||||||
|
|
||||||
{/* Server List Sidebar - Always visible on desktop, overlay on mobile */}
|
{/* Server List Sidebar - Always visible on desktop, overlay on mobile */}
|
||||||
<div className="relative w-[72px] sidebar-primary flex-shrink-0">
|
<div className="relative min-w-1/16 sidebar-primary flex-shrink-0">
|
||||||
<ServerSidebar />
|
<ServerSidebar />
|
||||||
</div>
|
</div>
|
||||||
{/* Channel Sidebar - Only shown when in a server context and not collapsed */}
|
{/* Channel Sidebar - Only shown when in a server context and not collapsed */}
|
||||||
@@ -68,7 +68,7 @@ const AppLayout: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
{/* Member List - Only shown when in a channel and member list is enabled */}
|
{/* Member List - Only shown when in a channel and member list is enabled */}
|
||||||
{showMemberList && shouldShowChannelSidebar && (
|
{showMemberList && shouldShowChannelSidebar && (
|
||||||
<div className="flex-0 sidebar-secondary order-l border-sidebar">
|
<div className="flex-0 min-w-1/7 sidebar-secondary order-l border-sidebar">
|
||||||
<MemberList />
|
<MemberList />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import { ChevronDown, Plus, Users } from "lucide-react";
|
import { ChevronDown, Plus } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { useInstanceDetails } from "@/hooks/useServers";
|
import { useInstanceDetails } from "@/hooks/useServers";
|
||||||
import { useUiStore } from "@/stores/uiStore";
|
import { useUiStore } from "@/stores/uiStore";
|
||||||
import ChannelList from "@/components/channel/ChannelList";
|
import ChannelList from "@/components/channel/ChannelList";
|
||||||
import { CreateCategoryModal, CreateChannelModal } from "../server/ServerIcon";
|
import { CreateChannelModal } from "@/components/modals/CreateChannelModal";
|
||||||
|
|
||||||
const ChannelSidebar: React.FC = () => {
|
const ChannelSidebar: React.FC = () => {
|
||||||
const { instanceId } = useParams();
|
const { instanceId } = useParams();
|
||||||
@@ -14,8 +14,8 @@ const ChannelSidebar: React.FC = () => {
|
|||||||
useInstanceDetails(instanceId);
|
useInstanceDetails(instanceId);
|
||||||
const categories = instance?.categories;
|
const categories = instance?.categories;
|
||||||
const {
|
const {
|
||||||
toggleMemberList,
|
showCreateChannel,
|
||||||
showMemberList,
|
closeCreateChannel,
|
||||||
openCreateChannel,
|
openCreateChannel,
|
||||||
openServerSettings,
|
openServerSettings,
|
||||||
} = useUiStore();
|
} = useUiStore();
|
||||||
@@ -45,11 +45,11 @@ const ChannelSidebar: React.FC = () => {
|
|||||||
<div className="sidebar-secondary flex-1">
|
<div className="sidebar-secondary flex-1">
|
||||||
<ScrollArea className="">
|
<ScrollArea className="">
|
||||||
{/* Server Header */}
|
{/* Server Header */}
|
||||||
<div className="flex items-center justify-between border-b border-concord-primary shadow-sm px-4 py-3">
|
<div className="flex items-center justify-between border-b border-concord-primary shadow-sm px-6 py-4">
|
||||||
<div className="flex items-center space-x-2 flex-1 min-w-0">
|
<div className="flex items-center space-x-2 flex-1 min-w-0">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="flex items-center justify-between w-full h-8 font-semibold text-concord-primary hover:bg-concord-tertiary"
|
className="flex items-center justify-between w-full h-8 font-semibold text-concord-primary text-xl hover:bg-concord-tertiary"
|
||||||
onClick={openServerSettings}
|
onClick={openServerSettings}
|
||||||
>
|
>
|
||||||
<span className="truncate">{instance.name}</span>
|
<span className="truncate">{instance.name}</span>
|
||||||
@@ -83,20 +83,15 @@ const ChannelSidebar: React.FC = () => {
|
|||||||
<Plus size={16} className="mr-1" />
|
<Plus size={16} className="mr-1" />
|
||||||
Add Channel
|
Add Channel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className={`h-8 w-8 flex-shrink-0 ${showMemberList ? "text-interactive-active" : "interactive-hover"}`}
|
|
||||||
onClick={toggleMemberList}
|
|
||||||
>
|
|
||||||
<Users size={16} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<CreateChannelModal />
|
<CreateChannelModal
|
||||||
<CreateCategoryModal />
|
isOpen={showCreateChannel}
|
||||||
|
onClose={closeCreateChannel}
|
||||||
|
categories={categories}
|
||||||
|
instanceId={instance.id}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -206,16 +206,16 @@ const MemberList: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-1 border-l border-concord-primary h-full bg-concord-secondary">
|
<div className="flex flex-col flex-grow-1 w-full border-l border-concord h-full sidebar-secondary">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="px-4 py-3 border-b border-concord-primary flex items-center justify-between">
|
<div className="px-6 py-4 pb-5 border-b border-concord flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center space-x-2">
|
||||||
<UserIcon size={20} className="text-concord-primary" />
|
<UserIcon size={20} className="h-5 w-5 text-concord-secondary" />
|
||||||
<p className="text-sm font-semibold text-concord-primary tracking-wide">
|
<p className="font-semibold text-xl text-concord-primary tracking-wide">
|
||||||
Members
|
Members
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-medium text-concord-secondary tracking-wide">
|
<p className="font-medium text-concord-secondary tracking-wide">
|
||||||
{members.length}
|
{members.length}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,17 +51,17 @@ const ServerSidebar: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="sidebar-primary flex flex-col items-center h-full space-y-2">
|
<div className="sidebar-primary flex flex-col items-center h-full space-y-2 w-full">
|
||||||
{/* Home/DM Button */}
|
{/* Home/DM Button */}
|
||||||
<Tooltip key={"home-server"}>
|
<Tooltip key={"home-server"}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={`w-10 h-10 p-0 mt-2 ml-2 rounded-full transition-all duration-200 ${
|
className={`w-12 h-12 mt-2 transition-all duration-200 ${
|
||||||
!instanceId || instanceId === "@me"
|
!instanceId || instanceId === "@me"
|
||||||
? "bg-primary text-primary-foreground"
|
? "rounded-xl border-primary bg-primary/10 border-2"
|
||||||
: "hover:bg-primary/10"
|
: "rounded-2xl hover:rounded-xl border hover:border-primary/50"
|
||||||
}`}
|
}`}
|
||||||
onClick={handleHomeClick}
|
onClick={handleHomeClick}
|
||||||
>
|
>
|
||||||
@@ -72,10 +72,8 @@ const ServerSidebar: React.FC = () => {
|
|||||||
<p>{isGlobalAdmin(currentUser) ? "Admin Dashboard" : "Home"}</p>
|
<p>{isGlobalAdmin(currentUser) ? "Admin Dashboard" : "Home"}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* Separator */}
|
{/* Separator */}
|
||||||
<div className="w-full ml-0 h-0.5 bg-border rounded-full" />
|
<div className="w-full h-0.5 bg-border rounded-full" />{" "}
|
||||||
|
|
||||||
{/* Server List */}
|
{/* Server List */}
|
||||||
<div className="flex-1 flex flex-col overflow-y-auto scrollbar-thin scrollbar-thumb-border space-y-2">
|
<div className="flex-1 flex flex-col overflow-y-auto scrollbar-thin scrollbar-thumb-border space-y-2">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@@ -116,26 +114,25 @@ const ServerSidebar: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{/* Add Server Button - Only show if user can create servers */}
|
||||||
|
{canCreateServer && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="w-12 mb-4 h-12 rounded-2xl hover:rounded-xl bg-concord-secondary hover:bg-green-600 text-green-500 hover:text-white transition-all duration-200"
|
||||||
|
onClick={handleCreateServer}
|
||||||
|
>
|
||||||
|
<Plus size={24} />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">
|
||||||
|
<p>Add a Server</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Add Server Button - Only show if user can create servers */}
|
|
||||||
{canCreateServer && (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="w-12 h-12 ml-3 rounded-2xl hover:rounded-xl bg-concord-secondary hover:bg-green-600 text-green-500 hover:text-white transition-all duration-200"
|
|
||||||
onClick={handleCreateServer}
|
|
||||||
>
|
|
||||||
<Plus size={24} />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right">
|
|
||||||
<p>Add a Server</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ const UserPanel: React.FC = () => {
|
|||||||
useVoiceStore();
|
useVoiceStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="user-panel flex items-center p-2 bg-concord-tertiary border-t border-sidebar">
|
<div className="user-panel flex items-center p-3 bg-concord-tertiary border-t border-sidebar min-h-16 rounded-xl m-2">
|
||||||
{/* User Info */}
|
{/* User Info */}
|
||||||
<UserAvatar user={user} size="md" />
|
<UserAvatar user={user} size="md" />
|
||||||
<div className="ml-2 flex-1 min-w-0 text-left">
|
<div className="ml-2 flex-1 min-w-0 text-left">
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
// src/components/message/MessageInput.tsx
|
|
||||||
|
|
||||||
import { Message } from "@/lib/api-client";
|
import { Message } from "@/lib/api-client";
|
||||||
import { useState, useRef, useEffect } from "react"; // Keep useRef
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { useSendMessage } from "@/hooks/useMessages";
|
import { useSendMessage } from "@/hooks/useMessages";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
|
||||||
interface MessageUser {
|
interface MessageUser {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -27,24 +26,21 @@ export const MessageInput: React.FC<MessageInputProps> = ({
|
|||||||
replyingToUser,
|
replyingToUser,
|
||||||
}) => {
|
}) => {
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
// Use the API hook for sending messages
|
// Use the API hook for sending messages
|
||||||
const sendMessageMutation = useSendMessage();
|
const sendMessageMutation = useSendMessage();
|
||||||
|
|
||||||
// Auto-resize textarea and focus when replying
|
// Auto-resize textarea (using direct DOM access as a fallback, no ref needed)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (textareaRef.current) {
|
const textarea = document.getElementById(
|
||||||
textareaRef.current.style.height = "auto";
|
"message-input-textarea",
|
||||||
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
|
) as HTMLTextAreaElement | null;
|
||||||
|
if (textarea) {
|
||||||
// Focus the input when a reply is initiated
|
textarea.style.height = "auto";
|
||||||
if (replyingTo) {
|
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||||
textareaRef.current.focus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [content, replyingTo]);
|
}, [content]);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -71,7 +67,7 @@ export const MessageInput: React.FC<MessageInputProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4 pb-4">
|
<div className="px-2 pb-2">
|
||||||
{replyingTo && replyingToUser && (
|
{replyingTo && replyingToUser && (
|
||||||
<div className="mb-2 p-3 bg-concord-secondary rounded-lg border border-b-0 border-border">
|
<div className="mb-2 p-3 bg-concord-secondary rounded-lg border border-b-0 border-border">
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
@@ -97,26 +93,15 @@ export const MessageInput: React.FC<MessageInputProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<form ref={formRef} onSubmit={handleSubmit}>
|
<form ref={formRef} onSubmit={handleSubmit}>
|
||||||
<div className="relative">
|
<Textarea
|
||||||
<textarea
|
id="message-input-textarea" // Unique ID for DOM targeting
|
||||||
ref={textareaRef}
|
value={content}
|
||||||
value={content}
|
onChange={(e) => setContent(e.target.value)}
|
||||||
onChange={(e) => setContent(e.target.value)}
|
onKeyDown={handleKeyDown}
|
||||||
onKeyDown={handleKeyDown}
|
placeholder={`Message #${channelName || "channel"}`}
|
||||||
placeholder={`Message #${channelName || "channel"}`}
|
disabled={sendMessageMutation.isPending}
|
||||||
disabled={sendMessageMutation.isPending}
|
className="w-full bg-concord-tertiary border border-border rounded-lg px-4 py-3 text-concord-primary placeholder-concord-muted resize-none focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:opacity-50 min-h-8 max-h-56"
|
||||||
className="w-full bg-concord-tertiary border border-border rounded-lg px-4 py-3 text-concord-primary placeholder-concord-muted resize-none focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:opacity-50"
|
/>
|
||||||
style={{
|
|
||||||
minHeight: "44px",
|
|
||||||
maxHeight: "200px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="absolute right-3 bottom-3 text-xs text-concord-secondary">
|
|
||||||
{sendMessageMutation.isPending
|
|
||||||
? "Sending..."
|
|
||||||
: "Press Enter to send • Shift+Enter for new line"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,39 +11,58 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Hash, Volume2, Loader2 } from "lucide-react";
|
import { Hash, Volume2, Loader2 } from "lucide-react";
|
||||||
import { useCreateChannel } from "@/hooks/useServers";
|
import { useCreateChannel } from "@/hooks/useServers";
|
||||||
|
import { CategoryWithChannels } from "@/types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface CreateChannelModalProps {
|
interface CreateChannelModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
instanceId: string; // Changed to use instanceId instead of categories prop
|
instanceId: string;
|
||||||
defaultCategoryId?: string;
|
categories: CategoryWithChannels[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
defaultCategoryId,
|
categories,
|
||||||
}) => {
|
}) => {
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [type, setType] = useState<"text" | "voice">("text");
|
const [type, setType] = useState<"text" | "voice">("text");
|
||||||
const [categoryId, setCategoryId] = useState(defaultCategoryId || "");
|
const [categoryId, setCategoryId] = useState("");
|
||||||
|
|
||||||
const createChannelMutation = useCreateChannel();
|
const createChannelMutation = useCreateChannel();
|
||||||
|
|
||||||
// Reset form when modal opens/closes
|
// Reset form when modal opens or closes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
setName("");
|
setName("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
setType("text");
|
setType("text");
|
||||||
setCategoryId(defaultCategoryId || "");
|
setCategoryId("");
|
||||||
|
} else {
|
||||||
|
setCategoryId("");
|
||||||
}
|
}
|
||||||
}, [isOpen, defaultCategoryId]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!name.trim() || !categoryId) return;
|
// Basic validation: ensure name is not empty and a category is selected
|
||||||
|
if (!name.trim() || !categoryId || categoryId === "no-categories") {
|
||||||
|
console.warn("Channel name and a valid category are required.");
|
||||||
|
toast("Error", {
|
||||||
|
description: "Channel name and a valid category are required.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createChannelMutation.mutateAsync({
|
await createChannelMutation.mutateAsync({
|
||||||
@@ -53,17 +72,25 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
|||||||
categoryId,
|
categoryId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset form
|
// Reset form after successful creation
|
||||||
setName("");
|
setName("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
setType("text");
|
setType("text");
|
||||||
setCategoryId(defaultCategoryId || "");
|
setCategoryId(""); // Reset to default or empty
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to create channel:", error);
|
console.error("Failed to create channel:", error);
|
||||||
|
toast("Error", { description: <p>{`${error}`}</p> });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper to determine if the form is in a valid state for submission
|
||||||
|
const isFormInvalid =
|
||||||
|
!name.trim() || // Name is required and cannot be just whitespace
|
||||||
|
!categoryId || // Category must be selected
|
||||||
|
categoryId === "no-categories" || // Handle the "no categories available" placeholder
|
||||||
|
createChannelMutation.isPending; // Disable while mutation is in progress
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
<DialogContent className="sm:max-w-[400px]">
|
<DialogContent className="sm:max-w-[400px]">
|
||||||
@@ -72,12 +99,13 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
{/* Channel Type Selection */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Channel Type</Label>
|
<Label>Channel Type</Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={type === "text" ? "default" : "outline"}
|
variant={type === "text" ? "secondary" : "ghost"}
|
||||||
onClick={() => setType("text")}
|
onClick={() => setType("text")}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
@@ -86,7 +114,7 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={type === "voice" ? "default" : "outline"}
|
variant={type === "voice" ? "secondary" : "ghost"}
|
||||||
onClick={() => setType("voice")}
|
onClick={() => setType("voice")}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
@@ -96,6 +124,7 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Channel Name Input */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="channel-name">Channel Name</Label>
|
<Label htmlFor="channel-name">Channel Name</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -107,6 +136,35 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Category Selection */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="channel-category">Category</Label>
|
||||||
|
<Select
|
||||||
|
value={categoryId}
|
||||||
|
onValueChange={(value) => setCategoryId(value)}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select a category" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{categories && categories.length > 0 ? (
|
||||||
|
categories.map((category) => (
|
||||||
|
<SelectItem key={category.id} value={category.id}>
|
||||||
|
{category.name}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
// Display this option if there are no categories
|
||||||
|
<SelectItem value="no-categories" disabled>
|
||||||
|
No categories available
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Channel Description Textarea */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="channel-description">Description</Label>
|
<Label htmlFor="channel-description">Description</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
@@ -118,20 +176,12 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button type="button" variant="outline" onClick={onClose}>
|
<Button type="button" variant="outline" onClick={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button type="submit" disabled={isFormInvalid}>
|
||||||
type="submit"
|
|
||||||
disabled={
|
|
||||||
!name.trim() ||
|
|
||||||
!categoryId ||
|
|
||||||
createChannelMutation.isPending ||
|
|
||||||
categoryId === "loading" ||
|
|
||||||
categoryId === "no-categories"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{createChannelMutation.isPending ? (
|
{createChannelMutation.isPending ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
|||||||
@@ -1,30 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Instance } from "@/types/database";
|
import { Instance } from "@/types/database";
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
} from "../ui/dialog";
|
|
||||||
import { DialogDescription, DialogTitle } from "@radix-ui/react-dialog";
|
|
||||||
import { Label } from "../ui/label";
|
|
||||||
import { Input } from "../ui/input";
|
|
||||||
import { Textarea } from "../ui/textarea";
|
|
||||||
import { Category } from "@/types";
|
|
||||||
import { useUiStore } from "@/stores/uiStore";
|
|
||||||
import {
|
|
||||||
useCreateCategory,
|
|
||||||
useCreateChannel,
|
|
||||||
useCreateInstance,
|
|
||||||
} from "@/hooks/useServers";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectItem,
|
|
||||||
SelectValue,
|
|
||||||
} from "../ui/select";
|
|
||||||
|
|
||||||
interface ServerIconProps {
|
interface ServerIconProps {
|
||||||
server: Instance;
|
server: Instance;
|
||||||
@@ -47,17 +23,17 @@ const ServerIcon: React.FC<ServerIconProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative group">
|
<div className="relative group w-12">
|
||||||
{/* Active indicator */}
|
{/* Active indicator - Positioned outside to the left */}
|
||||||
<div
|
<div
|
||||||
className={`absolute left-0 top-1/2 transform -translate-y-1/2 w-1 bg-accent-foreground rounded transition-all duration-200 ${
|
className={`absolute -left-2 top-1/2 transform -translate-y-1/2 w-1 bg-accent-foreground rounded transition-all duration-200 ${
|
||||||
isActive ? "h-10 rounded-xl" : "rounded-r h-2 group-hover:h-5"
|
isActive ? "h-10 rounded-xl" : "rounded-r h-2 group-hover:h-5"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={`w-12 h-12 ml-3 transition-all duration-200 ${
|
className={`w-12 h-12 transition-all duration-200 ${
|
||||||
isActive
|
isActive
|
||||||
? "rounded-xl border-primary bg-primary/10 border-2"
|
? "rounded-xl border-primary bg-primary/10 border-2"
|
||||||
: "rounded-2xl hover:rounded-xl border hover:border-primary/50"
|
: "rounded-2xl hover:rounded-xl border hover:border-primary/50"
|
||||||
@@ -80,308 +56,4 @@ const ServerIcon: React.FC<ServerIconProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create Server Modal
|
|
||||||
export const CreateServerModal: React.FC = () => {
|
|
||||||
const { showCreateServer, closeCreateServer } = useUiStore();
|
|
||||||
const { mutate: createInstance, isPending } = useCreateInstance();
|
|
||||||
|
|
||||||
const [name, setName] = useState("");
|
|
||||||
const [description, setDescription] = useState("");
|
|
||||||
const [icon, setIcon] = useState("");
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (name.trim()) {
|
|
||||||
createInstance(
|
|
||||||
{
|
|
||||||
name: name.trim(),
|
|
||||||
icon: icon.trim() || undefined,
|
|
||||||
// description: description.trim() || undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
setName("");
|
|
||||||
setDescription("");
|
|
||||||
setIcon("");
|
|
||||||
closeCreateServer();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error("Failed to create server:", error);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setName("");
|
|
||||||
setDescription("");
|
|
||||||
setIcon("");
|
|
||||||
closeCreateServer();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={showCreateServer} onOpenChange={handleClose}>
|
|
||||||
<DialogContent className="sm:max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Create a Server</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Create a new server to chat with friends and communities.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="server-name">Server Name</Label>
|
|
||||||
<Input
|
|
||||||
id="server-name"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
placeholder="Enter server name"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="server-description">Description (Optional)</Label>
|
|
||||||
<Textarea
|
|
||||||
id="server-description"
|
|
||||||
value={description}
|
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
|
||||||
placeholder="What's this server about?"
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="server-icon">Server Icon URL (Optional)</Label>
|
|
||||||
<Input
|
|
||||||
id="server-icon"
|
|
||||||
type="url"
|
|
||||||
value={icon}
|
|
||||||
onChange={(e) => setIcon(e.target.value)}
|
|
||||||
placeholder="https://example.com/icon.png"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button type="button" variant="outline" onClick={handleClose}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isPending || !name.trim()}>
|
|
||||||
{isPending ? "Creating..." : "Create Server"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create Channel Modal
|
|
||||||
export const CreateChannelModal: React.FC = () => {
|
|
||||||
const { showCreateChannel, closeCreateChannel } = useUiStore();
|
|
||||||
const { mutate: createChannel, isPending } = useCreateChannel();
|
|
||||||
|
|
||||||
const [name, setName] = useState("");
|
|
||||||
const [description, setDescription] = useState("");
|
|
||||||
const [type, setType] = useState<"text" | "voice">("text");
|
|
||||||
const [categoryId, setCategoryId] = useState("");
|
|
||||||
|
|
||||||
// You'd need to get categories for the current instance
|
|
||||||
// This is a simplified version
|
|
||||||
const categories: Category[] = []; // Get from context or props
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (name.trim() && categoryId) {
|
|
||||||
createChannel(
|
|
||||||
{
|
|
||||||
name: name.trim(),
|
|
||||||
description: description.trim(),
|
|
||||||
type,
|
|
||||||
categoryId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
setName("");
|
|
||||||
setDescription("");
|
|
||||||
setType("text");
|
|
||||||
setCategoryId("");
|
|
||||||
closeCreateChannel();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error("Failed to create channel:", error);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setName("");
|
|
||||||
setDescription("");
|
|
||||||
setType("text");
|
|
||||||
setCategoryId("");
|
|
||||||
closeCreateChannel();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={showCreateChannel} onOpenChange={handleClose}>
|
|
||||||
<DialogContent className="sm:max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Create a Channel</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Create a new text or voice channel in this server.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="channel-type">Channel Type</Label>
|
|
||||||
<Select
|
|
||||||
value={type}
|
|
||||||
onValueChange={(value: "text" | "voice") => setType(value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select channel type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="text">Text Channel</SelectItem>
|
|
||||||
<SelectItem value="voice">Voice Channel</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="channel-category">Category</Label>
|
|
||||||
<Select value={categoryId} onValueChange={setCategoryId}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a category" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{categories.map((category) => (
|
|
||||||
<SelectItem key={category.id} value={category.id}>
|
|
||||||
{category.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="channel-name">Channel Name</Label>
|
|
||||||
<Input
|
|
||||||
id="channel-name"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
placeholder="Enter channel name"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="channel-description">Description (Optional)</Label>
|
|
||||||
<Textarea
|
|
||||||
id="channel-description"
|
|
||||||
value={description}
|
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
|
||||||
placeholder="What's this channel for?"
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button type="button" variant="outline" onClick={handleClose}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isPending || !name.trim()}>
|
|
||||||
{isPending ? "Creating..." : "Create Channel"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create Category Modal
|
|
||||||
export const CreateCategoryModal: React.FC = () => {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const { mutate: createCategory, isPending } = useCreateCategory();
|
|
||||||
|
|
||||||
const [name, setName] = useState("");
|
|
||||||
const [instanceId, setInstanceId] = useState("");
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (name.trim() && instanceId) {
|
|
||||||
createCategory(
|
|
||||||
{
|
|
||||||
name: name.trim(),
|
|
||||||
instanceId,
|
|
||||||
position: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
setName("");
|
|
||||||
setInstanceId("");
|
|
||||||
setOpen(false);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error("Failed to create category:", error);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setName("");
|
|
||||||
setInstanceId("");
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<DialogContent className="sm:max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Create a Category</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Create a new category to organize your channels.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="category-name">Category Name</Label>
|
|
||||||
<Input
|
|
||||||
id="category-name"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
placeholder="Enter category name"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button type="button" variant="outline" onClick={handleClose}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isPending || !name.trim()}>
|
|
||||||
{isPending ? "Creating..." : "Create Category"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AdminModals: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<CreateServerModal />
|
|
||||||
<CreateChannelModal />
|
|
||||||
<CreateCategoryModal />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default ServerIcon;
|
export default ServerIcon;
|
||||||
|
|||||||
@@ -156,12 +156,6 @@ export const useEditMessage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Replace with actual API call when available
|
// TODO: Replace with actual API call when available
|
||||||
console.log(
|
|
||||||
"Editing message:",
|
|
||||||
data.messageId,
|
|
||||||
"New content:",
|
|
||||||
data.content,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -207,10 +201,6 @@ export const usePinMessage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Replace with actual API call when available
|
// TODO: Replace with actual API call when available
|
||||||
console.log(
|
|
||||||
`${data.pinned ? "Pinning" : "Unpinning"} message:`,
|
|
||||||
data.messageId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -256,6 +256,8 @@ export class ApiClient {
|
|||||||
requestingUserId: string;
|
requestingUserId: string;
|
||||||
requestingUserToken: string;
|
requestingUserToken: string;
|
||||||
}): Promise<Channel> {
|
}): Promise<Channel> {
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
return this.request<Channel>("/api/channel", {
|
return this.request<Channel>("/api/channel", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
|
|||||||
@@ -4,30 +4,10 @@ import App from "./App.tsx";
|
|||||||
import { io } from "socket.io-client";
|
import { io } from "socket.io-client";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
function printPayload(data: unknown) {
|
|
||||||
console.log(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
const socket = io("http://localhost:3000");
|
const socket = io("http://localhost:3000");
|
||||||
socket.on("connect", () => {
|
|
||||||
console.log("connected!");
|
|
||||||
socket.emit("ping", "world");
|
|
||||||
});
|
|
||||||
socket.on("pong", () => {
|
|
||||||
console.log("pong");
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("joined-voicechannel", printPayload);
|
|
||||||
socket.on("user-joined-voicechannel", printPayload);
|
|
||||||
socket.on("error-voicechannel", printPayload);
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App socket={socket} />
|
<App socket={socket} />
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use contextBridge
|
|
||||||
window.ipcRenderer.on("main-process-message", (_event, message) => {
|
|
||||||
console.log(message);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -76,6 +76,44 @@ const ChatPage: React.FC = () => {
|
|||||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
}, [channelMessages]);
|
}, [channelMessages]);
|
||||||
|
|
||||||
|
// Auto-focus on channel change or initial load (using DOM query)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentUser) return; // Skip if input isn't rendered
|
||||||
|
|
||||||
|
let retryCount = 0;
|
||||||
|
const maxRetries = 10;
|
||||||
|
const retryInterval = 100; // ms
|
||||||
|
|
||||||
|
const focusInput = () => {
|
||||||
|
retryCount++;
|
||||||
|
const textarea = document.getElementById(
|
||||||
|
"message-input-textarea",
|
||||||
|
) as HTMLTextAreaElement | null;
|
||||||
|
if (textarea) {
|
||||||
|
textarea.focus();
|
||||||
|
} else if (retryCount < maxRetries) {
|
||||||
|
setTimeout(focusInput, retryInterval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
focusInput();
|
||||||
|
}, [channelId, currentUser]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!replyingTo) return; // Skip if no reply
|
||||||
|
|
||||||
|
const focusInput = () => {
|
||||||
|
const textarea = document.getElementById(
|
||||||
|
"message-input-textarea",
|
||||||
|
) as HTMLTextAreaElement | null;
|
||||||
|
if (textarea) {
|
||||||
|
textarea.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
focusInput();
|
||||||
|
}, [replyingTo]);
|
||||||
|
|
||||||
// Event handlers
|
// Event handlers
|
||||||
const handleLoadMore = React.useCallback(async () => {
|
const handleLoadMore = React.useCallback(async () => {
|
||||||
if (!channelMessages || channelMessages.length === 0 || isLoadingMore)
|
if (!channelMessages || channelMessages.length === 0 || isLoadingMore)
|
||||||
@@ -214,10 +252,10 @@ const ChatPage: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-shrink h-full bg-concord-primary">
|
<div className="flex flex-col flex-shrink h-full bg-concord-primary">
|
||||||
{/* Channel Header */}
|
{/* Channel Header */}
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-concord bg-concord-secondary">
|
<div className="flex items-center justify-between px-6 py-4 border-b border-concord bg-concord-secondary">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<ChannelIcon size={20} className="text-concord-secondary" />
|
<ChannelIcon size={20} className="text-concord-secondary" />
|
||||||
<span className="font-semibold text-concord-primary">
|
<span className="font-semibold text-xl text-concord-primary">
|
||||||
{currentChannel?.name}
|
{currentChannel?.name}
|
||||||
</span>
|
</span>
|
||||||
{currentChannel?.description && (
|
{currentChannel?.description && (
|
||||||
@@ -275,7 +313,6 @@ const ChatPage: React.FC = () => {
|
|||||||
{sortedMessages && sortedMessages.length > 0 ? (
|
{sortedMessages && sortedMessages.length > 0 ? (
|
||||||
<div>
|
<div>
|
||||||
{sortedMessages.map((message) => {
|
{sortedMessages.map((message) => {
|
||||||
console.log(message);
|
|
||||||
const user = users?.find((u) => u.id === message.userId);
|
const user = users?.find((u) => u.id === message.userId);
|
||||||
const replyToMessage = channelMessages?.find(
|
const replyToMessage = channelMessages?.find(
|
||||||
(m) => m.id === message.replies?.repliesToId,
|
(m) => m.id === message.replies?.repliesToId,
|
||||||
|
|||||||
@@ -72,16 +72,6 @@ const AccountSettings: React.FC = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: Implement actual profile update API call
|
// TODO: Implement actual profile update API call
|
||||||
// await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate API call
|
|
||||||
|
|
||||||
// console.log("Updating profile:", { username, nickname, bio });
|
|
||||||
// const updatedUser = await userClient.updateProfile({
|
|
||||||
// userId: user.id,
|
|
||||||
// username: username.trim(),
|
|
||||||
// nickname: nickname.trim() || null,
|
|
||||||
// bio: bio.trim() || null,
|
|
||||||
// token: authStore.token
|
|
||||||
// });
|
|
||||||
|
|
||||||
// Update local state
|
// Update local state
|
||||||
// updateUser({
|
// updateUser({
|
||||||
@@ -595,7 +585,7 @@ const SettingsPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<ScrollArea className="min-h-0 w-full">
|
<ScrollArea className="min-h-0 w-full bg-concord-primary h-full">
|
||||||
<div className="p-6 flex w-full">{renderSettingsContent()}</div>
|
<div className="p-6 flex w-full">{renderSettingsContent()}</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ export const useVoiceStore = create<VoiceState & VoiceActions>((set, get) => {
|
|||||||
* @returns The configured RTCPeerConnection instance.
|
* @returns The configured RTCPeerConnection instance.
|
||||||
*/
|
*/
|
||||||
const createPeerConnection = (targetUserId: string): RTCPeerConnection => {
|
const createPeerConnection = (targetUserId: string): RTCPeerConnection => {
|
||||||
console.log(`Creating peer connection for: ${targetUserId}`);
|
|
||||||
const { iceServers, localStream, socket, peerConnections } = get();
|
const { iceServers, localStream, socket, peerConnections } = get();
|
||||||
|
|
||||||
const peerConnection = new RTCPeerConnection({ iceServers });
|
const peerConnection = new RTCPeerConnection({ iceServers });
|
||||||
@@ -95,7 +94,6 @@ export const useVoiceStore = create<VoiceState & VoiceActions>((set, get) => {
|
|||||||
|
|
||||||
// Handle incoming remote tracks
|
// Handle incoming remote tracks
|
||||||
peerConnection.ontrack = (event) => {
|
peerConnection.ontrack = (event) => {
|
||||||
console.log(`Received remote track from: ${targetUserId}`);
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newStreams = new Map(state.remoteStreams);
|
const newStreams = new Map(state.remoteStreams);
|
||||||
newStreams.set(targetUserId, event.streams[0]);
|
newStreams.set(targetUserId, event.streams[0]);
|
||||||
@@ -105,9 +103,6 @@ export const useVoiceStore = create<VoiceState & VoiceActions>((set, get) => {
|
|||||||
|
|
||||||
// For debugging connection state
|
// For debugging connection state
|
||||||
peerConnection.onconnectionstatechange = () => {
|
peerConnection.onconnectionstatechange = () => {
|
||||||
console.log(
|
|
||||||
`Connection state change for ${targetUserId}: ${peerConnection.connectionState}`,
|
|
||||||
);
|
|
||||||
if (
|
if (
|
||||||
peerConnection.connectionState === "disconnected" ||
|
peerConnection.connectionState === "disconnected" ||
|
||||||
peerConnection.connectionState === "failed"
|
peerConnection.connectionState === "failed"
|
||||||
@@ -128,10 +123,6 @@ export const useVoiceStore = create<VoiceState & VoiceActions>((set, get) => {
|
|||||||
connectedUserIds: string[];
|
connectedUserIds: string[];
|
||||||
iceServers: IceServerConfig[];
|
iceServers: IceServerConfig[];
|
||||||
}) => {
|
}) => {
|
||||||
console.log(
|
|
||||||
"Successfully joined voice channel. Users:",
|
|
||||||
data.connectedUserIds,
|
|
||||||
);
|
|
||||||
set({
|
set({
|
||||||
iceServers: data.iceServers,
|
iceServers: data.iceServers,
|
||||||
isConnecting: false,
|
isConnecting: false,
|
||||||
@@ -150,7 +141,6 @@ export const useVoiceStore = create<VoiceState & VoiceActions>((set, get) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onUserLeft = (data: { userId: string }) => {
|
const onUserLeft = (data: { userId: string }) => {
|
||||||
console.log(`User ${data.userId} left the channel.`);
|
|
||||||
cleanupPeerConnection(data.userId);
|
cleanupPeerConnection(data.userId);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -158,7 +148,6 @@ export const useVoiceStore = create<VoiceState & VoiceActions>((set, get) => {
|
|||||||
senderUserId: string;
|
senderUserId: string;
|
||||||
sdp: RTCSessionDescriptionInit;
|
sdp: RTCSessionDescriptionInit;
|
||||||
}) => {
|
}) => {
|
||||||
console.log("Received WebRTC offer from:", data.senderUserId);
|
|
||||||
const peerConnection = createPeerConnection(data.senderUserId);
|
const peerConnection = createPeerConnection(data.senderUserId);
|
||||||
await peerConnection.setRemoteDescription(
|
await peerConnection.setRemoteDescription(
|
||||||
new RTCSessionDescription(data.sdp),
|
new RTCSessionDescription(data.sdp),
|
||||||
@@ -175,7 +164,6 @@ export const useVoiceStore = create<VoiceState & VoiceActions>((set, get) => {
|
|||||||
senderUserId: string;
|
senderUserId: string;
|
||||||
sdp: RTCSessionDescriptionInit;
|
sdp: RTCSessionDescriptionInit;
|
||||||
}) => {
|
}) => {
|
||||||
console.log("Received WebRTC answer from:", data.senderUserId);
|
|
||||||
const peerConnection = get().peerConnections.get(data.senderUserId);
|
const peerConnection = get().peerConnections.get(data.senderUserId);
|
||||||
if (peerConnection) {
|
if (peerConnection) {
|
||||||
await peerConnection.setRemoteDescription(
|
await peerConnection.setRemoteDescription(
|
||||||
@@ -271,7 +259,6 @@ export const useVoiceStore = create<VoiceState & VoiceActions>((set, get) => {
|
|||||||
get();
|
get();
|
||||||
if (!socket || !activeVoiceChannelId) return;
|
if (!socket || !activeVoiceChannelId) return;
|
||||||
|
|
||||||
console.log(`Leaving voice channel: ${activeVoiceChannelId}`);
|
|
||||||
socket.emit("leave-voicechannel", { channelId: activeVoiceChannelId });
|
socket.emit("leave-voicechannel", { channelId: activeVoiceChannelId });
|
||||||
|
|
||||||
// Clean up all event listeners
|
// Clean up all event listeners
|
||||||
|
|||||||
Reference in New Issue
Block a user