Skip to content

Commit 2611f00

Browse files
committed
feat: group
1 parent 9ac3873 commit 2611f00

File tree

4 files changed

+268
-0
lines changed

4 files changed

+268
-0
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<script lang="ts">
2+
import type { HTMLAttributes } from 'svelte/elements';
3+
import Avatar from '../../ui/Avatar/Avatar.svelte';
4+
import { cn } from '../../utils';
5+
6+
interface IGroupProps extends HTMLAttributes<HTMLButtonElement> {
7+
avatar: string;
8+
name: string;
9+
unread?: boolean;
10+
callback: () => void;
11+
}
12+
13+
const {
14+
avatar,
15+
name,
16+
unread = false,
17+
callback,
18+
...restProps
19+
}: IGroupProps = $props();
20+
</script>
21+
22+
<button
23+
{...restProps}
24+
class={cn([
25+
'relative flex w-full cursor-pointer items-center gap-3 rounded-lg py-4 px-2',
26+
restProps.class
27+
])}
28+
onclick={callback}
29+
>
30+
<Avatar src={avatar} alt="Group Avatar" size="md" />
31+
<span class="flex w-full items-center justify-between">
32+
<h2 class="text-left font-medium">{name}</h2>
33+
{#if unread}
34+
<span class="h-2 w-2 rounded-full bg-blue-500"></span>
35+
{/if}
36+
</span>
37+
</button>
38+
39+
<!--
40+
@component
41+
@name GroupItem
42+
@description A group item component that displays a group avatar and name, with an optional unread indicator.
43+
@props
44+
- avatar: string - The URL of the group avatar image.
45+
- name: string - The group name.
46+
- unread: boolean - Optional. Indicates if there are unread messages. Defaults to false.
47+
- callback: () => void - Function to call when the group is clicked.
48+
@usage
49+
<script>
50+
import GroupItem from '$lib/ui/GroupItem.svelte';
51+
</script>
52+
53+
<GroupItem
54+
avatar="https://example.com/group-avatar.jpg"
55+
name="Study Buddies"
56+
unread={true}
57+
callback={() => console.log('Group clicked')}
58+
/>
59+
-->

platforms/pictique/src/lib/fragments/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ export { default as Comment } from './Comment/Comment.svelte';
1717
export { default as SettingsDeleteButton } from './SettingsDeleteButton/SettingsDeleteButton.svelte';
1818
export { default as UserRequest } from './UserRequest/UserRequest.svelte';
1919
export { default as UploadedPostView } from './UploadedPostView/UploadedPostView.svelte';
20+
export { default as Group } from "./Group/Group.svelte";
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
<script lang="ts">
2+
import { onMount } from 'svelte';
3+
import { ChatMessage, MessageInput } from '$lib/fragments';
4+
import { Avatar, Button } from '$lib/ui';
5+
import { clickOutside } from '$lib/utils';
6+
7+
let messagesContainer: HTMLDivElement;
8+
let messageValue = $state('');
9+
let showMembers = $state(false);
10+
11+
let userId = 'user-1';
12+
13+
let group = {
14+
id: 'group-123',
15+
name: 'Design Team',
16+
avatar: 'https://i.pravatar.cc/150?img=15',
17+
description: 'Discuss all design-related tasks and updates here.',
18+
members: [
19+
{ id: 'user-1', name: 'Alice', avatar: 'https://i.pravatar.cc/150?img=1', role: 'owner' },
20+
{ id: 'user-2', name: 'Bob', avatar: 'https://i.pravatar.cc/150?img=2', role: 'admin' },
21+
{ id: 'user-3', name: 'Charlie', avatar: 'https://i.pravatar.cc/150?img=3', role: 'member' }
22+
]
23+
};
24+
25+
let messages = $state([
26+
{
27+
id: 'msg-1',
28+
isOwn: false,
29+
userImgSrc: 'https://i.pravatar.cc/150?img=2',
30+
time: '2 minutes ago',
31+
message: 'Hey everyone, can we finalize the color palette today?'
32+
},
33+
{
34+
id: 'msg-2',
35+
isOwn: true,
36+
userImgSrc: 'https://i.pravatar.cc/150?img=1',
37+
time: '1 minute ago',
38+
message: 'Yes, I just pushed a new draft to Figma.'
39+
}
40+
]);
41+
42+
let openMenuId = $state<string | null>(null);
43+
44+
function scrollToBottom() {
45+
if (messagesContainer) {
46+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
47+
}
48+
}
49+
50+
onMount(() => {
51+
setTimeout(scrollToBottom, 0);
52+
});
53+
54+
function handleSend() {
55+
if (!messageValue.trim()) return;
56+
messages = [
57+
...messages,
58+
{
59+
id: `msg-${Date.now()}`,
60+
isOwn: true,
61+
userImgSrc: group.members.find((m) => m.id === userId)?.avatar || '',
62+
time: 'just now',
63+
message: messageValue
64+
}
65+
];
66+
messageValue = '';
67+
setTimeout(scrollToBottom, 0);
68+
}
69+
70+
function currentUserRole() {
71+
return group.members.find((m) => m.id === userId)?.role;
72+
}
73+
74+
function canManage(member: { id?: string; name?: string; avatar?: string; role: any; }) {
75+
const current = currentUserRole();
76+
if (member.role === 'owner') return false;
77+
if (current === 'owner') return true;
78+
if (current === 'admin' && member.role === 'member') return true;
79+
return false;
80+
}
81+
82+
function promoteToAdmin(memberId: string) {
83+
const m = group.members.find((m) => m.id === memberId);
84+
if (m && m.role === 'member') m.role = 'admin';
85+
openMenuId = null;
86+
}
87+
88+
function removeMember(memberId: string) {
89+
group.members = group.members.filter((m) => m.id !== memberId || m.role === 'owner');
90+
openMenuId = null;
91+
}
92+
93+
function addMember() {
94+
const newId = `user-${Date.now()}`;
95+
group.members = [
96+
...group.members,
97+
{
98+
id: newId,
99+
name: `New Member ${group.members.length + 1}`,
100+
avatar: `https://i.pravatar.cc/150?u=${newId}`,
101+
role: 'member'
102+
}
103+
];
104+
}
105+
</script>
106+
107+
<section class="flex items-center justify-between gap-4 px-4 py-3 border-b border-gray-200">
108+
<div class="flex items-center gap-4">
109+
<Avatar src={group.avatar} />
110+
<div>
111+
<h1 class="text-lg font-semibold">{group.name}</h1>
112+
<p class="text-sm text-gray-600">{group.description}</p>
113+
</div>
114+
</div>
115+
<Button
116+
variant="secondary"
117+
size="sm"
118+
class="w-[max-content]"
119+
callback={() => {
120+
showMembers = !showMembers;
121+
openMenuId = null;
122+
}}
123+
>
124+
{showMembers ? 'Hide Members' : 'View Members'}
125+
</Button>
126+
</section>
127+
128+
{#if showMembers}
129+
<section class="px-4 py-3 border-b border-gray-200 space-y-4">
130+
{#each group.members as member (member.id)}
131+
<div class="flex items-center justify-between">
132+
<div class="flex items-center gap-3">
133+
<Avatar src={member.avatar} size="sm" />
134+
<div>
135+
<span class="text-sm font-medium">{member.name}</span>
136+
{#if member.role !== 'member'}
137+
<span class="ml-2 text-xs text-gray-500">({member.role})</span>
138+
{/if}
139+
</div>
140+
</div>
141+
142+
{#if canManage(member)}
143+
<div class="relative" use:clickOutside={() => (openMenuId = null)}>
144+
<button
145+
onclick={() => {(openMenuId = openMenuId === member.id ? null : member.id)}}
146+
>
147+
148+
</button>
149+
150+
{#if openMenuId === member.id}
151+
<div class="absolute right-0 mt-2 w-40 rounded-md bg-white shadow-lg border z-10">
152+
<ul class="text-sm">
153+
<!-- svelte-ignore a11y_click_events_have_key_events -->
154+
{#if currentUserRole() === 'owner' && member.role === 'member'}
155+
<!-- svelte-ignore a11y_click_events_have_key_events -->
156+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
157+
<li
158+
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
159+
onclick={() => promoteToAdmin(member.id)}
160+
>
161+
Make admin
162+
</li>
163+
{/if}
164+
<!-- svelte-ignore a11y_click_events_have_key_events -->
165+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
166+
<li
167+
class="px-4 py-2 hover:bg-red-50 text-red-600 cursor-pointer"
168+
onclick={() => removeMember(member.id)}
169+
>
170+
Remove member
171+
</li>
172+
</ul>
173+
</div>
174+
{/if}
175+
</div>
176+
{/if}
177+
</div>
178+
{/each}
179+
180+
<Button size="sm" variant="primary" class="w-[max-content] mt-4" callback={addMember}>
181+
Add Member
182+
</Button>
183+
</section>
184+
{/if}
185+
186+
<section class="chat relative px-0">
187+
<div class="h-[calc(100vh-300px)] mt-4 overflow-auto" bind:this={messagesContainer}>
188+
{#each messages as msg (msg.id)}
189+
<ChatMessage
190+
isOwn={msg.isOwn}
191+
userImgSrc={msg.userImgSrc}
192+
time={msg.time}
193+
message={msg.message}
194+
/>
195+
{/each}
196+
</div>
197+
198+
<MessageInput
199+
class="sticky start-0 bottom-[-15px] w-full"
200+
variant="dm"
201+
src={group.avatar}
202+
bind:value={messageValue}
203+
{handleSend}
204+
/>
205+
</section>

platforms/pictique/src/routes/(protected)/messages/+page.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { onMount } from 'svelte';
55
import { apiClient } from '$lib/utils/axios';
66
import { heading } from '../../store';
7+
import Group from '$lib/fragments/Group/Group.svelte';
78
89
let messages = $state([]);
910
@@ -45,5 +46,7 @@
4546
You don't have any messages yet, please start a Direct Message with Someone by searching
4647
their name
4748
</div>
49+
<!-- group id needs to be added -->
50+
<Group name="Developers" avatar="https://picsum.photos/200/300" unread={true} callback={() => goto("/group/123")}/>
4851
{/if}
4952
</section>

0 commit comments

Comments
 (0)