Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: notification component ui #48

Draft
wants to merge 8 commits into
base: feat/react-components
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/ui/postcss.config.cjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module.exports = {
plugins: {
tailwindcss: {},
tailwindcss: {
config: "tailwind.config.cjs",
},
autoprefixer: {},
},
}
136 changes: 136 additions & 0 deletions packages/ui/src/components/ActivityToast/ActivityToast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { FC, useEffect, useMemo, useState } from "react"
import { CloseIcon } from "../../icons/CloseIcon"
import { SuccessIcon } from "../../icons/SuccessIcon"

type vertical = "top" | "center" | "bottom"
type horizontal = "left" | "center" | "right"

interface ActivityToastProps {
action: string
description: string
showToast?: boolean
closeToast: () => void
duration?: 1000 | 2000 | 3000 | 4000 | 5000
vertical?: vertical
horizontal?: horizontal
}

const durationMap = {
1000: "duration-1000",
2000: "duration-2000",
3000: "duration-3000",
4000: "duration-4000",
5000: "duration-5000",
}

const ActivityToast: FC<ActivityToastProps> = ({
action,
description,
showToast,
closeToast,
duration = 5000,
vertical = "bottom",
horizontal = "center",
}) => {
return showToast ? (
<ActivityToastComponent
action={action}
description={description}
closeToast={closeToast}
duration={duration}
vertical={vertical}
horizontal={horizontal}
/>
) : null
}

const ActivityToastComponent: FC<ActivityToastProps> = ({
action,
description,
closeToast,
duration = 5000,
vertical = "bottom",
horizontal = "center",
}) => {
const [isActive, setIsActive] = useState(true)
useEffect(() => {
// ensure that the toast is visible, setting the width to full for the progression bar
setTimeout(() => setIsActive(false), 10)
const timeout = setTimeout(() => closeToast(), duration)
return () => {
closeToast()
clearTimeout(timeout)
}
}, [])

const position = useMemo(() => {
let v, h

switch (horizontal) {
case "left":
h = `left-2`
break
case "center":
h = `left-2/4 transform -translate-x-2/4`
break
case "right":
h = `right-2`
break
default:
h = `left-2/4`
break
}

switch (vertical) {
case "top":
v = `top-2`
break
case "center":
v = `top-2/4 transform -translate-y-2/4`
break
case "bottom":
v = `bottom-2`
break
default:
v = `top-2/4`
break
}

return `${v} ${h}`
}, [vertical, horizontal])

return (
<div className={`fixed ${position}`}>
<div className="relative p-5 bg-white rounded-lg overflow-hidden shadow-lg border border-solid border-neutrals.200 w-96">
<div className="flex flex-1">
<div className="flex flex-col justify-center mr-4">
{/* TODO: remove hardcoded icon - wait for transaction info */}
<SuccessIcon />
</div>
<div className="flex flex-col items-start">
<h5 className="font-barlow text-xl leading-6 font-semibold">
{action}
</h5>
<p className="font-barlow text-base text-start leading-5 font-medium text-neutral-600 mt-0.5 mb-1">
{description}
</p>
</div>

<div className="flex flex-1" />

<div className="flex flex-col justify-center items-end relative w-6">
<CloseIcon cursor="pointer" onClick={closeToast} />
</div>
</div>
<div
className={`absolute bottom-0 left-0 right-0 h-1 transition-all ${
durationMap[duration]
} ease-linear ${isActive ? "w-full" : "w-0"}`}
style={{ backgroundColor: "#197AA6" }}
/>
</div>
</div>
)
}

export { ActivityToast }
29 changes: 0 additions & 29 deletions packages/ui/src/components/NotificationButton.tsx

This file was deleted.

72 changes: 72 additions & 0 deletions packages/ui/src/components/Notifications/NotificationButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { FC, useEffect, useRef, useState } from "react"
import { BellIcon } from "../../icons/BellIcon"
import { NotificationMenu } from "./NotificationMenu"

interface Notification {
action: string
description: string
isUnread?: boolean
time: string
}

interface NotificationButtonProps {
address?: string
hasNotifications?: boolean
notifications: Notification[]
}

const NotificationButton: FC<NotificationButtonProps> = ({
hasNotifications,
notifications,
}) => {
const [isOpen, setIsOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)

const toggleMenu = () => {
setIsOpen((prev) => !prev)
}

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
setIsOpen(false)
}
}

document.addEventListener("mousedown", handleClickOutside)

return () => {
document.removeEventListener("mousedown", handleClickOutside)
}
}, [])

return (
<div className="relative inline-block" ref={ref}>
<button
onClick={toggleMenu}
className="flex items-center justify-center shadow-list-item rounded-lg h-10 w-10 bg-white"
>
<div className="relative">
<BellIcon className="w-6 h-6" />
{hasNotifications && (
<div className="w-[10px] h-[10px] absolute top-0 right-1 rounded-full p-1 z-10 bg-white">
<div className="w-2 h-2 bg-[#29C5FF] rounded-full absolute transform -translate-y-2/3"></div>
</div>
)}
</div>
</button>
<div
className={`absolute top-10 mt-0.5 w-50 rounded-lg shadow-lg z-20 text-black bg-white ${
!isOpen ? "hidden" : ""
}`}
>
<NotificationMenu
notifications={notifications}
toggleMenu={toggleMenu}
/>
</div>
</div>
)
}

export { NotificationButton }
63 changes: 63 additions & 0 deletions packages/ui/src/components/Notifications/NotificationItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { FC } from "react"
import { SuccessIcon } from "../../icons/SuccessIcon"

interface NotificationItemProps {
action: string
description: string
isUnread?: boolean
time: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onNotificationClick?: (notification: any) => void // TODO: remove any
}

const NotificationItem: FC<NotificationItemProps> = ({
action,
description,
isUnread,
time,
onNotificationClick,
}) => {
const txHash = "0x123" // TODO: remove hardcoded txHash and get from transaction

return (
<button
onClick={() =>
onNotificationClick
? onNotificationClick({})
: window.open(`https://starkscan.co/tx/${txHash}`, "_blank")
}
className="flex items-center px-6 py-5 border border-solid border-neutrals.200 rounded-lg w-full hover:bg-[#F0F0F0]"
>
<div className="flex flex-1">
<div className="flex flex-col justify-center mr-4">
{/* TODO: remove hardcoded icon - wait for transaction info */}
<SuccessIcon />
</div>
<div className="flex flex-col items-start">
<h5 className="font-barlow text-xl leading-6 font-semibold">
{action}
</h5>
<p className="font-barlow text-base text-start leading-5 font-medium text-neutral-600 mt-0.5 mb-1">
{description}
</p>
<p className="font-barlow text-xs leading-4 font-normal text-neutral-400">
{time}
</p>
</div>

<div className="flex flex-1" />

<div className="flex flex-col justify-center items-end relative w-6">
{isUnread && (
<div
className="rounded-full w-2.5 h-2.5 mt-2"
style={{ backgroundColor: "#29C5FF" }}
/>
)}
</div>
</div>
</button>
)
}

export { NotificationItem }
66 changes: 66 additions & 0 deletions packages/ui/src/components/Notifications/NotificationMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { FC, useMemo } from "react"
import { CloseIcon } from "../../icons/CloseIcon"
import { NotificationItem } from "./NotificationItem"

// TODO: discuss structure and service to get transactions informations
interface Notification {
action: string
description: string
isUnread?: boolean
time: string
}

interface NotificationMenuProps {
notifications: Notification[]
toggleMenu: () => void
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onNotificationClick?: (notification: any) => void // TODO: remove any
}

const NotificationMenu: FC<NotificationMenuProps> = ({
notifications,
toggleMenu,
}) => {
const totalUnread = useMemo(
() =>
notifications?.filter((notification) => notification.isUnread).length ??
0,
[notifications],
)

return (
<div className="w-112.5 font-barlow shadow-list-item rounded-lg bg-white">
<div className="flex flex-col px-5 py-4">
<div className="flex justify-center">
<h5 className="text-xl leading-6 font-semibold">Notifications</h5>
{totalUnread > 0 && (
<div
className="flex rounded-full w-6 h-6 justify-center items-center ml-2"
style={{ backgroundColor: "#29C5FF" }}
>
<span className="text-sm font-bold leading-3.5 letter text-white">
{totalUnread}
</span>
</div>
)}
<div className="flex flex-1" />
<CloseIcon cursor="pointer" onClick={toggleMenu} />
</div>

{notifications?.length > 0 ? (
<div className="flex flex-col gap-1 p-2">
{notifications?.map((notification) => (
<NotificationItem {...notification} />
))}
</div>
) : (
<p className="font-barlow text-base text-start leading-5 font-medium text-neutral-600 px-5 pb-3">
No new notifications
</p>
)}
</div>
</div>
)
}

export { NotificationMenu }
Loading