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
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:
124
packages/shared/src/components/channel/ChannelWelcomeSection.tsx
Normal file
124
packages/shared/src/components/channel/ChannelWelcomeSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user