fix: repair invite links, social features, messaging + Discord UI overhaul

Phase 1 - Feature Repair:
- Fix member kick/leave: add missing db.delete() call in servers.ts
- Stabilize invite codes: return existing code instead of regenerating
- Fix user search: use LIKE instead of exact match in social.ts
- Wire DM button on FriendsPage to create/navigate to DM channels
- Add cancel outgoing friend request (DELETE endpoint + frontend)
- Add accept/decline friend request actions with WS real-time events
- Fix replyToId persistence in message creation
- Hydrate reactions and replyTo in message queries
- Add joinByCode to API client and serverStore
- Add friend_request_received/accepted WebSocket events

Phase 2 - Discord UI Overhaul:
- Remove stray borders between layout columns
- Replace shadow-sm with shadow-header on content headers
- Replace all bg-gray-*/text-gray-* with Discord color tokens
- Ensure flat color contrast (#1E1F22, #2B2D31, #313338)

Testing:
- Set up vitest + @testing-library/react + jsdom
- Add 17 tests across InviteModal, JoinServer, FriendsPage (all passing)
- Fix vite resolve.extensions to prefer .tsx over stale .js files
This commit is contained in:
Jannis Braun
2026-02-18 05:34:45 +01:00
parent 4fd17084a5
commit 5ef502f2e3
82 changed files with 4906 additions and 552 deletions
+25 -8
View File
@@ -1,4 +1,6 @@
import React from 'react';
import type { User } from '@opencord/shared';
import { useUIStore } from '../../stores/uiStore';
interface AvatarProps {
src?: string | null;
@@ -6,25 +8,40 @@ interface AvatarProps {
size?: number;
status?: 'online' | 'idle' | 'dnd' | 'offline' | null;
className?: string;
onClick?: () => void;
onClick?: (e: React.MouseEvent) => void;
user?: User;
}
const statusColors: Record<string, string> = {
online: 'bg-discord-green',
idle: 'bg-discord-yellow',
dnd: 'bg-discord-red',
offline: 'bg-gray-500',
offline: 'bg-discord-text-muted',
};
export function Avatar({ src, name, size = 40, status, className = '', onClick }: AvatarProps) {
export function Avatar({ src, name, size = 40, status, className = '', onClick, user }: AvatarProps) {
const openUserProfile = useUIStore((s) => s.openUserProfile);
const initials = name.charAt(0).toUpperCase();
const fontSize = size < 32 ? 'text-xs' : size < 48 ? 'text-sm' : 'text-lg';
const handleClick = (e: React.MouseEvent) => {
if (onClick) {
onClick(e);
} else if (user) {
e.stopPropagation();
const rect = e.currentTarget.getBoundingClientRect();
openUserProfile(user, {
top: Math.min(rect.top, window.innerHeight - 450),
left: rect.right + 16,
});
}
};
return (
<div
className={`relative inline-flex flex-shrink-0 ${onClick ? 'cursor-pointer' : ''} ${className}`}
className={`relative inline-flex flex-shrink-0 ${(onClick || user) ? 'cursor-pointer' : ''} ${className}`}
style={{ width: size, height: size }}
onClick={onClick}
onClick={handleClick}
>
{src ? (
<img
@@ -49,12 +66,12 @@ export function Avatar({ src, name, size = 40, status, className = '', onClick }
</div>
{status && (
<div
className={`absolute -bottom-0.5 -right-0.5 rounded-full border-[3px] border-discord-bg-secondary ${statusColors[status] ?? 'bg-gray-500'}`}
className={`absolute -bottom-0.5 -right-0.5 rounded-full border-[3px] border-[#2b2d31] ${statusColors[status] ?? 'bg-discord-text-muted'}`}
style={{
width: size * 0.35,
height: size * 0.35,
minWidth: 10,
minHeight: 10,
minWidth: 12,
minHeight: 12,
}}
/>
)}