cetirizine

An experimental matrix client written in reactjs utilizing tailwind and storybook
git clone git://archive.git.mtrnord.blog/MTRNord/cetirizine.git
Log | Files | Refs | README | LICENSE

commit 0466f5d4344d41f92f4bb8bb471c60bc306c6a2c
parent 211ceccb9f5bbaea5ef2f4083bfc48f02245f75b
Author: MTRNord <mtrnord1@gmail.com>
Date:   Fri, 21 Apr 2023 22:43:08 +0200

feat: Add roomlist item component

Diffstat:
Msrc/app/hooks.ts | 21+++++++++++++++++++++
Msrc/components/avatar/avatar.stories.tsx | 14+++++++-------
Msrc/components/avatar/avatar.tsx | 20++++++++++----------
Asrc/components/roomList/roomListItem/roomListItem.stories.tsx | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/roomList/roomListItem/roomListItem.tsx | 43+++++++++++++++++++++++++++++++++++++++++++
5 files changed, 232 insertions(+), 17 deletions(-)

diff --git a/src/app/hooks.ts b/src/app/hooks.ts @@ -1,6 +1,27 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import type { RootState, AppDispatch } from './store'; +import { RefObject, useEffect, useMemo, useState } from 'react'; // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch = () => useDispatch<AppDispatch>(); export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; + +export default function useOnScreen(ref: RefObject<HTMLElement>) { + const [isIntersecting, setIntersecting] = useState(false) + + const observer = useMemo(() => new IntersectionObserver( + ([entry]) => setIntersecting(entry.isIntersecting), + { + // TODO: This is probably too much or too less + rootMargin: "70px" + } + ), [ref]) + + + useEffect(() => { + observer.observe(ref.current as Element) + return () => observer.disconnect() + }, []) + + return isIntersecting +} diff --git a/src/components/avatar/avatar.stories.tsx b/src/components/avatar/avatar.stories.tsx @@ -15,7 +15,7 @@ const meta: Meta<typeof Avatar> = { name: "Avatar URL", description: "The URL of the avatar image", }, - userID: { + displayname: { required: true, name: "User ID", description: "The DISPLAY name of the User", @@ -42,7 +42,7 @@ type Story = StoryObj<typeof Avatar>; export const Fallback: Story = { args: { - userID: "test", + displayname: "test", dm: false, online: false, avatarUrl: "" @@ -51,7 +51,7 @@ export const Fallback: Story = { export const Image: Story = { args: { - userID: "test", + displayname: "test", avatarUrl: "https://randomuser.me/api/portraits/men/62.jpg", dm: false, online: false, @@ -60,7 +60,7 @@ export const Image: Story = { export const DMOffline: Story = { args: { - userID: "test", + displayname: "test", avatarUrl: "https://randomuser.me/api/portraits/men/62.jpg", dm: true, online: false, @@ -69,7 +69,7 @@ export const DMOffline: Story = { export const DMOnline: Story = { args: { - userID: "test", + displayname: "test", avatarUrl: "https://randomuser.me/api/portraits/men/62.jpg", dm: true, online: true, @@ -78,7 +78,7 @@ export const DMOnline: Story = { export const DMNoAvatarOffline: Story = { args: { - userID: "test", + displayname: "test", avatarUrl: "", dm: true, online: false, @@ -87,7 +87,7 @@ export const DMNoAvatarOffline: Story = { export const DMNoAvatarOnline: Story = { args: { - userID: "test", + displayname: "test", avatarUrl: "", dm: true, online: true, diff --git a/src/components/avatar/avatar.tsx b/src/components/avatar/avatar.tsx @@ -6,7 +6,7 @@ type AvatarProps = { /** * The displayname of the avatar user */ - userID: string + displayname: string /** * Wether it is a DM or not */ @@ -17,17 +17,17 @@ type AvatarProps = { online: boolean }; -export default function Avatar({ avatarUrl, userID, dm = false, online = false }: AvatarProps) { +export default function Avatar({ avatarUrl, displayname, dm = false, online = false }: AvatarProps) { if (avatarUrl) { return ( - <div className="flex relative w-12 h-12 justify-center items-center m-1 mr-2 text-xl rounded-full text-white"> - <img className="rounded-full" alt={userID} src={avatarUrl} /> + <div className="flex relative w-8 h-8 justify-center items-center m-1 mr-2 text-xl rounded-full text-white"> + <img className="rounded-full" alt={displayname} src={avatarUrl} /> { dm ? ( online ? - <div className="bg-green-500 rounded-full w-3 h-3 absolute bottom-[2px] right-[2px]"></div> : - <div className="bg-red-500 rounded-full w-3 h-3 absolute bottom-[2px] right-[2px]"></div> + <div className="bg-green-500 rounded-full w-3 h-3 absolute bottom-0 right-0"></div> : + <div className="bg-red-500 rounded-full w-3 h-3 absolute bottom-0 right-0"></div> ) : <></> } @@ -35,14 +35,14 @@ export default function Avatar({ avatarUrl, userID, dm = false, online = false } ); } return ( - <div className="flex relative w-12 h-12 bg-orange-500 justify-center items-center m-1 mr-2 text-xl rounded-full text-white"> - {userID[0].toUpperCase()} + <div className="flex relative w-8 h-8 bg-orange-500 justify-center items-center m-1 mr-2 text-xl rounded-full text-white"> + {displayname[0].toUpperCase()} { dm ? ( online ? - <div className="bg-green-500 rounded-full w-3 h-3 absolute bottom-[2px] right-[2px]"></div> : - <div className="bg-red-500 rounded-full w-3 h-3 absolute bottom-[2px] right-[2px]"></div> + <div className="bg-green-500 rounded-full w-3 h-3 absolute bottom-0 right-0"></div> : + <div className="bg-red-500 rounded-full w-3 h-3 absolute bottom-0 right-0"></div> ) : <></> } diff --git a/src/components/roomList/roomListItem/roomListItem.stories.tsx b/src/components/roomList/roomListItem/roomListItem.stories.tsx @@ -0,0 +1,150 @@ +import { Meta, StoryObj } from '@storybook/react'; +import RoomListItem from './roomListItem'; +import { BADGE } from '@geometricpanda/storybook-addon-badges'; + +const meta: Meta<typeof RoomListItem> = { + title: 'Chat/RoomList/Item', + tags: ['autodocs'], + component: RoomListItem, + parameters: { + badges: [BADGE.EXPERIMENTAL] + }, + argTypes: { + avatarUrl: { + required: false, + name: "Avatar URL", + description: "The URL of the avatar image", + }, + displayname: { + required: true, + name: "Displayname", + description: "The displayname of the Room", + }, + dm: { + required: false, + defaultValue: false, + control: "boolean", + name: "DM", + description: "Wether the room is a DM", + }, + online: { + required: false, + defaultValue: false, + control: "boolean", + name: "Online", + description: "Wether the user is online. Only used if dm is true", + }, + active: { + required: false, + defaultValue: false, + control: "boolean", + name: "Active", + description: "Wether the room is currently selected", + }, + } +}; + +export default meta; +type Story = StoryObj<typeof RoomListItem>; + +export const Fallback: Story = { + args: { + displayname: "test", + dm: false, + online: false, + avatarUrl: "", + active: false, + } +}; + +export const FallbackActive: Story = { + args: { + displayname: "test", + dm: false, + online: false, + avatarUrl: "", + active: true, + } +}; + + +export const Image: Story = { + args: { + displayname: "test", + avatarUrl: "https://randomuser.me/api/portraits/men/62.jpg", + dm: false, + online: false, + active: false, + } +}; + + +export const ImageActive: Story = { + args: { + displayname: "test", + avatarUrl: "https://randomuser.me/api/portraits/men/62.jpg", + dm: false, + online: false, + active: true, + } +}; + +export const DMOffline: Story = { + args: { + displayname: "test", + avatarUrl: "https://randomuser.me/api/portraits/men/62.jpg", + dm: true, + online: false, + active: false, + } +}; + +export const DMOfflineActive: Story = { + args: { + displayname: "test", + avatarUrl: "https://randomuser.me/api/portraits/men/62.jpg", + dm: true, + online: false, + active: true, + } +}; + +export const DMOnline: Story = { + args: { + displayname: "test", + avatarUrl: "https://randomuser.me/api/portraits/men/62.jpg", + dm: true, + online: true, + active: false, + } +}; + +export const DMOnlineActive: Story = { + args: { + displayname: "test", + avatarUrl: "https://randomuser.me/api/portraits/men/62.jpg", + dm: true, + online: true, + active: true, + } +}; + +export const DMNoAvatarOffline: Story = { + args: { + displayname: "test", + avatarUrl: "", + dm: true, + online: false, + active: false, + } +}; + +export const DMNoAvatarOnline: Story = { + args: { + displayname: "test", + avatarUrl: "", + dm: true, + online: true, + active: false, + } +}; +\ No newline at end of file diff --git a/src/components/roomList/roomListItem/roomListItem.tsx b/src/components/roomList/roomListItem/roomListItem.tsx @@ -0,0 +1,42 @@ +import Avatar from "../../avatar/avatar"; + +type RoomListItemProps = { + /** + * The URL of the Avatar image + */ + avatarUrl?: string + /** + * The displayname of the room list item + */ + displayname: string + /** + * Wether it is a DM or not + */ + dm: boolean + /** + * Wether the user is online. Only used if dm is true. + */ + online: boolean + /** + * Wether the current room is selected + */ + active: boolean +}; + +export default function RoomListItem({ avatarUrl, displayname, dm = false, online = false, active = false }: RoomListItemProps) { + if (!active) { + return ( + <div className="flex flex-row gap-2 p-1 hover:bg-gray-300 hover:rounded duration-150 ease-in items-center"> + <Avatar avatarUrl={avatarUrl} displayname={displayname} dm={dm} online={online} /> + <span className='text-black font-normal text-xl capitalize'>{displayname}</span> + </div> + ); + } else { + return ( + <div className="flex flex-row gap-2 p-1 bg-gray-300 rounded duration-150 ease-in items-center"> + <Avatar avatarUrl={avatarUrl} displayname={displayname} dm={dm} online={online} /> + <span className='text-black font-normal text-xl capitalize'>{displayname}</span> + </div> + ); + } +} +\ No newline at end of file