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:
2025-10-07 15:55:24 -04:00
parent 24a99900b1
commit 2f91713c11
17 changed files with 190 additions and 509 deletions

View File

@@ -38,11 +38,6 @@ const ChannelItem: React.FC<ChannelItemProps> = ({ channel }) => {
if (isConnectedToThisChannel) {
leaveChannel();
} else if (currentUser && token) {
console.log({
channelId: channel.id,
currentUser: currentUser.id,
token: token,
});
joinChannel(channel.id, currentUser.id, token);
}
}

View File

@@ -18,7 +18,7 @@ const CategoryHeader: React.FC<CategoryHeaderProps> = ({
return (
<Button
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={() => {
onToggle();
}}

View File

@@ -2,6 +2,7 @@ import { Component, ErrorInfo, ReactNode } from "react";
import { AlertTriangle, RotateCcw } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Card } from "../ui/card";
interface Props {
children: ReactNode;
@@ -48,8 +49,8 @@ class ErrorBoundary extends Component<Props, State> {
}
return (
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
<div className="max-w-md w-full space-y-4">
<div className="min-h-screen bg-concord-primary flex items-center justify-center p-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">
<AlertTriangle className="h-4 w-4 text-red-500" />
<AlertTitle className="text-red-400">
@@ -82,7 +83,7 @@ class ErrorBoundary extends Component<Props, State> {
{/* Error details in development */}
{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">
Error Details (Development)
</summary>
@@ -107,7 +108,7 @@ class ErrorBoundary extends Component<Props, State> {
</div>
</details>
)}
</div>
</Card>
</div>
);
}

View File

@@ -39,7 +39,7 @@ const AppLayout: React.FC = () => {
<VoiceConnectionManager />
{/* 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 />
</div>
{/* Channel Sidebar - Only shown when in a server context and not collapsed */}
@@ -68,7 +68,7 @@ const AppLayout: React.FC = () => {
</div>
{/* Member List - Only shown when in a channel and member list is enabled */}
{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 />
</div>
)}

View File

@@ -1,12 +1,12 @@
import React from "react";
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 { ScrollArea } from "@/components/ui/scroll-area";
import { useInstanceDetails } from "@/hooks/useServers";
import { useUiStore } from "@/stores/uiStore";
import ChannelList from "@/components/channel/ChannelList";
import { CreateCategoryModal, CreateChannelModal } from "../server/ServerIcon";
import { CreateChannelModal } from "@/components/modals/CreateChannelModal";
const ChannelSidebar: React.FC = () => {
const { instanceId } = useParams();
@@ -14,8 +14,8 @@ const ChannelSidebar: React.FC = () => {
useInstanceDetails(instanceId);
const categories = instance?.categories;
const {
toggleMemberList,
showMemberList,
showCreateChannel,
closeCreateChannel,
openCreateChannel,
openServerSettings,
} = useUiStore();
@@ -45,11 +45,11 @@ const ChannelSidebar: React.FC = () => {
<div className="sidebar-secondary flex-1">
<ScrollArea className="">
{/* 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">
<Button
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}
>
<span className="truncate">{instance.name}</span>
@@ -83,20 +83,15 @@ const ChannelSidebar: React.FC = () => {
<Plus size={16} className="mr-1" />
Add Channel
</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>
</ScrollArea>
<CreateChannelModal />
<CreateCategoryModal />
<CreateChannelModal
isOpen={showCreateChannel}
onClose={closeCreateChannel}
categories={categories}
instanceId={instance.id}
/>
</div>
);
};

View File

@@ -206,16 +206,16 @@ const MemberList: React.FC = () => {
);
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 */}
<div className="px-4 py-3 border-b border-concord-primary flex items-center justify-between">
<div className="flex items-center gap-2">
<UserIcon size={20} className="text-concord-primary" />
<p className="text-sm font-semibold text-concord-primary tracking-wide">
<div className="px-6 py-4 pb-5 border-b border-concord flex items-center justify-between">
<div className="flex items-center space-x-2">
<UserIcon size={20} className="h-5 w-5 text-concord-secondary" />
<p className="font-semibold text-xl text-concord-primary tracking-wide">
Members
</p>
</div>
<p className="text-sm font-medium text-concord-secondary tracking-wide">
<p className="font-medium text-concord-secondary tracking-wide">
{members.length}
</p>
</div>

View File

@@ -51,17 +51,17 @@ const ServerSidebar: React.FC = () => {
return (
<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 */}
<Tooltip key={"home-server"}>
<TooltipTrigger asChild>
<Button
variant="ghost"
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"
? "bg-primary text-primary-foreground"
: "hover:bg-primary/10"
? "rounded-xl border-primary bg-primary/10 border-2"
: "rounded-2xl hover:rounded-xl border hover:border-primary/50"
}`}
onClick={handleHomeClick}
>
@@ -72,10 +72,8 @@ const ServerSidebar: React.FC = () => {
<p>{isGlobalAdmin(currentUser) ? "Admin Dashboard" : "Home"}</p>
</TooltipContent>
</Tooltip>
{/* 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 */}
<div className="flex-1 flex flex-col overflow-y-auto scrollbar-thin scrollbar-thumb-border space-y-2">
{isLoading ? (
@@ -116,26 +114,25 @@ const ServerSidebar: React.FC = () => {
)}
</div>
) : 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>
{/* 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>
</TooltipProvider>
);

View File

@@ -133,7 +133,7 @@ const UserPanel: React.FC = () => {
useVoiceStore();
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 */}
<UserAvatar user={user} size="md" />
<div className="ml-2 flex-1 min-w-0 text-left">

View File

@@ -1,9 +1,8 @@
// src/components/message/MessageInput.tsx
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 { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
interface MessageUser {
id: string;
@@ -27,24 +26,21 @@ export const MessageInput: React.FC<MessageInputProps> = ({
replyingToUser,
}) => {
const [content, setContent] = useState("");
const textareaRef = useRef<HTMLTextAreaElement>(null);
const formRef = useRef<HTMLFormElement>(null);
// Use the API hook for sending messages
const sendMessageMutation = useSendMessage();
// Auto-resize textarea and focus when replying
// Auto-resize textarea (using direct DOM access as a fallback, no ref needed)
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
// Focus the input when a reply is initiated
if (replyingTo) {
textareaRef.current.focus();
}
const textarea = document.getElementById(
"message-input-textarea",
) as HTMLTextAreaElement | null;
if (textarea) {
textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight}px`;
}
}, [content, replyingTo]);
}, [content]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -71,7 +67,7 @@ export const MessageInput: React.FC<MessageInputProps> = ({
};
return (
<div className="px-4 pb-4">
<div className="px-2 pb-2">
{replyingTo && replyingToUser && (
<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">
@@ -97,26 +93,15 @@ export const MessageInput: React.FC<MessageInputProps> = ({
)}
<form ref={formRef} onSubmit={handleSubmit}>
<div className="relative">
<textarea
ref={textareaRef}
value={content}
onChange={(e) => setContent(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={`Message #${channelName || "channel"}`}
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"
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>
<Textarea
id="message-input-textarea" // Unique ID for DOM targeting
value={content}
onChange={(e) => setContent(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={`Message #${channelName || "channel"}`}
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"
/>
</form>
</div>
);

View File

@@ -11,39 +11,58 @@ import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Hash, Volume2, Loader2 } from "lucide-react";
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 {
isOpen: boolean;
onClose: () => void;
instanceId: string; // Changed to use instanceId instead of categories prop
defaultCategoryId?: string;
instanceId: string;
categories: CategoryWithChannels[] | undefined;
}
export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
isOpen,
onClose,
defaultCategoryId,
categories,
}) => {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [type, setType] = useState<"text" | "voice">("text");
const [categoryId, setCategoryId] = useState(defaultCategoryId || "");
const [categoryId, setCategoryId] = useState("");
const createChannelMutation = useCreateChannel();
// Reset form when modal opens/closes
// Reset form when modal opens or closes
useEffect(() => {
if (!isOpen) {
setName("");
setDescription("");
setType("text");
setCategoryId(defaultCategoryId || "");
setCategoryId("");
} else {
setCategoryId("");
}
}, [isOpen, defaultCategoryId]);
}, [isOpen]);
const handleSubmit = async (e: React.FormEvent) => {
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 {
await createChannelMutation.mutateAsync({
@@ -53,17 +72,25 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
categoryId,
});
// Reset form
// Reset form after successful creation
setName("");
setDescription("");
setType("text");
setCategoryId(defaultCategoryId || "");
setCategoryId(""); // Reset to default or empty
onClose();
} catch (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 (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[400px]">
@@ -72,12 +99,13 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Channel Type Selection */}
<div className="space-y-2">
<Label>Channel Type</Label>
<div className="flex gap-2">
<Button
type="button"
variant={type === "text" ? "default" : "outline"}
variant={type === "text" ? "secondary" : "ghost"}
onClick={() => setType("text")}
className="flex-1"
>
@@ -86,7 +114,7 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
</Button>
<Button
type="button"
variant={type === "voice" ? "default" : "outline"}
variant={type === "voice" ? "secondary" : "ghost"}
onClick={() => setType("voice")}
className="flex-1"
>
@@ -96,6 +124,7 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
</div>
</div>
{/* Channel Name Input */}
<div className="space-y-2">
<Label htmlFor="channel-name">Channel Name</Label>
<Input
@@ -107,6 +136,35 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
/>
</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">
<Label htmlFor="channel-description">Description</Label>
<Textarea
@@ -118,20 +176,12 @@ export const CreateChannelModal: React.FC<CreateChannelModalProps> = ({
/>
</div>
{/* Action Buttons */}
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={onClose}>
Cancel
</Button>
<Button
type="submit"
disabled={
!name.trim() ||
!categoryId ||
createChannelMutation.isPending ||
categoryId === "loading" ||
categoryId === "no-categories"
}
>
<Button type="submit" disabled={isFormInvalid}>
{createChannelMutation.isPending ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />

View File

@@ -1,30 +1,6 @@
import React, { useState } from "react";
import React from "react";
import { Button } from "@/components/ui/button";
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 {
server: Instance;
@@ -47,17 +23,17 @@ const ServerIcon: React.FC<ServerIconProps> = ({
};
return (
<div className="relative group">
{/* Active indicator */}
<div className="relative group w-12">
{/* Active indicator - Positioned outside to the left */}
<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"
}`}
/>
<Button
variant="ghost"
size="icon"
className={`w-12 h-12 ml-3 transition-all duration-200 ${
className={`w-12 h-12 transition-all duration-200 ${
isActive
? "rounded-xl border-primary bg-primary/10 border-2"
: "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;

View File

@@ -156,12 +156,6 @@ export const useEditMessage = () => {
}
// TODO: Replace with actual API call when available
console.log(
"Editing message:",
data.messageId,
"New content:",
data.content,
);
return {
success: true,
@@ -207,10 +201,6 @@ export const usePinMessage = () => {
}
// TODO: Replace with actual API call when available
console.log(
`${data.pinned ? "Pinning" : "Unpinning"} message:`,
data.messageId,
);
return {
success: true,

View File

@@ -256,6 +256,8 @@ export class ApiClient {
requestingUserId: string;
requestingUserToken: string;
}): Promise<Channel> {
console.log(data);
return this.request<Channel>("/api/channel", {
method: "POST",
body: JSON.stringify(data),

View File

@@ -4,30 +4,10 @@ import App from "./App.tsx";
import { io } from "socket.io-client";
import "./index.css";
function printPayload(data: unknown) {
console.log(data);
}
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(
<React.StrictMode>
<App socket={socket} />
</React.StrictMode>,
);
// Use contextBridge
window.ipcRenderer.on("main-process-message", (_event, message) => {
console.log(message);
});

View File

@@ -76,6 +76,44 @@ const ChatPage: React.FC = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [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
const handleLoadMore = React.useCallback(async () => {
if (!channelMessages || channelMessages.length === 0 || isLoadingMore)
@@ -214,10 +252,10 @@ const ChatPage: React.FC = () => {
return (
<div className="flex flex-col flex-shrink h-full bg-concord-primary">
{/* 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">
<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}
</span>
{currentChannel?.description && (
@@ -275,7 +313,6 @@ const ChatPage: React.FC = () => {
{sortedMessages && sortedMessages.length > 0 ? (
<div>
{sortedMessages.map((message) => {
console.log(message);
const user = users?.find((u) => u.id === message.userId);
const replyToMessage = channelMessages?.find(
(m) => m.id === message.replies?.repliesToId,

View File

@@ -72,16 +72,6 @@ const AccountSettings: React.FC = () => {
try {
// 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
// updateUser({
@@ -595,7 +585,7 @@ const SettingsPage: React.FC = () => {
</div>
{/* 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>
</ScrollArea>
</div>

View File

@@ -71,7 +71,6 @@ export const useVoiceStore = create<VoiceState & VoiceActions>((set, get) => {
* @returns The configured RTCPeerConnection instance.
*/
const createPeerConnection = (targetUserId: string): RTCPeerConnection => {
console.log(`Creating peer connection for: ${targetUserId}`);
const { iceServers, localStream, socket, peerConnections } = get();
const peerConnection = new RTCPeerConnection({ iceServers });
@@ -95,7 +94,6 @@ export const useVoiceStore = create<VoiceState & VoiceActions>((set, get) => {
// Handle incoming remote tracks
peerConnection.ontrack = (event) => {
console.log(`Received remote track from: ${targetUserId}`);
set((state) => {
const newStreams = new Map(state.remoteStreams);
newStreams.set(targetUserId, event.streams[0]);
@@ -105,9 +103,6 @@ export const useVoiceStore = create<VoiceState & VoiceActions>((set, get) => {
// For debugging connection state
peerConnection.onconnectionstatechange = () => {
console.log(
`Connection state change for ${targetUserId}: ${peerConnection.connectionState}`,
);
if (
peerConnection.connectionState === "disconnected" ||
peerConnection.connectionState === "failed"
@@ -128,10 +123,6 @@ export const useVoiceStore = create<VoiceState & VoiceActions>((set, get) => {
connectedUserIds: string[];
iceServers: IceServerConfig[];
}) => {
console.log(
"Successfully joined voice channel. Users:",
data.connectedUserIds,
);
set({
iceServers: data.iceServers,
isConnecting: false,
@@ -150,7 +141,6 @@ export const useVoiceStore = create<VoiceState & VoiceActions>((set, get) => {
};
const onUserLeft = (data: { userId: string }) => {
console.log(`User ${data.userId} left the channel.`);
cleanupPeerConnection(data.userId);
};
@@ -158,7 +148,6 @@ export const useVoiceStore = create<VoiceState & VoiceActions>((set, get) => {
senderUserId: string;
sdp: RTCSessionDescriptionInit;
}) => {
console.log("Received WebRTC offer from:", data.senderUserId);
const peerConnection = createPeerConnection(data.senderUserId);
await peerConnection.setRemoteDescription(
new RTCSessionDescription(data.sdp),
@@ -175,7 +164,6 @@ export const useVoiceStore = create<VoiceState & VoiceActions>((set, get) => {
senderUserId: string;
sdp: RTCSessionDescriptionInit;
}) => {
console.log("Received WebRTC answer from:", data.senderUserId);
const peerConnection = get().peerConnections.get(data.senderUserId);
if (peerConnection) {
await peerConnection.setRemoteDescription(
@@ -271,7 +259,6 @@ export const useVoiceStore = create<VoiceState & VoiceActions>((set, get) => {
get();
if (!socket || !activeVoiceChannelId) return;
console.log(`Leaving voice channel: ${activeVoiceChannelId}`);
socket.emit("leave-voicechannel", { channelId: activeVoiceChannelId });
// Clean up all event listeners