refactor(channels): cap name length at 32 chars and fix sidebar truncation layout

This commit is contained in:
Chneemann 2026-08-31 12:08:16 +02:00
parent 2325c5725e
commit be4e10893e
No known key found for this signature in database
6 changed files with 36 additions and 20 deletions

View file

@ -32,13 +32,21 @@ export async function PATCH(
return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
} }
if (!name || !name.trim()) { if (!name) {
return NextResponse.json( return NextResponse.json(
{ error: "Channel name cannot be empty" }, { error: "Channel name cannot be empty" },
{ status: 400 }, { status: 400 },
); );
} }
// Validate maximum length for channel name
if (name.length > 32) {
return NextResponse.json(
{ error: "Channel name cannot exceed 32 characters" },
{ status: 400 },
);
}
const [existingChannel] = await db const [existingChannel] = await db
.select() .select()
.from(channels) .from(channels)

View file

@ -27,13 +27,21 @@ export async function POST(req: Request) {
const { name, serverId } = await req.json(); const { name, serverId } = await req.json();
if (!name?.trim() || !serverId) { if (!name || !serverId) {
return NextResponse.json( return NextResponse.json(
{ error: "Name and server ID are required" }, { error: "Name and server ID are required" },
{ status: 400 }, { status: 400 },
); );
} }
// Validate maximum length for channel name
if (name.length > 32) {
return NextResponse.json(
{ error: "Channel name cannot exceed 32 characters" },
{ status: 400 },
);
}
// Check if the user is a member of the server // Check if the user is a member of the server
const [member] = await db const [member] = await db
.select() .select()

View file

@ -124,6 +124,7 @@ export function CreateChannelModal({
type="text" type="text"
required required
value={name} value={name}
maxLength={32}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
placeholder="new-channel" placeholder="new-channel"
disabled={isLoading} disabled={isLoading}

View file

@ -82,16 +82,15 @@ export function EditChannelModal({
}); });
if (!response.ok) { if (!response.ok) {
throw new Error("Fehler beim Aktualisieren des Channels."); throw new Error("Error updating the channel.");
} }
const updated = await response.json(); const updated = await response.json();
updateChannel(updated); updateChannel(updated);
router.refresh();
onClose(); onClose();
} catch (err: unknown) { } catch (err: unknown) {
setError( setError(err instanceof Error ? err.message : "Something went wrong.");
err instanceof Error ? err.message : "Etwas ist schiefgelaufen.",
);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -116,7 +115,7 @@ export function EditChannelModal({
}); });
if (!response.ok) { if (!response.ok) {
throw new Error("Fehler beim Löschen des Channels."); throw new Error("Error deleting the channel.");
} }
removeChannel(channel.id); removeChannel(channel.id);
@ -135,9 +134,7 @@ export function EditChannelModal({
} }
} }
} catch (err: unknown) { } catch (err: unknown) {
setError( setError(err instanceof Error ? err.message : "Something went wrong.");
err instanceof Error ? err.message : "Etwas ist schiefgelaufen.",
);
} finally { } finally {
setIsDeleting(false); setIsDeleting(false);
} }
@ -179,6 +176,7 @@ export function EditChannelModal({
type="text" type="text"
required required
value={name} value={name}
maxLength={32}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
placeholder="channel-name" placeholder="channel-name"
disabled={isLoading || isDeleting} disabled={isLoading || isDeleting}

View file

@ -62,22 +62,22 @@ export function ChannelSidebar() {
return ( return (
<> <>
<div className="flex-1 w-full md:w-72 bg-surface/50 border-r border-background flex flex-col h-full shrink-0"> <div className="flex-1 w-full bg-surface/50 border-r border-background flex flex-col h-full min-w-0 overflow-hidden">
{/* Server Header */} {/* Server Header */}
<div className="h-14 border-b border-background flex items-center justify-between px-4 font-bold text-white shadow-sm"> <div className="h-14 border-b border-background flex items-center justify-between px-4 font-bold text-white shadow-sm shrink-0">
<span className="truncate">{activeServer.name}</span> <span className="truncate">{activeServer.name}</span>
<button <button
type="button" type="button"
onClick={toggleNav} onClick={toggleNav}
title="Collapse the sidebar" title="Collapse the sidebar"
className="p-1.5 rounded-md text-muted hover:text-white hover:bg-surface transition-colors cursor-pointer" className="p-1.5 rounded-md text-muted hover:text-white hover:bg-surface transition-colors cursor-pointer shrink-0"
> >
<PanelLeftClose className="w-5 h-5" /> <PanelLeftClose className="w-5 h-5" />
</button> </button>
</div> </div>
{/* Channel List */} {/* Channel List */}
<div className="flex-1 overflow-y-auto p-3 space-y-1"> <div className="flex-1 overflow-y-auto p-3 space-y-1 min-w-0">
<div className="flex items-center justify-between text-xs font-semibold text-muted px-2 py-1 uppercase tracking-wider"> <div className="flex items-center justify-between text-xs font-semibold text-muted px-2 py-1 uppercase tracking-wider">
<span>Text Channels</span> <span>Text Channels</span>
<button <button
@ -90,7 +90,7 @@ export function ChannelSidebar() {
</button> </button>
</div> </div>
<div className="space-y-0.5"> <div className="space-y-0.5 w-full">
{activeServer.channels.map((channel) => { {activeServer.channels.map((channel) => {
const isActive = currentChannelId === channel.id; const isActive = currentChannelId === channel.id;
@ -100,14 +100,15 @@ export function ChannelSidebar() {
href={`/servers/${activeServer.id}/channels/${channel.id}`} href={`/servers/${activeServer.id}/channels/${channel.id}`}
onClick={handleChannelClick} onClick={handleChannelClick}
prefetch={false} prefetch={false}
className={`flex items-center justify-between px-2 py-1.5 rounded-md text-sm transition-all group ${ className={`flex items-center justify-between w-full px-2 py-1.5 rounded-md text-sm transition-all group min-w-0 overflow-hidden ${
isActive isActive
? "bg-accent/50 text-white font-medium" ? "bg-accent/50 text-white font-medium"
: "text-muted hover:bg-surface hover:text-white" : "text-muted hover:bg-surface hover:text-white"
}`} }`}
> >
<div className="flex items-center gap-2 min-w-0"> {/* Text Area */}
<span className="text-muted group-hover:text-white text-base"> <div className="flex items-center gap-2 min-w-0 flex-1 overflow-hidden">
<span className="text-muted group-hover:text-white text-base shrink-0">
# #
</span> </span>
<span className="truncate">{channel.name}</span> <span className="truncate">{channel.name}</span>
@ -118,7 +119,7 @@ export function ChannelSidebar() {
<button <button
type="button" type="button"
onClick={(e) => handleOpenSettings(e, channel)} onClick={(e) => handleOpenSettings(e, channel)}
className="opacity-0 group-hover:opacity-100 p-1 text-muted hover:text-white focus:outline-none transition-all cursor-pointer" className="opacity-0 group-hover:opacity-100 p-1 text-muted hover:text-white focus:outline-none transition-all cursor-pointer shrink-0 ml-2"
aria-label="Channel Settings" aria-label="Channel Settings"
title="Channel Settings" title="Channel Settings"
> >

View file

@ -85,7 +85,7 @@ export const members = pgTable("members", {
*/ */
export const channels = pgTable("channels", { export const channels = pgTable("channels", {
id: uuid("id").primaryKey().defaultRandom(), id: uuid("id").primaryKey().defaultRandom(),
name: text("name").notNull(), name: varchar("name", { length: 32 }).notNull(),
serverId: uuid("server_id") serverId: uuid("server_id")
.references(() => servers.id, { onDelete: "cascade" }) .references(() => servers.id, { onDelete: "cascade" })
.notNull(), .notNull(),