feat(ui): add Button, Modal, Spinner, Toast, and Tooltip components with styles
All checks were successful
Build and Release / build-and-release (push) Successful in 13m12s

- Implemented Button component with various props for customization.
- Created Modal component with header, content, and footer subcomponents.
- Added Spinner component for loading indicators.
- Developed Toast component for displaying notifications.
- Introduced Tooltip component for contextual hints with keyboard shortcuts.
- Added corresponding CSS modules for styling each component.
- Updated index file to export new components.
- Configured TypeScript settings for the UI package.
This commit is contained in:
Bryan1029384756
2026-04-14 09:02:14 -05:00
parent 9ef839938e
commit b7a4cf4ce8
376 changed files with 52619 additions and 167641 deletions

View File

@@ -0,0 +1,124 @@
/**
* ChannelWelcomeSection — the "start of channel" header rendered above
* the first message in any channel view. Two variants:
*
* - DM (1:1) → centred avatar + display name + "@username" handle +
* "This is the beginning of your direct message history with Name."
* - Text channel → "Welcome to #channel-name!" with the Hash icon
* and a short description.
*
* Voice channels never reach the message list so they never need this.
*/
import { useMemo } from 'react';
import { useQuery } from 'convex/react';
import { Hash } from '@phosphor-icons/react';
import { Avatar } from '@discord-clone/ui';
import { api } from '../../../../../convex/_generated/api';
import { useOnlineUsers } from '../../contexts/PresenceContext';
import styles from './ChannelWelcomeSection.module.css';
interface ChannelWelcomeSectionProps {
channelId: string;
channelName?: string;
channelType?: string;
}
function mapPresence(
status: string | undefined,
): 'online' | 'idle' | 'dnd' | 'offline' {
switch (status) {
case 'online':
return 'online';
case 'idle':
return 'idle';
case 'dnd':
return 'dnd';
default:
return 'offline';
}
}
export function ChannelWelcomeSection({
channelId,
channelName,
channelType,
}: ChannelWelcomeSectionProps) {
if (channelType === 'dm') {
return <DmWelcome channelId={channelId} channelName={channelName} />;
}
const name = channelName || 'channel';
return (
<div className={styles.container}>
<div className={styles.iconWrapper}>
<Hash size={42} weight="bold" />
</div>
<h1 className={styles.title}>Welcome to #{name}!</h1>
<p className={styles.description}>
This is the start of the #{name} channel. All messages are
end-to-end encrypted.
</p>
</div>
);
}
interface DmWelcomeProps {
channelId: string;
channelName?: string;
}
function DmWelcome({ channelId, channelName }: DmWelcomeProps) {
const { resolveStatus } = useOnlineUsers();
const myUserId =
typeof localStorage !== 'undefined' ? localStorage.getItem('userId') : null;
const dmRows = useQuery(
api.dms.listDMs,
myUserId ? { userId: myUserId as any } : 'skip',
);
const allUsers = useQuery(api.auth.getPublicKeys) ?? [];
const other = useMemo(() => {
if (!dmRows) return null;
const row = (dmRows as any[]).find((r) => r.channel_id === channelId);
if (!row) return null;
const profile = allUsers.find((u) => u.id === row.other_user_id);
const username = profile?.username || row.other_username || '';
return {
userId: row.other_user_id as string,
displayName: username || channelName || 'this user',
username,
avatarUrl: profile?.avatarUrl ?? row.other_user_avatar_url ?? null,
status:
(profile?.status as string | undefined) ||
(row.other_user_status as string | undefined) ||
'offline',
};
}, [dmRows, allUsers, channelId, channelName]);
const presence = mapPresence(
other ? resolveStatus(other.status, other.userId) : 'offline',
);
const displayName = other?.displayName ?? channelName ?? 'this user';
return (
<div className={styles.dmContainer}>
<div className={styles.dmAvatar}>
<Avatar
src={other?.avatarUrl ?? null}
fallback={displayName}
size={80}
status={presence}
/>
</div>
<div className={styles.dmNameRow}>
<h1 className={styles.dmName}>{displayName}</h1>
</div>
<p className={styles.dmDescription}>
This is the beginning of your direct message history with{' '}
<strong>{displayName}</strong>.
</p>
</div>
);
}