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:
@@ -12,6 +12,7 @@ export function InviteModal() {
|
||||
const generateInvite = useServerStore((s) => s.generateInvite);
|
||||
const currentServerId = useServerStore((s) => s.currentServerId);
|
||||
const isOpen = activeModal === 'invite';
|
||||
const inviteUrl = inviteCode ? `${window.location.origin}/join/${inviteCode}` : '';
|
||||
useEffect(() => {
|
||||
if (isOpen && currentServerId) {
|
||||
setIsLoading(true);
|
||||
@@ -24,8 +25,10 @@ export function InviteModal() {
|
||||
}
|
||||
}, [isOpen, currentServerId, generateInvite]);
|
||||
const handleCopy = async () => {
|
||||
if (!inviteUrl)
|
||||
return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(inviteCode);
|
||||
await navigator.clipboard.writeText(inviteUrl);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
@@ -40,7 +43,7 @@ export function InviteModal() {
|
||||
}
|
||||
}
|
||||
};
|
||||
return (_jsxs(Modal, { isOpen: isOpen, onClose: closeModal, title: "Invite Friends", children: [_jsx("p", { className: "text-discord-text-secondary text-sm mb-4", children: "Share this invite code with friends to let them join your server." }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { type: "text", value: isLoading ? 'Generating...' : inviteCode, readOnly: true, className: "invite-code-input flex-1 px-3 py-2 bg-discord-bg-tertiary rounded text-discord-text-primary outline-none font-mono text-sm" }), _jsx("button", { onClick: handleCopy, disabled: isLoading, className: `px-4 py-2 text-sm font-medium rounded transition-colors ${copied
|
||||
return (_jsxs(Modal, { isOpen: isOpen, onClose: closeModal, title: "Invite Friends", children: [_jsx("p", { className: "text-discord-text-secondary text-sm mb-4", children: "Share this invite link with friends to let them join your server." }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { type: "text", value: isLoading ? 'Generating...' : inviteUrl, readOnly: true, className: "invite-code-input flex-1 px-3 py-2 bg-discord-bg-tertiary rounded text-discord-text-primary outline-none font-mono text-xs" }), _jsx("button", { onClick: handleCopy, disabled: isLoading || !inviteUrl, className: `px-4 py-2 text-sm font-medium rounded transition-colors ${copied
|
||||
? 'bg-discord-green text-white'
|
||||
: 'bg-discord-blurple hover:bg-discord-blurple-hover text-white'}`, children: copied ? 'Copied!' : 'Copy' })] })] }));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { InviteModal } from './InviteModal';
|
||||
import { useUIStore } from '../../stores/uiStore';
|
||||
import { useServerStore } from '../../stores/serverStore';
|
||||
|
||||
// Mock the stores by spying on their getState
|
||||
beforeEach(() => {
|
||||
// Reset stores to default state
|
||||
useUIStore.setState({
|
||||
activeModal: null,
|
||||
modalData: {},
|
||||
});
|
||||
useServerStore.setState({
|
||||
currentServerId: null,
|
||||
servers: [],
|
||||
});
|
||||
});
|
||||
|
||||
describe('InviteModal', () => {
|
||||
it('does not render when activeModal is not "invite"', () => {
|
||||
useUIStore.setState({ activeModal: null });
|
||||
render(<InviteModal />);
|
||||
expect(screen.queryByText('Invite Friends')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls generateInvite and displays the invite URL when opened', async () => {
|
||||
const mockGenerateInvite = vi.fn().mockResolvedValue('test-invite-code');
|
||||
useUIStore.setState({ activeModal: 'invite' });
|
||||
useServerStore.setState({
|
||||
currentServerId: 'server-123',
|
||||
generateInvite: mockGenerateInvite,
|
||||
});
|
||||
|
||||
render(<InviteModal />);
|
||||
|
||||
// Modal title should be visible
|
||||
expect(screen.getByText('Invite Friends')).toBeInTheDocument();
|
||||
|
||||
// Should show "Generating..." initially
|
||||
expect(screen.getByDisplayValue('Generating...')).toBeInTheDocument();
|
||||
|
||||
// Wait for the invite code to load
|
||||
await waitFor(() => {
|
||||
const input = screen.getByDisplayValue(/\/join\/test-invite-code/);
|
||||
expect(input).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// generateInvite should have been called with the server ID
|
||||
expect(mockGenerateInvite).toHaveBeenCalledWith('server-123');
|
||||
});
|
||||
|
||||
it('displays an error when generateInvite fails', async () => {
|
||||
const mockGenerateInvite = vi.fn().mockRejectedValue(new Error('Not authorized'));
|
||||
useUIStore.setState({ activeModal: 'invite' });
|
||||
useServerStore.setState({
|
||||
currentServerId: 'server-123',
|
||||
generateInvite: mockGenerateInvite,
|
||||
});
|
||||
|
||||
render(<InviteModal />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Not authorized')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Copy button is disabled while loading', () => {
|
||||
const mockGenerateInvite = vi.fn().mockReturnValue(new Promise(() => {})); // never resolves
|
||||
useUIStore.setState({ activeModal: 'invite' });
|
||||
useServerStore.setState({
|
||||
currentServerId: 'server-123',
|
||||
generateInvite: mockGenerateInvite,
|
||||
});
|
||||
|
||||
render(<InviteModal />);
|
||||
|
||||
const copyButton = screen.getByText('Copy');
|
||||
expect(copyButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('Copy button calls clipboard.writeText with the invite URL', async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockGenerateInvite = vi.fn().mockResolvedValue('abc123');
|
||||
const mockClipboard = vi.fn().mockResolvedValue(undefined);
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: { writeText: mockClipboard },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
useUIStore.setState({ activeModal: 'invite' });
|
||||
useServerStore.setState({
|
||||
currentServerId: 'server-123',
|
||||
generateInvite: mockGenerateInvite,
|
||||
});
|
||||
|
||||
render(<InviteModal />);
|
||||
|
||||
// Wait for invite to load
|
||||
await waitFor(() => {
|
||||
expect(screen.getByDisplayValue(/\/join\/abc123/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click copy
|
||||
const copyButton = screen.getByText('Copy');
|
||||
await user.click(copyButton);
|
||||
|
||||
expect(mockClipboard).toHaveBeenCalledWith(expect.stringContaining('/join/abc123'));
|
||||
|
||||
// Button text should change to "Copied!"
|
||||
expect(screen.getByText('Copied!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -7,32 +7,38 @@ export function InviteModal() {
|
||||
const [inviteCode, setInviteCode] = useState('');
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const activeModal = useUIStore((s) => s.activeModal);
|
||||
const closeModal = useUIStore((s) => s.closeModal);
|
||||
const generateInvite = useServerStore((s) => s.generateInvite);
|
||||
const currentServerId = useServerStore((s) => s.currentServerId);
|
||||
|
||||
const isOpen = activeModal === 'invite';
|
||||
const inviteUrl = inviteCode ? `${window.location.origin}/join/${inviteCode}` : '';
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && currentServerId) {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
generateInvite(currentServerId)
|
||||
.then(code => {
|
||||
setInviteCode(code);
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch(() => setIsLoading(false));
|
||||
.catch((err) => {
|
||||
setError(err instanceof Error ? err.message : 'Failed to generate invite link');
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
}, [isOpen, currentServerId, generateInvite]);
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (!inviteUrl) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(inviteCode);
|
||||
await navigator.clipboard.writeText(inviteUrl);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
// Fallback: select the text
|
||||
const input = document.querySelector<HTMLInputElement>('.invite-code-input');
|
||||
if (input) {
|
||||
input.select();
|
||||
@@ -46,18 +52,23 @@ export function InviteModal() {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={closeModal} title="Invite Friends">
|
||||
<p className="text-discord-text-secondary text-sm mb-4">
|
||||
Share this invite code with friends to let them join your server.
|
||||
Share this invite link with friends to let them join your server.
|
||||
</p>
|
||||
{error && (
|
||||
<div className="mb-3 p-2 bg-discord-red/10 border border-discord-red/30 rounded text-discord-text-danger text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={isLoading ? 'Generating...' : inviteCode}
|
||||
value={isLoading ? 'Generating...' : inviteUrl}
|
||||
readOnly
|
||||
className="invite-code-input flex-1 px-3 py-2 bg-discord-bg-tertiary rounded text-discord-text-primary outline-none font-mono text-sm"
|
||||
className="invite-code-input flex-1 px-3 py-2 bg-discord-bg-tertiary rounded text-discord-text-primary outline-none font-mono text-xs"
|
||||
/>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || !inviteUrl}
|
||||
className={`px-4 py-2 text-sm font-medium rounded transition-colors ${
|
||||
copied
|
||||
? 'bg-discord-green text-white'
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Modal } from '../ui/Modal';
|
||||
import { useUIStore } from '../../stores/uiStore';
|
||||
import { useServerStore } from '../../stores/serverStore';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
export function JoinServerModal() {
|
||||
const { inviteCode: urlInviteCode } = useParams();
|
||||
const [inviteCode, setInviteCode] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -13,6 +14,11 @@ export function JoinServerModal() {
|
||||
const loadServers = useServerStore((s) => s.loadServers);
|
||||
const navigate = useNavigate();
|
||||
const isOpen = activeModal === 'joinServer';
|
||||
useEffect(() => {
|
||||
if (isOpen && urlInviteCode) {
|
||||
setInviteCode(urlInviteCode);
|
||||
}
|
||||
}, [isOpen, urlInviteCode]);
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { JoinServerModal } from './JoinServer';
|
||||
import { useUIStore } from '../../stores/uiStore';
|
||||
import { useServerStore } from '../../stores/serverStore';
|
||||
|
||||
const mockNavigate = vi.fn();
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual('react-router-dom');
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: () => mockNavigate,
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockNavigate.mockClear();
|
||||
useUIStore.setState({ activeModal: null });
|
||||
useServerStore.setState({
|
||||
servers: [],
|
||||
currentServerId: null,
|
||||
});
|
||||
});
|
||||
|
||||
function renderModal() {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<JoinServerModal />
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
describe('JoinServerModal', () => {
|
||||
it('does not render when activeModal is not "joinServer"', () => {
|
||||
useUIStore.setState({ activeModal: null });
|
||||
renderModal();
|
||||
expect(screen.queryByText('Join a Server')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the form when opened', () => {
|
||||
useUIStore.setState({ activeModal: 'joinServer' });
|
||||
renderModal();
|
||||
expect(screen.getByText('Join a Server')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('e.g. abc123')).toBeInTheDocument();
|
||||
expect(screen.getByText('Join Server')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows validation error when submitting empty code', async () => {
|
||||
const user = userEvent.setup();
|
||||
useUIStore.setState({ activeModal: 'joinServer' });
|
||||
renderModal();
|
||||
|
||||
const submitButton = screen.getByText('Join Server');
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(screen.getByText('Invite code is required')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls joinByCode with the entered invite code and navigates on success', async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockJoinByCode = vi.fn().mockResolvedValue({ id: 'new-server-id', name: 'Test Server' });
|
||||
useUIStore.setState({ activeModal: 'joinServer' });
|
||||
useServerStore.setState({ joinByCode: mockJoinByCode });
|
||||
|
||||
renderModal();
|
||||
|
||||
// Type invite code
|
||||
const input = screen.getByPlaceholderText('e.g. abc123');
|
||||
await user.type(input, 'my-invite-code');
|
||||
|
||||
// Click join
|
||||
const submitButton = screen.getByText('Join Server');
|
||||
await user.click(submitButton);
|
||||
|
||||
// joinByCode should be called with the code
|
||||
await waitFor(() => {
|
||||
expect(mockJoinByCode).toHaveBeenCalledWith('my-invite-code');
|
||||
});
|
||||
|
||||
// Should navigate to the new server
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/channels/new-server-id');
|
||||
});
|
||||
|
||||
// Modal should close (activeModal becomes null)
|
||||
expect(useUIStore.getState().activeModal).toBeNull();
|
||||
});
|
||||
|
||||
it('shows error message when joinByCode fails', async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockJoinByCode = vi.fn().mockRejectedValue(new Error('Invalid invite code'));
|
||||
useUIStore.setState({ activeModal: 'joinServer' });
|
||||
useServerStore.setState({ joinByCode: mockJoinByCode });
|
||||
|
||||
renderModal();
|
||||
|
||||
const input = screen.getByPlaceholderText('e.g. abc123');
|
||||
await user.type(input, 'bad-code');
|
||||
|
||||
const submitButton = screen.getByText('Join Server');
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Invalid invite code')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,48 +1,40 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal } from '../ui/Modal';
|
||||
import { useUIStore } from '../../stores/uiStore';
|
||||
import { useServerStore } from '../../stores/serverStore';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
export function JoinServerModal() {
|
||||
const { inviteCode: urlInviteCode } = useParams<{ inviteCode?: string }>();
|
||||
const [inviteCode, setInviteCode] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const activeModal = useUIStore((s) => s.activeModal);
|
||||
const closeModal = useUIStore((s) => s.closeModal);
|
||||
const loadServers = useServerStore((s) => s.loadServers);
|
||||
const joinByCode = useServerStore((s) => s.joinByCode);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const isOpen = activeModal === 'joinServer';
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && urlInviteCode) {
|
||||
setInviteCode(urlInviteCode);
|
||||
}
|
||||
}, [isOpen, urlInviteCode]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
if (!inviteCode.trim()) {
|
||||
const code = inviteCode.trim();
|
||||
if (!code) {
|
||||
setError('Invite code is required');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const token = localStorage.getItem('opencord_token');
|
||||
const response = await fetch('/api/servers/join', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ inviteCode: inviteCode.trim() }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json() as { error: string };
|
||||
throw new Error(data.error || 'Failed to join server');
|
||||
}
|
||||
|
||||
const server = await response.json() as { id: string };
|
||||
await loadServers();
|
||||
const server = await joinByCode(code);
|
||||
closeModal();
|
||||
setInviteCode('');
|
||||
navigate(`/channels/${server.id}`);
|
||||
@@ -60,7 +52,7 @@ export function JoinServerModal() {
|
||||
Enter an invite code to join an existing server.
|
||||
</p>
|
||||
{error && (
|
||||
<div className="mb-3 p-2 bg-discord-red/10 border border-discord-red/30 rounded text-discord-red text-sm">
|
||||
<div className="mb-3 p-2 bg-discord-red/10 border border-discord-red/30 rounded text-discord-text-danger text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -12,6 +12,7 @@ export function UserSettingsModal() {
|
||||
const logout = useAuthStore((s) => s.logout);
|
||||
const [displayName, setDisplayName] = useState(user?.displayName ?? '');
|
||||
const [customStatus, setCustomStatus] = useState(user?.customStatus ?? '');
|
||||
const [status, setStatus] = useState(user?.status ?? 'online');
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -24,6 +25,7 @@ export function UserSettingsModal() {
|
||||
await updateProfile({
|
||||
displayName: displayName.trim() || undefined,
|
||||
customStatus: customStatus.trim() || undefined,
|
||||
status: status,
|
||||
});
|
||||
setSuccess('Profile updated!');
|
||||
setTimeout(() => setSuccess(''), 2000);
|
||||
@@ -41,5 +43,5 @@ export function UserSettingsModal() {
|
||||
};
|
||||
if (!user)
|
||||
return null;
|
||||
return (_jsx(Modal, { isOpen: isOpen, onClose: closeModal, title: "User Settings", maxWidth: "max-w-lg", children: _jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "flex items-center gap-4 p-4 bg-discord-bg-secondary rounded-lg", children: [_jsx(Avatar, { src: user.avatar, name: user.displayName ?? user.username, size: 64, status: user.status }), _jsxs("div", { children: [_jsx("div", { className: "font-bold text-lg", children: user.displayName ?? user.username }), _jsxs("div", { className: "text-discord-text-muted text-sm", children: ["@", user.username] }), user.customStatus && (_jsx("div", { className: "text-discord-text-secondary text-sm mt-1", children: user.customStatus }))] })] }), error && (_jsx("div", { className: "p-2 bg-discord-red/10 border border-discord-red/30 rounded text-discord-red text-sm", children: error })), success && (_jsx("div", { className: "p-2 bg-discord-green/10 border border-discord-green/30 rounded text-discord-green text-sm", children: success })), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold text-discord-text-secondary uppercase mb-2", children: "Display Name" }), _jsx("input", { type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value), className: "w-full px-3 py-2 bg-discord-bg-tertiary rounded text-discord-text-primary outline-none focus:ring-2 focus:ring-discord-blurple" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold text-discord-text-secondary uppercase mb-2", children: "Custom Status" }), _jsx("input", { type: "text", value: customStatus, onChange: (e) => setCustomStatus(e.target.value), className: "w-full px-3 py-2 bg-discord-bg-tertiary rounded text-discord-text-primary outline-none focus:ring-2 focus:ring-discord-blurple", placeholder: "What are you up to?" })] }), _jsxs("div", { className: "flex items-center justify-between pt-2", children: [_jsx("button", { onClick: handleLogout, className: "px-4 py-2 text-sm text-discord-red hover:bg-discord-red/10 rounded transition-colors", children: "Log Out" }), _jsx("button", { onClick: handleSave, disabled: isLoading, className: "px-4 py-2 bg-discord-blurple hover:bg-discord-blurple-hover text-white text-sm font-medium rounded transition-colors disabled:opacity-50", children: isLoading ? 'Saving...' : 'Save Changes' })] })] }) }));
|
||||
return (_jsx(Modal, { isOpen: isOpen, onClose: closeModal, title: "User Settings", maxWidth: "max-w-lg", children: _jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "flex items-center gap-4 p-4 bg-discord-bg-secondary rounded-lg", children: [_jsx(Avatar, { src: user.avatar, name: user.displayName ?? user.username, size: 64, status: user.status }), _jsxs("div", { children: [_jsx("div", { className: "font-bold text-lg", children: user.displayName ?? user.username }), _jsxs("div", { className: "text-discord-text-muted text-sm", children: ["@", user.username] }), user.customStatus && (_jsx("div", { className: "text-discord-text-secondary text-sm mt-1", children: user.customStatus }))] })] }), error && (_jsx("div", { className: "p-2 bg-discord-red/10 border border-discord-red/30 rounded text-discord-red text-sm", children: error })), success && (_jsx("div", { className: "p-2 bg-discord-green/10 border border-discord-green/30 rounded text-discord-green text-sm", children: success })), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold text-discord-text-secondary uppercase mb-2", children: "Status" }), _jsxs("select", { value: status, onChange: (e) => setStatus(e.target.value), className: "w-full px-3 py-2 bg-discord-bg-tertiary rounded text-discord-text-primary outline-none focus:ring-2 focus:ring-discord-blurple appearance-none", children: [_jsx("option", { value: "online", children: "Online" }), _jsx("option", { value: "idle", children: "Idle" }), _jsx("option", { value: "dnd", children: "Do Not Disturb" })] })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold text-discord-text-secondary uppercase mb-2", children: "Display Name" }), _jsx("input", { type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value), className: "w-full px-3 py-2 bg-discord-bg-tertiary rounded text-discord-text-primary outline-none focus:ring-2 focus:ring-discord-blurple" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold text-discord-text-secondary uppercase mb-2", children: "Custom Status" }), _jsx("input", { type: "text", value: customStatus, onChange: (e) => setCustomStatus(e.target.value), className: "w-full px-3 py-2 bg-discord-bg-tertiary rounded text-discord-text-primary outline-none focus:ring-2 focus:ring-discord-blurple", placeholder: "What are you up to?" })] }), _jsxs("div", { className: "flex items-center justify-between pt-2", children: [_jsx("button", { onClick: handleLogout, className: "px-4 py-2 text-sm text-discord-red hover:bg-discord-red/10 rounded transition-colors", children: "Log Out" }), _jsx("button", { onClick: handleSave, disabled: isLoading, className: "px-4 py-2 bg-discord-blurple hover:bg-discord-blurple-hover text-white text-sm font-medium rounded transition-colors disabled:opacity-50", children: isLoading ? 'Saving...' : 'Save Changes' })] })] }) }));
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export function UserSettingsModal() {
|
||||
|
||||
const [displayName, setDisplayName] = useState(user?.displayName ?? '');
|
||||
const [customStatus, setCustomStatus] = useState(user?.customStatus ?? '');
|
||||
const [status, setStatus] = useState(user?.status ?? 'online');
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -27,7 +28,8 @@ export function UserSettingsModal() {
|
||||
await updateProfile({
|
||||
displayName: displayName.trim() || undefined,
|
||||
customStatus: customStatus.trim() || undefined,
|
||||
});
|
||||
status: status as any,
|
||||
} as any);
|
||||
setSuccess('Profile updated!');
|
||||
setTimeout(() => setSuccess(''), 2000);
|
||||
} catch (err) {
|
||||
@@ -71,6 +73,21 @@ export function UserSettingsModal() {
|
||||
<div className="p-2 bg-discord-green/10 border border-discord-green/30 rounded text-discord-green text-sm">{success}</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-discord-text-secondary uppercase mb-2">
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
value={status}
|
||||
onChange={(e) => setStatus(e.target.value as any)}
|
||||
className="w-full px-3 py-2 bg-discord-bg-tertiary rounded text-discord-text-primary outline-none focus:ring-2 focus:ring-discord-blurple appearance-none"
|
||||
>
|
||||
<option value="online">Online</option>
|
||||
<option value="idle">Idle</option>
|
||||
<option value="dnd">Do Not Disturb</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-discord-text-secondary uppercase mb-2">
|
||||
Display Name
|
||||
|
||||
Reference in New Issue
Block a user