refactor(settings): sync status and color updates directly via UserContext
Some checks failed
Deploy Waveform to VPS / deploy (push) Failing after 14s
Some checks failed
Deploy Waveform to VPS / deploy (push) Failing after 14s
This commit is contained in:
parent
00b11f2113
commit
256c3d03e3
6 changed files with 78 additions and 108 deletions
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue