diff --git a/packages/ui/postcss.config.cjs b/packages/ui/postcss.config.cjs index 33ad091d..21efa6a4 100644 --- a/packages/ui/postcss.config.cjs +++ b/packages/ui/postcss.config.cjs @@ -1,6 +1,8 @@ module.exports = { plugins: { - tailwindcss: {}, + tailwindcss: { + config: "tailwind.config.cjs", + }, autoprefixer: {}, }, } diff --git a/packages/ui/src/components/ActivityToast/ActivityToast.tsx b/packages/ui/src/components/ActivityToast/ActivityToast.tsx new file mode 100644 index 00000000..acd1faa3 --- /dev/null +++ b/packages/ui/src/components/ActivityToast/ActivityToast.tsx @@ -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 = ({ + action, + description, + showToast, + closeToast, + duration = 5000, + vertical = "bottom", + horizontal = "center", +}) => { + return showToast ? ( + + ) : null +} + +const ActivityToastComponent: FC = ({ + 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 ( +
+
+
+
+ {/* TODO: remove hardcoded icon - wait for transaction info */} + +
+
+
+ {action} +
+

+ {description} +

+
+ +
+ +
+ +
+
+
+
+
+ ) +} + +export { ActivityToast } diff --git a/packages/ui/src/components/NotificationButton.tsx b/packages/ui/src/components/NotificationButton.tsx deleted file mode 100644 index c219cc14..00000000 --- a/packages/ui/src/components/NotificationButton.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { FC } from "react" -import { BellIcon } from "../icons/BellIcon" - -interface NotificationButtonProps { - address?: string - hasNotifications?: boolean -} - -const NotificationButton: FC = ({ - hasNotifications, -}) => { - return ( - - ) -} - -export { NotificationButton } diff --git a/packages/ui/src/components/Notifications/NotificationButton.tsx b/packages/ui/src/components/Notifications/NotificationButton.tsx new file mode 100644 index 00000000..9f66a843 --- /dev/null +++ b/packages/ui/src/components/Notifications/NotificationButton.tsx @@ -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 = ({ + hasNotifications, + notifications, +}) => { + const [isOpen, setIsOpen] = useState(false) + const ref = useRef(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 ( +
+ +
+ +
+
+ ) +} + +export { NotificationButton } diff --git a/packages/ui/src/components/Notifications/NotificationItem.tsx b/packages/ui/src/components/Notifications/NotificationItem.tsx new file mode 100644 index 00000000..fa8c4597 --- /dev/null +++ b/packages/ui/src/components/Notifications/NotificationItem.tsx @@ -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 = ({ + action, + description, + isUnread, + time, + onNotificationClick, +}) => { + const txHash = "0x123" // TODO: remove hardcoded txHash and get from transaction + + return ( + + ) +} + +export { NotificationItem } diff --git a/packages/ui/src/components/Notifications/NotificationMenu.tsx b/packages/ui/src/components/Notifications/NotificationMenu.tsx new file mode 100644 index 00000000..b1d7deaf --- /dev/null +++ b/packages/ui/src/components/Notifications/NotificationMenu.tsx @@ -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 = ({ + notifications, + toggleMenu, +}) => { + const totalUnread = useMemo( + () => + notifications?.filter((notification) => notification.isUnread).length ?? + 0, + [notifications], + ) + + return ( +
+
+
+
Notifications
+ {totalUnread > 0 && ( +
+ + {totalUnread} + +
+ )} +
+ +
+ + {notifications?.length > 0 ? ( +
+ {notifications?.map((notification) => ( + + ))} +
+ ) : ( +

+ No new notifications +

+ )} +
+
+ ) +} + +export { NotificationMenu } diff --git a/packages/ui/src/icons/CloseIcon.tsx b/packages/ui/src/icons/CloseIcon.tsx new file mode 100644 index 00000000..202f271c --- /dev/null +++ b/packages/ui/src/icons/CloseIcon.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from "react" + +const CloseIcon = (props: SVGProps) => ( + + + +) + +export { CloseIcon } diff --git a/packages/ui/src/icons/FailureIcon.tsx b/packages/ui/src/icons/FailureIcon.tsx new file mode 100644 index 00000000..748338f8 --- /dev/null +++ b/packages/ui/src/icons/FailureIcon.tsx @@ -0,0 +1,21 @@ +import type { SVGProps } from "react" + +const FailureIcon = (props: SVGProps) => ( + + + +) + +export { FailureIcon } diff --git a/packages/ui/src/icons/InProgressIcon.tsx b/packages/ui/src/icons/InProgressIcon.tsx new file mode 100644 index 00000000..d0f27416 --- /dev/null +++ b/packages/ui/src/icons/InProgressIcon.tsx @@ -0,0 +1,23 @@ +import type { SVGProps } from "react" + +const InProgressIcon = (props: SVGProps) => ( + + + + +) + +export { InProgressIcon } diff --git a/packages/ui/src/icons/SuccessIcon.tsx b/packages/ui/src/icons/SuccessIcon.tsx new file mode 100644 index 00000000..adacdb80 --- /dev/null +++ b/packages/ui/src/icons/SuccessIcon.tsx @@ -0,0 +1,25 @@ +import type { SVGProps } from "react" + +const SuccessIcon = (props: SVGProps) => ( + + + + +) + +export { SuccessIcon } diff --git a/packages/ui/src/main.ts b/packages/ui/src/main.ts index 6b981ee4..aaef29f0 100644 --- a/packages/ui/src/main.ts +++ b/packages/ui/src/main.ts @@ -3,12 +3,18 @@ import "./styles.css" import { useAccount } from "./hooks/useAccount" import { ConnectButtonProvider } from "./components/WalletContext" import { StarknetkitButton } from "./components/Connect/StarknetkitButton" +import { NotificationButton } from "./components/Notifications/NotificationButton" +import { NotificationMenu } from "./components/Notifications/NotificationMenu" +import { NotificationItem } from "./components/Notifications/NotificationItem" import { ConnectButton } from "./components/Connect/ConnectButton" import { ConnectedButton } from "./components/Connect/ConnectedButton" export { useAccount, ConnectButtonProvider, + NotificationButton, + NotificationMenu, + NotificationItem, ConnectButton, ConnectedButton, StarknetkitButton, diff --git a/packages/ui/src/types/Notification.ts b/packages/ui/src/types/Notification.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/ui/storybook/ActivityToast.stories.tsx b/packages/ui/storybook/ActivityToast.stories.tsx new file mode 100644 index 00000000..18fdf956 --- /dev/null +++ b/packages/ui/storybook/ActivityToast.stories.tsx @@ -0,0 +1,140 @@ +import type { Meta, StoryObj } from "@storybook/react" +import React from "react" + +import { ActivityToast } from "../src/components/ActivityToast/ActivityToast" + +// More on how to set up stories at: https://storybook.js.org/docs/svelte/writing-stories/introduction +const meta: Meta = { + component: ActivityToast, +} + +export default meta +type Story = StoryObj + +// More on writing stories with args: https://storybook.js.org/docs/svelte/writing-stories/args +const ShowToastButton: React.FC<{ + onClick: () => void + label: string +}> = ({ onClick, label }) => { + return ( + + ) +} + +const ActivityToastStory = () => { + // Sets the hooks for both the label and primary props + + const [showToast, setShowToast] = React.useState(false) + const [vertical, setVertical] = React.useState<"top" | "center" | "bottom">( + "bottom", + ) + const [horizontal, setHorizontal] = React.useState< + "left" | "center" | "right" + >("center") + + const clickButton = ( + v: "top" | "center" | "bottom", + h: "left" | "center" | "right", + ) => { + setVertical(v) + setHorizontal(h) + setShowToast(true) + } + + return ( + <> +
+
+ clickButton("top", "left")} + label="Top left" + /> + clickButton("top", "center")} + label="Top center" + /> + clickButton("top", "right")} + label="Top right" + /> +
+
+ clickButton("bottom", "left")} + label="Bottom left" + /> + clickButton("bottom", "center")} + label="Bottom center" + /> + clickButton("bottom", "right")} + label="Bottom right" + /> +
+
+ {" "} + clickButton("center", "center")} + label="Center" + /> +
+ {showToast && ( + setShowToast(false)} + vertical={vertical} + horizontal={horizontal} + duration={1000} + /> + )} +
+ + ) +} + +export const Base: Story = { + render: () => , +} diff --git a/packages/ui/storybook/NotificationButton.stories.ts b/packages/ui/storybook/NotificationButton.stories.ts index 0e26ac62..cc46b2d0 100644 --- a/packages/ui/storybook/NotificationButton.stories.ts +++ b/packages/ui/storybook/NotificationButton.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react" -import { NotificationButton } from "../src/components/NotificationButton" +import { NotificationButton } from "../src/components/Notifications/NotificationButton" // More on how to set up stories at: https://storybook.js.org/docs/svelte/writing-stories/introduction const meta: Meta = { @@ -14,11 +14,48 @@ type Story = StoryObj // More on writing stories with args: https://storybook.js.org/docs/svelte/writing-stories/args export const Base: Story = { - args: {}, + args: { + notifications: [ + { + action: "Send", + description: "0.005 ETH was sent to 0x1234...5678", + time: "Just now", + }, + { + action: "Mint", + description: "A cool nft was minted", + time: "2 minutes ago", + }, + { + action: "Adding liquidity", + description: "0.05 ETH and 100 DAI are being added to the pool", + time: "3 Oct 2023, 11:40 AM", + }, + ], + }, } export const WithNotifications: Story = { args: { hasNotifications: true, + notifications: [ + { + action: "Send", + description: "0.005 ETH was sent to 0x1234...5678", + time: "Just now", + isUnread: true, + }, + { + action: "Mint", + description: "A cool nft was minted", + time: "2 minutes ago", + }, + { + action: "Adding liquidity", + description: "0.05 ETH and 100 DAI are being added to the pool", + time: "3 Oct 2023, 11:40 AM", + isUnread: true, + }, + ], }, } diff --git a/packages/ui/storybook/NotificationItem.stories.ts b/packages/ui/storybook/NotificationItem.stories.ts new file mode 100644 index 00000000..5e2c2717 --- /dev/null +++ b/packages/ui/storybook/NotificationItem.stories.ts @@ -0,0 +1,31 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { NotificationItem } from "../src/components/Notifications/NotificationItem" + +// More on how to set up stories at: https://storybook.js.org/docs/svelte/writing-stories/introduction +const meta: Meta = { + component: NotificationItem, + argTypes: {}, +} + +export default meta +type Story = StoryObj + +// More on writing stories with args: https://storybook.js.org/docs/svelte/writing-stories/args + +export const Base: Story = { + args: { + action: "Send", + description: "0.005 ETH was sent to 0x1234...5678", + time: "just now", + }, +} + +export const Unread: Story = { + args: { + action: "Send", + description: "0.005 ETH was sent to 0x1234...5678", + time: "just now", + isUnread: true, + }, +} diff --git a/packages/ui/storybook/NotificationMenu.stories.ts b/packages/ui/storybook/NotificationMenu.stories.ts new file mode 100644 index 00000000..31a5d8e3 --- /dev/null +++ b/packages/ui/storybook/NotificationMenu.stories.ts @@ -0,0 +1,60 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { NotificationMenu } from "../src/components/Notifications/NotificationMenu" + +// More on how to set up stories at: https://storybook.js.org/docs/svelte/writing-stories/introduction +const meta: Meta = { + component: NotificationMenu, + argTypes: {}, +} + +export default meta +type Story = StoryObj + +// More on writing stories with args: https://storybook.js.org/docs/svelte/writing-stories/args + +export const Base: Story = { + args: { + notifications: [ + { + action: "Send", + description: "0.005 ETH was sent to 0x1234...5678", + time: "Just now", + }, + { + action: "Mint", + description: "A cool nft was minted", + time: "2 minutes ago", + }, + { + action: "Adding liquidity", + description: "0.05 ETH and 100 DAI are being added to the pool", + time: "3 Oct 2023, 11:40 AM", + }, + ], + }, +} + +export const WithNotifications: Story = { + args: { + notifications: [ + { + action: "Send", + description: "0.005 ETH was sent to 0x1234...5678", + time: "Just now", + isUnread: true, + }, + { + action: "Mint", + description: "A cool nft was minted", + time: "2 minutes ago", + }, + { + action: "Adding liquidity", + description: "0.05 ETH and 100 DAI are being added to the pool", + time: "3 Oct 2023, 11:40 AM", + isUnread: true, + }, + ], + }, +} diff --git a/packages/ui/tailwind.config.cjs b/packages/ui/tailwind.config.cjs index c4b596cd..2eba889a 100644 --- a/packages/ui/tailwind.config.cjs +++ b/packages/ui/tailwind.config.cjs @@ -17,10 +17,24 @@ module.exports = { }, width: { 50: "12.5rem", + 112.5: "28.125rem", + }, + textColor: { + "neutrals.400": "#8C8C8C", + "neutrals.600": "#595959", + }, + leading: { + 3.5: "0.875rem", }, borderColor: { "neutrals.200": "#F0F0F0", }, + transitionDuration: { + 2000: "2000ms", + 3000: "3000ms", + 4000: "4000ms", + 5000: "5000ms", + }, }, }, plugins: [],