refactor(settings): sync status and color updates directly via UserContext
Some checks failed
Deploy Waveform to VPS / deploy (push) Failing after 14s

This commit is contained in:
Chneemann 2026-09-23 10:32:44 +02:00
parent 00b11f2113
commit 256c3d03e3
No known key found for this signature in database
6 changed files with 78 additions and 108 deletions

View file

@ -76,7 +76,6 @@ export default async function AppLayout({
<AppSidebar
servers={userServers}
conversations={formattedConversations}
user={currentUser}
/>
<div className="flex-1 flex min-w-0">{children}</div>
<MemberSidebar />

View file

@ -13,7 +13,6 @@ import {
useActiveServer,
type ServerWithChannels,
} from "@/lib/context/ServerContext";
import type { UserStatus } from "@/db/schema";
import { clsx } from "clsx";
import {
DirectMessageSidebar,
@ -21,26 +20,14 @@ import {
} from "../sidebar/DirectMessageSidebar";
import { usePathname } from "next/navigation";
/** Properties representing the user in the sidebar. */
export interface SidebarUser {
username: string;
color: string;
status: UserStatus;
}
/** Properties for the AppSidebar component. */
interface AppSidebarProps {
servers: ServerWithChannels[];
conversations: SidebarConversation[];
user: SidebarUser;
}
/**Renders the responsive application sidebar containing server navigation, channel lists, and user profile. */
export function AppSidebar({
servers,
user,
conversations = [],
}: AppSidebarProps) {
/** Renders the responsive application sidebar containing server navigation, channel lists, and user profile. */
export function AppSidebar({ servers, conversations = [] }: AppSidebarProps) {
const { isNavOpen, closeAll } = useSidebarStore();
const { activeServer } = useActiveServer();
const pathname = usePathname();
@ -66,7 +53,7 @@ export function AppSidebar({
)}
</div>
<div className="w-78">
<UserPanel user={user} />
<UserPanel />
</div>
</aside>
@ -92,7 +79,7 @@ export function AppSidebar({
<ChannelSidebar />
)}
</div>
<UserPanel user={user} />
<UserPanel />
</div>
</>
);

View file

@ -1,33 +1,22 @@
/**
* @file components/settings/SettingsView.tsx
* @description Main settings container component for managing user profile and preferences.
* @description Settings view component for updating user profile attributes like online status and avatar color.
*/
"use client";
import { useState } from "react";
import { UserAvatar } from "@/components/ui/UserAvatar";
import { UserStatus, User } from "@/db/schema";
import { UserStatus } from "@/db/schema";
import {
MEMBER_STATUS_COLOR_CLASSES,
MEMBER_COLOR_OPTIONS,
} from "@/lib/constants/member.styles";
import { UserRound, Palette } from "lucide-react";
import { useUser } from "@/lib/context/UserContext";
/** Props for the SettingsView component. */
interface SettingsViewProps {
userId: string;
currentUser: User;
}
/** Renders the user settings view for profile management, status selection, and avatar color customization. */
export function SettingsView({ userId, currentUser }: SettingsViewProps) {
const [selectedStatus, setSelectedStatus] = useState<UserStatus>(
currentUser.status,
);
const [selectedColor, setSelectedColor] = useState<string>(
currentUser.color || MEMBER_COLOR_OPTIONS[0],
);
/** Renders the user settings view allowing profile, online status, and color customizations. */
export function SettingsView() {
const { currentUser, updateCurrentUser } = useUser();
const statuses: { label: string; value: UserStatus }[] = [
{ label: "Online", value: "ONLINE" },
@ -36,42 +25,18 @@ export function SettingsView({ userId, currentUser }: SettingsViewProps) {
{ label: "DND", value: "DND" },
];
/** Updates local status state on user selection. */
const handleStatusChange = async (newStatus: UserStatus) => {
setSelectedStatus(newStatus);
try {
await fetch("/api/users/profile", {
/** Sends optimistic updates to context and persists profile changes to the server API. */
const updateProfile = (fields: Partial<typeof currentUser>) => {
updateCurrentUser(fields);
fetch("/api/users/profile", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status: newStatus }),
});
} catch (err) {
console.error("Failed to update status", err);
}
};
/** Updates local avatar color state on user selection. */
const handleColorChange = async (newColor: string) => {
setSelectedColor(newColor);
try {
await fetch("/api/users/profile", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ color: newColor }),
});
} catch (err) {
console.error("Failed to update color", err);
}
body: JSON.stringify(fields),
}).catch((err) => console.error("Failed to update profile", err));
};
if (!currentUser) return null;
const userWithUpdatedSettings = {
...currentUser,
status: selectedStatus,
color: selectedColor,
};
return (
<div className="flex flex-col h-full w-full bg-background text-white gap-6">
{/* Header */}
@ -85,7 +50,7 @@ export function SettingsView({ userId, currentUser }: SettingsViewProps) {
{/* Profile Overview Card */}
<div className="bg-surface border border-muted/20 rounded-xl p-6 space-y-6">
<div className="flex items-center gap-4">
<UserAvatar user={userWithUpdatedSettings} size="md" />
<UserAvatar user={currentUser} size="md" />
<div>
<h2 className="text-lg font-bold text-white">
{currentUser.username}
@ -101,14 +66,12 @@ export function SettingsView({ userId, currentUser }: SettingsViewProps) {
</h3>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{statuses.map((status) => {
const isSelected = selectedStatus === status.value;
const statusBgColor = MEMBER_STATUS_COLOR_CLASSES[status.value];
const isSelected = currentUser.status === status.value;
return (
<button
key={status.value}
type="button"
onClick={() => handleStatusChange(status.value)}
onClick={() => updateProfile({ status: status.value })}
className={`flex items-center gap-2.5 p-3 rounded-lg border text-sm font-medium transition-all ${
isSelected
? "border-accent bg-accent/10 text-white font-semibold"
@ -116,7 +79,9 @@ export function SettingsView({ userId, currentUser }: SettingsViewProps) {
}`}
>
<span
className={`w-2.5 h-2.5 rounded-full ${statusBgColor}`}
className={`w-2.5 h-2.5 rounded-full ${
MEMBER_STATUS_COLOR_CLASSES[status.value]
}`}
/>
{status.label}
</button>
@ -132,12 +97,12 @@ export function SettingsView({ userId, currentUser }: SettingsViewProps) {
</h3>
<div className="flex flex-wrap gap-3">
{MEMBER_COLOR_OPTIONS.map((colorClass) => {
const isSelected = selectedColor === colorClass;
const isSelected = currentUser.color === colorClass;
return (
<button
key={colorClass}
type="button"
onClick={() => handleColorChange(colorClass)}
onClick={() => updateProfile({ color: colorClass })}
className={`w-8 h-8 rounded-full ${colorClass} transition-all flex items-center justify-center ${
isSelected
? "ring-2 ring-white ring-offset-2 ring-offset-surface scale-110"

View file

@ -1,6 +1,6 @@
/**
* @file components/sidebar/UserPanel.tsx
* @description Client component providing a footer user panel with session information, avatar display, online status, and settings/logout actions.
* @file components/layout/UserPanel.tsx
* @description User panel component displaying the current user's avatar, status, settings link, and logout button.
*/
"use client";
@ -8,31 +8,14 @@
import { LogOut, Settings } from "lucide-react";
import { signOut } from "next-auth/react";
import { UserAvatar } from "@/components/ui/UserAvatar";
import { UserStatus } from "@/db/schema";
import { useUser } from "@/lib/context/UserContext";
import Link from "next/link";
/**
* Props for the UserPanel component.
*/
interface UserPanelProps {
/** User details including username, avatar color, and online status. */
user: {
username: string;
color: string;
status: UserStatus;
};
}
/** Renders the bottom user panel with user information and quick action controls. */
export function UserPanel() {
const { currentUser } = useUser();
/**
* Renders the user panel footer containing the user's avatar, status, and action buttons.
*
* @param {UserPanelProps} props - Component properties.
* @returns {JSX.Element} The rendered user panel component.
*/
export function UserPanel({ user }: UserPanelProps) {
/**
* Handles user logout by invalidating the local session and redirecting to the login page.
*/
/** Handles logging out the user and redirecting to the login page. */
const handleLogout = async () => {
await fetch("/api/auth/logout", { method: "POST" });
await signOut({ callbackUrl: "/login" });
@ -43,17 +26,17 @@ export function UserPanel({ user }: UserPanelProps) {
<footer className="h-14 bg-background/80 hover:bg-background/90 border border-background/50 rounded-xl flex items-center justify-between px-3 gap-2 shadow-lg backdrop-blur-md transition-all duration-200">
<div className="flex items-center gap-2.5 min-w-0 flex-1 cursor-pointer group">
<UserAvatar
user={user}
user={currentUser}
size="sm"
className="transition-transform group-hover:scale-105"
/>
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-foreground truncate leading-tight group-hover:text-accent transition-colors">
{user.username}
{currentUser.username}
</p>
<p className="text-xs text-muted truncate leading-tight font-medium capitalize">
{user.status.toLowerCase()}
{currentUser.status.toLowerCase()}
</p>
</div>
</div>

View file

@ -1,6 +1,6 @@
/**
* @file lib/context/ServerContext.tsx
* @description Context for managing active server state and members across sidebars and mobile drawers.
* @description Context for managing active server state, current user state, and members across sidebars and mobile drawers.
*/
"use client";
@ -31,6 +31,9 @@ export interface ServerMember {
interface ServerContextType {
activeServer: ServerWithChannels | null;
setActiveServer: (server: ServerWithChannels | null) => void;
currentUser: User | null;
setCurrentUser: (user: User | null) => void;
updateCurrentUser: (partialUser: Partial<User>) => void;
addChannel: (channel: Channel) => void;
removeChannel: (channelId: string) => void;
updateChannel: (channel: Channel) => void;
@ -43,11 +46,18 @@ interface ServerContextType {
const ServerContext = createContext<ServerContextType | null>(null);
/** Provides active server, channel, and member management state to child components. */
export function ServerProvider({ children }: { children: React.ReactNode }) {
/** Provides active server, channel, user, and member management state to child components. */
export function ServerProvider({
children,
initialUser = null,
}: {
children: React.ReactNode;
initialUser?: User | null;
}) {
const [activeServer, setActiveServer] = useState<ServerWithChannels | null>(
null,
);
const [currentUser, setCurrentUser] = useState<User | null>(initialUser);
const [members, setMembers] = useState<ServerMember[]>([]);
/** Updates the active server state using a partial updater function. */
@ -57,6 +67,19 @@ export function ServerProvider({ children }: { children: React.ReactNode }) {
setActiveServer((prev) => (prev ? { ...prev, ...fn(prev) } : null));
};
/** Updates the local current user state and reflects changes in active server members list. */
const updateCurrentUser = (partialUser: Partial<User>) => {
setCurrentUser((prev) => (prev ? { ...prev, ...partialUser } : null));
if (currentUser?.id) {
updateServer((s) => ({
members: s.members.map((m) =>
m.id === currentUser.id ? { ...m, ...partialUser } : m,
),
}));
}
};
/** Adds a new channel to the active server. */
const addChannel = (ch: Channel) =>
updateServer((s) => ({ channels: [...s.channels, ch] }));
@ -95,6 +118,9 @@ export function ServerProvider({ children }: { children: React.ReactNode }) {
value={{
activeServer,
setActiveServer,
currentUser,
setCurrentUser,
updateCurrentUser,
addChannel,
removeChannel,
updateChannel,

View file

@ -5,7 +5,7 @@
"use client";
import { createContext, useContext } from "react";
import { createContext, useContext, useState } from "react";
import type { User } from "@/db/schema";
/** Represents a friendship record between two users. */
@ -19,6 +19,7 @@ interface Friendship {
interface UserContextType {
currentUser: User;
friendships: Friendship[];
updateCurrentUser: (partialUser: Partial<User>) => void;
}
const UserContext = createContext<UserContextType | null>(null);
@ -26,15 +27,24 @@ const UserContext = createContext<UserContextType | null>(null);
/** Provides user data and friendship states to child components. */
export function UserProvider({
children,
currentUser,
currentUser: initialUser,
friendships,
}: {
children: React.ReactNode;
currentUser: User;
friendships: Friendship[];
}) {
const [currentUser, setCurrentUser] = useState<User>(initialUser);
/** Updates local current user state dynamically. */
const updateCurrentUser = (partialUser: Partial<User>) => {
setCurrentUser((prev) => ({ ...prev, ...partialUser }));
};
return (
<UserContext.Provider value={{ currentUser, friendships }}>
<UserContext.Provider
value={{ currentUser, friendships, updateCurrentUser }}
>
{children}
</UserContext.Provider>
);