refactor(servers): integrate responsive color picker grid into EditServerModal
All checks were successful
Deploy Waveform to VPS / deploy (push) Successful in 1m20s

This commit is contained in:
Chneemann 2026-10-02 04:14:47 +02:00
parent dffcfca0ad
commit e150c819ca
No known key found for this signature in database
5 changed files with 82 additions and 49 deletions

View file

@ -48,7 +48,7 @@ export default async function ChannelPage({
<AppHeader <AppHeader
title={channel.name} title={channel.name}
showMembersButton showMembersButton
server={{ id: server.id, name: server.name }} server={{ id: server.id, name: server.name, color: server.color }}
/> />
<ChatMessages <ChatMessages

View file

@ -10,16 +10,7 @@ import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
/** /** Handles PATCH requests to update an existing server's name. */
* Handles PATCH requests to update an existing server's name.
*
* @async
* @function PATCH
* @param {Request} req - The incoming HTTP request object containing the updated server name in the body.
* @param {Object} context - The route context parameters.
* @param {Promise<{ serverId: string }>} context.params - A promise resolving to the route parameters containing the server ID.
* @returns {Promise<NextResponse>} A JSON response containing the updated server object or an error message.
*/
export async function PATCH( export async function PATCH(
req: Request, req: Request,
{ params }: { params: Promise<{ serverId: string }> }, { params }: { params: Promise<{ serverId: string }> },
@ -34,6 +25,7 @@ export async function PATCH(
const body = await req.json().catch(() => null); const body = await req.json().catch(() => null);
const name = body?.name; const name = body?.name;
const color = body?.color;
// Ensure name is present, valid string type, and not just whitespace // Ensure name is present, valid string type, and not just whitespace
if (!name || typeof name !== "string" || !name.trim()) { if (!name || typeof name !== "string" || !name.trim()) {
@ -53,6 +45,14 @@ export async function PATCH(
); );
} }
// Validate color if provided
if (color !== undefined && (typeof color !== "string" || !color.trim())) {
return NextResponse.json(
{ error: "Invalid color value provided." },
{ status: 400 },
);
}
// Check if the server exists // Check if the server exists
const [existingServer] = await db const [existingServer] = await db
.select() .select()
@ -72,10 +72,19 @@ export async function PATCH(
); );
} }
// Prepare fields to update
const updateData: { name: string; color?: string } = {
name: trimmedName,
};
if (color && typeof color === "string") {
updateData.color = color.trim();
}
// Update Server // Update Server
const [updatedServer] = await db const [updatedServer] = await db
.update(servers) .update(servers)
.set({ name: trimmedName }) .set(updateData)
.where(eq(servers.id, serverId)) .where(eq(servers.id, serverId))
.returning(); .returning();
@ -90,16 +99,7 @@ export async function PATCH(
} }
} }
/** /** Handles DELETE requests to remove an existing server. */
* Handles DELETE requests to remove an existing server.
*
* @async
* @function DELETE
* @param {Request} req - The incoming HTTP request object.
* @param {Object} context - The route context parameters.
* @param {Promise<{ serverId: string }>} context.params - A promise resolving to the route parameters containing the server ID.
* @returns {Promise<NextResponse>} A JSON response confirming deletion or returning an error message.
*/
export async function DELETE( export async function DELETE(
req: Request, req: Request,
{ params }: { params: Promise<{ serverId: string }> }, { params }: { params: Promise<{ serverId: string }> },

View file

@ -25,6 +25,7 @@ interface AppHeaderProps {
server?: { server?: {
id: string; id: string;
name: string; name: string;
color: string;
}; };
showFriendsTabs?: boolean; showFriendsTabs?: boolean;
activeTab?: TabType; activeTab?: TabType;
@ -140,7 +141,11 @@ export function AppHeader({
{/* Right Action Buttons */} {/* Right Action Buttons */}
<div className="flex items-center gap-1 shrink-0"> <div className="flex items-center gap-1 shrink-0">
{server && ( {server && (
<ServerSettingsMenu serverId={server.id} serverName={server.name} /> <ServerSettingsMenu
serverId={server.id}
serverName={server.name}
serverColor={server.color}
/>
)} )}
{showMembersButton && ( {showMembersButton && (

View file

@ -15,12 +15,14 @@ import { CreateChannelModal } from "@/components/modals/CreateChannelModal";
interface ServerSettingsMenuProps { interface ServerSettingsMenuProps {
serverId: string; serverId: string;
serverName: string; serverName: string;
serverColor: string;
} }
/** Renders a server management dropdown menu with modal triggers for channel, category, and server settings. */ /** Renders a server management dropdown menu with modal triggers for channel, category, and server settings. */
export function ServerSettingsMenu({ export function ServerSettingsMenu({
serverId, serverId,
serverName, serverName,
serverColor,
}: ServerSettingsMenuProps) { }: ServerSettingsMenuProps) {
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isEditServerOpen, setIsEditServerOpen] = useState(false); const [isEditServerOpen, setIsEditServerOpen] = useState(false);
@ -102,6 +104,7 @@ export function ServerSettingsMenu({
isOpen={isEditServerOpen} isOpen={isEditServerOpen}
serverId={serverId} serverId={serverId}
initialName={serverName} initialName={serverName}
initialColor={serverColor}
onClose={() => setIsEditServerOpen(false)} onClose={() => setIsEditServerOpen(false)}
/> />
<CreateCategoryModal <CreateCategoryModal

View file

@ -1,13 +1,17 @@
/** /**
* @file components/modals/EditServerModal.tsx * @file components/modals/EditServerModal.tsx
* @description Modal dialog to edit server settings or delete the server. * @description Modal dialog to edit server settings (name, color) or delete the server.
*/ */
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { X, Loader2, Trash2 } from "lucide-react"; import { X, Trash2, Check } from "lucide-react";
import {
SERVER_COLOR_CLASSES,
SERVER_COLOR_OPTIONS,
} from "@/lib/constants/server.styles";
import { ActionButton } from "../ui/ActionButton"; import { ActionButton } from "../ui/ActionButton";
/** /**
@ -18,31 +22,28 @@ import { ActionButton } from "../ui/ActionButton";
* @property {() => void} onClose - Callback function triggered to close the modal. * @property {() => void} onClose - Callback function triggered to close the modal.
* @property {string} serverId - The unique identifier of the server being edited. * @property {string} serverId - The unique identifier of the server being edited.
* @property {string} initialName - The current name of the server. * @property {string} initialName - The current name of the server.
* @property {string} [initialColor] - The current accent color of the server.
*/ */
interface EditServerModalProps { interface EditServerModalProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
serverId: string; serverId: string;
initialName: string; initialName: string;
initialColor: string;
} }
/** /**
* Renders a modal dialog allowing users to modify server properties or delete the server. * Renders a modal dialog allowing users to modify server properties or delete the server.
*
* @param {EditServerModalProps} props - The component props.
* @param {boolean} props.isOpen - Determines whether the modal dialog is currently visible.
* @param {() => void} props.onClose - Callback function triggered to close the modal.
* @param {string} props.serverId - The unique identifier of the server.
* @param {string} props.initialName - The current name of the server.
* @returns {JSX.Element | null} The rendered edit server modal, or null if closed.
*/ */
export function EditServerModal({ export function EditServerModal({
isOpen, isOpen,
onClose, onClose,
serverId, serverId,
initialName, initialName,
initialColor,
}: EditServerModalProps) { }: EditServerModalProps) {
const [name, setName] = useState(initialName); const [name, setName] = useState(initialName);
const [color, setColor] = useState(initialColor);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false); const [isConfirmingDelete, setIsConfirmingDelete] = useState(false);
@ -52,21 +53,17 @@ export function EditServerModal({
useEffect(() => { useEffect(() => {
setName(initialName); setName(initialName);
}, [initialName]); setColor(initialColor);
}, [initialName, initialColor]);
if (!isOpen) return null; if (!isOpen) return null;
const isChanged = name.trim() !== initialName; const isChanged = name.trim() !== initialName || color !== initialColor;
const isValid = name.trim().length > 0; const isValid = name.trim().length > 0;
const canSave = isChanged && isValid && !isLoading && !isDeleting; const canSave = isChanged && isValid && !isLoading && !isDeleting;
/** /**
* Handles the asynchronous update of the server name. * Handles the asynchronous update of the server name and accent color.
*
* @async
* @function handleUpdate
* @param {React.FormEvent} e - The form submission event.
* @returns {Promise<void>} Resolves when the server update process completes or fails.
*/ */
const handleUpdate = async (e: React.FormEvent) => { const handleUpdate = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@ -79,7 +76,7 @@ export function EditServerModal({
const response = await fetch(`/api/servers/${serverId}`, { const response = await fetch(`/api/servers/${serverId}`, {
method: "PATCH", method: "PATCH",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: name.trim() }), body: JSON.stringify({ name: name.trim(), color }),
}); });
if (!response.ok) { if (!response.ok) {
@ -96,11 +93,7 @@ export function EditServerModal({
}; };
/** /**
* Handles the asynchronous deletion of the server and redirects the user to the home page. * Handles the asynchronous deletion of the server.
*
* @async
* @function handleDelete
* @returns {Promise<void>} Resolves when the server deletion process completes or fails.
*/ */
const handleDelete = async () => { const handleDelete = async () => {
if (!isConfirmingDelete) { if (!isConfirmingDelete) {
@ -154,7 +147,7 @@ export function EditServerModal({
Change server details or delete this server. Change server details or delete this server.
</p> </p>
<form onSubmit={handleUpdate} className="space-y-4"> <form onSubmit={handleUpdate} className="space-y-5">
<div> <div>
<label className="block text-xs font-semibold text-muted uppercase tracking-wider mb-2"> <label className="block text-xs font-semibold text-muted uppercase tracking-wider mb-2">
Server Name Server Name
@ -172,9 +165,41 @@ export function EditServerModal({
/> />
</div> </div>
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-xs font-semibold text-muted uppercase tracking-wider">
Accent Color
</label>
</div>
<div className="grid grid-cols-8 gap-3 bg-background/50 border border-surface/80 p-3.5 rounded-2xl max-h-48 overflow-y-auto">
{SERVER_COLOR_OPTIONS.map((c) => {
const isSelected = color === c;
return (
<button
key={c}
type="button"
onClick={() => setColor(c)}
className={`group relative aspect-square rounded-full ${
SERVER_COLOR_CLASSES[c] || c
} flex items-center justify-center transition-all duration-150 cursor-pointer ${
isSelected
? "ring-2 ring-white ring-offset-2 ring-offset-surface scale-110 z-10 shadow-lg"
: "opacity-80 hover:opacity-100 hover:scale-105"
}`}
>
{isSelected && (
<Check className="w-3.5 h-3.5 text-white drop-shadow-md stroke-3" />
)}
</button>
);
})}
</div>
</div>
{error && <p className="text-xs text-red-400 mt-2">{error}</p>} {error && <p className="text-xs text-red-400 mt-2">{error}</p>}
<div className="flex items-center justify-between pt-4"> <div className="flex items-center justify-between pt-2">
<ActionButton <ActionButton
type="button" type="button"
variant="danger" variant="danger"
@ -183,7 +208,7 @@ export function EditServerModal({
disabled={isLoading} disabled={isLoading}
icon={Trash2} icon={Trash2}
> >
{isConfirmingDelete ? "Sure?" : "Delete Server "} {isConfirmingDelete ? "Sure?" : "Delete Server"}
</ActionButton> </ActionButton>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">