Skip to content
Closed
59 changes: 59 additions & 0 deletions platforms/pictique/src/lib/fragments/Group/Group.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import Avatar from '../../ui/Avatar/Avatar.svelte';
import { cn } from '../../utils';

interface IGroupProps extends HTMLAttributes<HTMLButtonElement> {
avatar: string;
name: string;
unread?: boolean;
callback: () => void;
}

const {
avatar,
name,
unread = false,
callback,
...restProps
}: IGroupProps = $props();
</script>

<button
{...restProps}
class={cn([
'relative flex w-full cursor-pointer items-center gap-3 rounded-lg py-4 px-2',
restProps.class
])}
onclick={callback}
>
<Avatar src={avatar} alt="Group Avatar" size="md" />
<span class="flex w-full items-center justify-between">
<h2 class="text-left font-medium">{name}</h2>
{#if unread}
<span class="h-2 w-2 rounded-full bg-blue-500"></span>
{/if}
</span>
</button>

<!--
@component
@name GroupItem
@description A group item component that displays a group avatar and name, with an optional unread indicator.
@props
- avatar: string - The URL of the group avatar image.
- name: string - The group name.
- unread: boolean - Optional. Indicates if there are unread messages. Defaults to false.
- callback: () => void - Function to call when the group is clicked.
@usage
<script>
import GroupItem from '$lib/ui/GroupItem.svelte';
</script>

<GroupItem
avatar="https://example.com/group-avatar.jpg"
name="Study Buddies"
unread={true}
callback={() => console.log('Group clicked')}
/>
-->
1 change: 1 addition & 0 deletions platforms/pictique/src/lib/fragments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export { default as Comment } from './Comment/Comment.svelte';
export { default as SettingsDeleteButton } from './SettingsDeleteButton/SettingsDeleteButton.svelte';
export { default as UserRequest } from './UserRequest/UserRequest.svelte';
export { default as UploadedPostView } from './UploadedPostView/UploadedPostView.svelte';
export { default as Group } from "./Group/Group.svelte";
111 changes: 111 additions & 0 deletions platforms/pictique/src/routes/(protected)/group/[id]/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<script lang="ts">
import { onMount } from 'svelte';
import { ChatMessage, MessageInput } from '$lib/fragments';
import { Avatar, Button } from '$lib/ui';
import { goto } from '$app/navigation';
import { page } from '$app/state';

let messagesContainer: HTMLDivElement;
let messageValue = $state('');

let userId = 'user-1';
let id = page.params.id;

let group = {
id: 'group-123',
name: 'Design Team',
avatar: 'https://i.pravatar.cc/150?img=15',
description: 'Discuss all design-related tasks and updates here.',
members: [
{ id: 'user-1', name: 'Alice', avatar: 'https://i.pravatar.cc/150?img=1', role: 'owner' },
{ id: 'user-2', name: 'Bob', avatar: 'https://i.pravatar.cc/150?img=2', role: 'admin' },
{ id: 'user-3', name: 'Charlie', avatar: 'https://i.pravatar.cc/150?img=3', role: 'member' }
]
};

let messages = $state([
{
id: 'msg-1',
isOwn: false,
userImgSrc: 'https://i.pravatar.cc/150?img=2',
time: '2 minutes ago',
message: 'Hey everyone, can we finalize the color palette today?'
},
{
id: 'msg-2',
isOwn: true,
userImgSrc: 'https://i.pravatar.cc/150?img=1',
time: '1 minute ago',
message: 'Yes, I just pushed a new draft to Figma.'
}
]);

let openMenuId = $state<string | null>(null);

function scrollToBottom() {
if (messagesContainer) {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
}

onMount(() => {
setTimeout(scrollToBottom, 0);
});

function handleSend() {
if (!messageValue.trim()) return;
messages = [
...messages,
{
id: `msg-${Date.now()}`,
isOwn: true,
userImgSrc: group.members.find((m) => m.id === userId)?.avatar || '',
time: 'just now',
message: messageValue
}
];
messageValue = '';
setTimeout(scrollToBottom, 0);
}
</script>

<section class="flex items-center justify-between gap-4 px-4 py-3 border-b border-gray-200">
<div class="flex items-center gap-4">
<Avatar src={group.avatar} />
<div>
<h1 class="text-lg font-semibold">{group.name}</h1>
<p class="text-sm text-gray-600">{group.description}</p>
</div>
</div>
<Button
variant="secondary"
size="sm"
class="w-[max-content]"
callback={() => {
goto(`/group/${id}/members`)
}}
>
View Members
</Button>
</section>

<section class="chat relative px-0">
<div class="h-[calc(100vh-300px)] mt-4 overflow-auto" bind:this={messagesContainer}>
{#each messages as msg (msg.id)}
<ChatMessage
isOwn={msg.isOwn}
userImgSrc={msg.userImgSrc}
time={msg.time}
message={msg.message}
/>
{/each}
</div>

<MessageInput
class="sticky start-0 bottom-[-15px] w-full"
variant="dm"
src={group.avatar}
bind:value={messageValue}
{handleSend}
/>
</section>
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<script lang="ts">
import { page } from '$app/state';
import { Avatar, Button } from '$lib/ui';
import { clickOutside } from '$lib/utils';
let groupId = page.params.id;
let userId = 'user-1'; // Simulated current user
let group = $state({
id: groupId,
name: 'Design Team',
avatar: 'https://i.pravatar.cc/150?img=15',
description: 'Discuss all design-related tasks and updates here.',
members: [
{ id: 'user-1', name: 'Alice', avatar: 'https://i.pravatar.cc/150?img=1', role: 'owner' },
{ id: 'user-2', name: 'Bob', avatar: 'https://i.pravatar.cc/150?img=2', role: 'admin' },
{ id: 'user-3', name: 'Charlie', avatar: 'https://i.pravatar.cc/150?img=3', role: 'member' }
]
});
let openMenuId = $state<string | null>(null);
function currentUserRole() {
return group.members.find((m) => m.id === userId)?.role;
}
function canManage(member: { id?: string; name?: string; avatar?: string; role: any; }) {
const current = currentUserRole();
if (member.role === 'owner') return false;
if (current === 'owner') return true;
if (current === 'admin' && member.role === 'member') return true;
return false;
}
function promoteToAdmin(memberId: string) {
const m = group.members.find((m) => m.id === memberId);
if (m && m.role === 'member') m.role = 'admin';
openMenuId = null;
}
function removeMember(memberId: string) {
group.members = group.members.filter((m) => m.id !== memberId || m.role === 'owner');
openMenuId = null;
}
function addMember() {
const newId = `user-${Date.now()}`;
group.members = [
...group.members,
{
id: newId,
name: `New Member ${group.members.length + 1}`,
avatar: `https://i.pravatar.cc/150?u=${newId}`,
role: 'member'
}
];
}
</script>

<section class="mx-auto w-full h-[80vh] flex flex-col justify-between">
<div>
<div class="flex items-center gap-4 mb-6">
<Avatar src={group.avatar} />
<div>
<h1 class="text-xl font-semibold">{group.name}</h1>
<p class="text-sm text-gray-600">{group.description}</p>
</div>
</div>

<div class="space-y-6">
{#each group.members as member (member.id)}
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<Avatar src={member.avatar} size="sm" />
<div>
<p class="text-sm font-medium">{member.name}</p>
{#if member.role !== 'member'}
<p class="text-xs text-gray-500">{member.role}</p>
{/if}
</div>
</div>

{#if canManage(member)}
<div class="relative" use:clickOutside={() => (openMenuId = null)}>
<button
onclick={() => (openMenuId = openMenuId === member.id ? null : member.id)}
>
</button>

{#if openMenuId === member.id}
<div class="absolute right-0 mt-2 w-40 rounded-md bg-white shadow-lg border z-10">
<!-- svelte-ignore a11y_click_events_have_key_events -->
<ul class="text-sm">
{#if currentUserRole() === 'owner' && member.role === 'member'}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<li
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
onclick={() => promoteToAdmin(member.id)}
>
Make admin
</li>
{/if}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<li
class="px-4 py-2 hover:bg-red-50 text-red-600 cursor-pointer"
onclick={() => removeMember(member.id)}
>
Remove member
</li>
</ul>
</div>
{/if}
</div>
{/if}
</div>
{/each}
</div>
</div>

<div class="flex justify-center">
<Button size="sm" variant="secondary" class="mt-6" callback={addMember}>
Add Member
</Button>
</div>
</section>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { onMount } from 'svelte';
import { apiClient } from '$lib/utils/axios';
import { heading } from '../../store';
import Group from '$lib/fragments/Group/Group.svelte';
let messages = $state([]);
Expand Down Expand Up @@ -45,5 +46,7 @@
You don't have any messages yet, please start a Direct Message with Someone by searching
their name
</div>
<!-- group id needs to be added -->
<Group name="Developers" avatar="https://picsum.photos/200/300" unread={true} callback={() => goto("/group/123")}/>
{/if}
</section>
Loading