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
|
<AppSidebar
|
||||||
servers={userServers}
|
servers={userServers}
|
||||||
conversations={formattedConversations}
|
conversations={formattedConversations}
|
||||||
user={currentUser}
|
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 flex min-w-0">{children}</div>
|
<div className="flex-1 flex min-w-0">{children}</div>
|
||||||
<MemberSidebar />
|
<MemberSidebar />
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import {
|
||||||
useActiveServer,
|
useActiveServer,
|
||||||
type ServerWithChannels,
|
type ServerWithChannels,
|
||||||
} from "@/lib/context/ServerContext";
|
} from "@/lib/context/ServerContext";
|
||||||
import type { UserStatus } from "@/db/schema";
|
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import {
|
import {
|
||||||
DirectMessageSidebar,
|
DirectMessageSidebar,
|
||||||
|
|
@ -21,26 +20,14 @@ import {
|
||||||
} from "../sidebar/DirectMessageSidebar";
|
} from "../sidebar/DirectMessageSidebar";
|
||||||
import { usePathname } from "next/navigation";
|
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. */
|
/** Properties for the AppSidebar component. */
|
||||||
interface AppSidebarProps {
|
interface AppSidebarProps {
|
||||||
servers: ServerWithChannels[];
|
servers: ServerWithChannels[];
|
||||||
conversations: SidebarConversation[];
|
conversations: SidebarConversation[];
|
||||||
user: SidebarUser;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**Renders the responsive application sidebar containing server navigation, channel lists, and user profile. */
|
/** Renders the responsive application sidebar containing server navigation, channel lists, and user profile. */
|
||||||
export function AppSidebar({
|
export function AppSidebar({ servers, conversations = [] }: AppSidebarProps) {
|
||||||
servers,
|
|
||||||
user,
|
|
||||||
conversations = [],
|
|
||||||
}: AppSidebarProps) {
|
|
||||||
const { isNavOpen, closeAll } = useSidebarStore();
|
const { isNavOpen, closeAll } = useSidebarStore();
|
||||||
const { activeServer } = useActiveServer();
|
const { activeServer } = useActiveServer();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
@ -66,7 +53,7 @@ export function AppSidebar({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-78">
|
<div className="w-78">
|
||||||
<UserPanel user={user} />
|
<UserPanel />
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|
@ -92,7 +79,7 @@ export function AppSidebar({
|
||||||
<ChannelSidebar />
|
<ChannelSidebar />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<UserPanel user={user} />
|
<UserPanel />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,22 @@
|
||||||
/**
|
/**
|
||||||
* @file components/settings/SettingsView.tsx
|
* @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";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { UserAvatar } from "@/components/ui/UserAvatar";
|
import { UserAvatar } from "@/components/ui/UserAvatar";
|
||||||
import { UserStatus, User } from "@/db/schema";
|
import { UserStatus } from "@/db/schema";
|
||||||
import {
|
import {
|
||||||
MEMBER_STATUS_COLOR_CLASSES,
|
MEMBER_STATUS_COLOR_CLASSES,
|
||||||
MEMBER_COLOR_OPTIONS,
|
MEMBER_COLOR_OPTIONS,
|
||||||
} from "@/lib/constants/member.styles";
|
} from "@/lib/constants/member.styles";
|
||||||
import { UserRound, Palette } from "lucide-react";
|
import { UserRound, Palette } from "lucide-react";
|
||||||
|
import { useUser } from "@/lib/context/UserContext";
|
||||||
|
|
||||||
/** Props for the SettingsView component. */
|
/** Renders the user settings view allowing profile, online status, and color customizations. */
|
||||||
interface SettingsViewProps {
|
export function SettingsView() {
|
||||||
userId: string;
|
const { currentUser, updateCurrentUser } = useUser();
|
||||||
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],
|
|
||||||
);
|
|
||||||
|
|
||||||
const statuses: { label: string; value: UserStatus }[] = [
|
const statuses: { label: string; value: UserStatus }[] = [
|
||||||
{ label: "Online", value: "ONLINE" },
|
{ label: "Online", value: "ONLINE" },
|
||||||
|
|
@ -36,42 +25,18 @@ export function SettingsView({ userId, currentUser }: SettingsViewProps) {
|
||||||
{ label: "DND", value: "DND" },
|
{ label: "DND", value: "DND" },
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Updates local status state on user selection. */
|
/** Sends optimistic updates to context and persists profile changes to the server API. */
|
||||||
const handleStatusChange = async (newStatus: UserStatus) => {
|
const updateProfile = (fields: Partial<typeof currentUser>) => {
|
||||||
setSelectedStatus(newStatus);
|
updateCurrentUser(fields);
|
||||||
try {
|
fetch("/api/users/profile", {
|
||||||
await fetch("/api/users/profile", {
|
method: "PATCH",
|
||||||
method: "PATCH",
|
headers: { "Content-Type": "application/json" },
|
||||||
headers: { "Content-Type": "application/json" },
|
body: JSON.stringify(fields),
|
||||||
body: JSON.stringify({ status: newStatus }),
|
}).catch((err) => console.error("Failed to update profile", err));
|
||||||
});
|
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!currentUser) return null;
|
if (!currentUser) return null;
|
||||||
|
|
||||||
const userWithUpdatedSettings = {
|
|
||||||
...currentUser,
|
|
||||||
status: selectedStatus,
|
|
||||||
color: selectedColor,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full w-full bg-background text-white gap-6">
|
<div className="flex flex-col h-full w-full bg-background text-white gap-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|
@ -85,7 +50,7 @@ export function SettingsView({ userId, currentUser }: SettingsViewProps) {
|
||||||
{/* Profile Overview Card */}
|
{/* Profile Overview Card */}
|
||||||
<div className="bg-surface border border-muted/20 rounded-xl p-6 space-y-6">
|
<div className="bg-surface border border-muted/20 rounded-xl p-6 space-y-6">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<UserAvatar user={userWithUpdatedSettings} size="md" />
|
<UserAvatar user={currentUser} size="md" />
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-bold text-white">
|
<h2 className="text-lg font-bold text-white">
|
||||||
{currentUser.username}
|
{currentUser.username}
|
||||||
|
|
@ -101,14 +66,12 @@ export function SettingsView({ userId, currentUser }: SettingsViewProps) {
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||||
{statuses.map((status) => {
|
{statuses.map((status) => {
|
||||||
const isSelected = selectedStatus === status.value;
|
const isSelected = currentUser.status === status.value;
|
||||||
const statusBgColor = MEMBER_STATUS_COLOR_CLASSES[status.value];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={status.value}
|
key={status.value}
|
||||||
type="button"
|
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 ${
|
className={`flex items-center gap-2.5 p-3 rounded-lg border text-sm font-medium transition-all ${
|
||||||
isSelected
|
isSelected
|
||||||
? "border-accent bg-accent/10 text-white font-semibold"
|
? "border-accent bg-accent/10 text-white font-semibold"
|
||||||
|
|
@ -116,7 +79,9 @@ export function SettingsView({ userId, currentUser }: SettingsViewProps) {
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<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}
|
{status.label}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -132,12 +97,12 @@ export function SettingsView({ userId, currentUser }: SettingsViewProps) {
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
{MEMBER_COLOR_OPTIONS.map((colorClass) => {
|
{MEMBER_COLOR_OPTIONS.map((colorClass) => {
|
||||||
const isSelected = selectedColor === colorClass;
|
const isSelected = currentUser.color === colorClass;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={colorClass}
|
key={colorClass}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleColorChange(colorClass)}
|
onClick={() => updateProfile({ color: colorClass })}
|
||||||
className={`w-8 h-8 rounded-full ${colorClass} transition-all flex items-center justify-center ${
|
className={`w-8 h-8 rounded-full ${colorClass} transition-all flex items-center justify-center ${
|
||||||
isSelected
|
isSelected
|
||||||
? "ring-2 ring-white ring-offset-2 ring-offset-surface scale-110"
|
? "ring-2 ring-white ring-offset-2 ring-offset-surface scale-110"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* @file components/sidebar/UserPanel.tsx
|
* @file components/layout/UserPanel.tsx
|
||||||
* @description Client component providing a footer user panel with session information, avatar display, online status, and settings/logout actions.
|
* @description User panel component displaying the current user's avatar, status, settings link, and logout button.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
@ -8,31 +8,14 @@
|
||||||
import { LogOut, Settings } from "lucide-react";
|
import { LogOut, Settings } from "lucide-react";
|
||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
import { UserAvatar } from "@/components/ui/UserAvatar";
|
import { UserAvatar } from "@/components/ui/UserAvatar";
|
||||||
import { UserStatus } from "@/db/schema";
|
import { useUser } from "@/lib/context/UserContext";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
/**
|
/** Renders the bottom user panel with user information and quick action controls. */
|
||||||
* Props for the UserPanel component.
|
export function UserPanel() {
|
||||||
*/
|
const { currentUser } = useUser();
|
||||||
interface UserPanelProps {
|
|
||||||
/** User details including username, avatar color, and online status. */
|
|
||||||
user: {
|
|
||||||
username: string;
|
|
||||||
color: string;
|
|
||||||
status: UserStatus;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/** Handles logging out the user and redirecting to the login page. */
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await fetch("/api/auth/logout", { method: "POST" });
|
await fetch("/api/auth/logout", { method: "POST" });
|
||||||
await signOut({ callbackUrl: "/login" });
|
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">
|
<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">
|
<div className="flex items-center gap-2.5 min-w-0 flex-1 cursor-pointer group">
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
user={user}
|
user={currentUser}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="transition-transform group-hover:scale-105"
|
className="transition-transform group-hover:scale-105"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-semibold text-foreground truncate leading-tight group-hover:text-accent transition-colors">
|
<p className="text-sm font-semibold text-foreground truncate leading-tight group-hover:text-accent transition-colors">
|
||||||
{user.username}
|
{currentUser.username}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted truncate leading-tight font-medium capitalize">
|
<p className="text-xs text-muted truncate leading-tight font-medium capitalize">
|
||||||
{user.status.toLowerCase()}
|
{currentUser.status.toLowerCase()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* @file lib/context/ServerContext.tsx
|
* @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";
|
"use client";
|
||||||
|
|
@ -31,6 +31,9 @@ export interface ServerMember {
|
||||||
interface ServerContextType {
|
interface ServerContextType {
|
||||||
activeServer: ServerWithChannels | null;
|
activeServer: ServerWithChannels | null;
|
||||||
setActiveServer: (server: ServerWithChannels | null) => void;
|
setActiveServer: (server: ServerWithChannels | null) => void;
|
||||||
|
currentUser: User | null;
|
||||||
|
setCurrentUser: (user: User | null) => void;
|
||||||
|
updateCurrentUser: (partialUser: Partial<User>) => void;
|
||||||
addChannel: (channel: Channel) => void;
|
addChannel: (channel: Channel) => void;
|
||||||
removeChannel: (channelId: string) => void;
|
removeChannel: (channelId: string) => void;
|
||||||
updateChannel: (channel: Channel) => void;
|
updateChannel: (channel: Channel) => void;
|
||||||
|
|
@ -43,11 +46,18 @@ interface ServerContextType {
|
||||||
|
|
||||||
const ServerContext = createContext<ServerContextType | null>(null);
|
const ServerContext = createContext<ServerContextType | null>(null);
|
||||||
|
|
||||||
/** Provides active server, channel, and member management state to child components. */
|
/** Provides active server, channel, user, and member management state to child components. */
|
||||||
export function ServerProvider({ children }: { children: React.ReactNode }) {
|
export function ServerProvider({
|
||||||
|
children,
|
||||||
|
initialUser = null,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
initialUser?: User | null;
|
||||||
|
}) {
|
||||||
const [activeServer, setActiveServer] = useState<ServerWithChannels | null>(
|
const [activeServer, setActiveServer] = useState<ServerWithChannels | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const [currentUser, setCurrentUser] = useState<User | null>(initialUser);
|
||||||
const [members, setMembers] = useState<ServerMember[]>([]);
|
const [members, setMembers] = useState<ServerMember[]>([]);
|
||||||
|
|
||||||
/** Updates the active server state using a partial updater function. */
|
/** 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));
|
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. */
|
/** Adds a new channel to the active server. */
|
||||||
const addChannel = (ch: Channel) =>
|
const addChannel = (ch: Channel) =>
|
||||||
updateServer((s) => ({ channels: [...s.channels, ch] }));
|
updateServer((s) => ({ channels: [...s.channels, ch] }));
|
||||||
|
|
@ -95,6 +118,9 @@ export function ServerProvider({ children }: { children: React.ReactNode }) {
|
||||||
value={{
|
value={{
|
||||||
activeServer,
|
activeServer,
|
||||||
setActiveServer,
|
setActiveServer,
|
||||||
|
currentUser,
|
||||||
|
setCurrentUser,
|
||||||
|
updateCurrentUser,
|
||||||
addChannel,
|
addChannel,
|
||||||
removeChannel,
|
removeChannel,
|
||||||
updateChannel,
|
updateChannel,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext, useState } from "react";
|
||||||
import type { User } from "@/db/schema";
|
import type { User } from "@/db/schema";
|
||||||
|
|
||||||
/** Represents a friendship record between two users. */
|
/** Represents a friendship record between two users. */
|
||||||
|
|
@ -19,6 +19,7 @@ interface Friendship {
|
||||||
interface UserContextType {
|
interface UserContextType {
|
||||||
currentUser: User;
|
currentUser: User;
|
||||||
friendships: Friendship[];
|
friendships: Friendship[];
|
||||||
|
updateCurrentUser: (partialUser: Partial<User>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserContext = createContext<UserContextType | null>(null);
|
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. */
|
/** Provides user data and friendship states to child components. */
|
||||||
export function UserProvider({
|
export function UserProvider({
|
||||||
children,
|
children,
|
||||||
currentUser,
|
currentUser: initialUser,
|
||||||
friendships,
|
friendships,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
currentUser: User;
|
currentUser: User;
|
||||||
friendships: Friendship[];
|
friendships: Friendship[];
|
||||||
}) {
|
}) {
|
||||||
|
const [currentUser, setCurrentUser] = useState<User>(initialUser);
|
||||||
|
|
||||||
|
/** Updates local current user state dynamically. */
|
||||||
|
const updateCurrentUser = (partialUser: Partial<User>) => {
|
||||||
|
setCurrentUser((prev) => ({ ...prev, ...partialUser }));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserContext.Provider value={{ currentUser, friendships }}>
|
<UserContext.Provider
|
||||||
|
value={{ currentUser, friendships, updateCurrentUser }}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</UserContext.Provider>
|
</UserContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue