Cara Membina Peti Masuk Pemberitahuan Seperti Notion dengan UI Chakra dan Novu

DDD
Lepaskan: 2024-10-03 06:20:02
asal
904 orang telah melayarinya

TL;DR

Dalam siaran pendek ini, anda akan belajar tentang cara saya mencipta semula komponen peti masuk pemberitahuan masa nyata berfungsi sepenuhnya yang mereplikasi keupayaan pemberitahuan terbina dalam Notion dengan hanya menggunakan UI Chakra untuk reka bentuk dan Novu untuk pemberitahuan.

Beginilah rupanya:

How to Build a Notion-Like Notification Inbox with Chakra UI and Novu

Pautan ke kod sumber dan versi apl ini digunakan berada di penghujung siaran.

Sebagai pengguna Notion harian, saya sangat menghargai pengalaman pemberitahuan mereka, dan ini adalah sebahagian besar mengapa saya menggunakan apl mereka dengan begitu banyak. Saya ingin tahu—bagaimanakah saya boleh mencipta semula peti masuk yang serupa dengan sistem pemberitahuan anggun Notion? Ternyata, ia agak mudah, terima kasih kepada komponen pemberitahuan dalam apl Novu.

Novu baru-baru ini melancarkan komponen pemberitahuan tindanan penuh mereka—komponen React stateful, boleh dibenamkan atau widget yang boleh disesuaikan dan sedia untuk digunakan.

Begini cara anda boleh menambahkannya pada apl React anda hanya dalam beberapa langkah mudah:

  1. Pasang pakej Novu

    $ npm install @novu/react
    
    Salin selepas log masuk
  2. Import komponen

    import { Inbox } from "@novu/react";
    
    Salin selepas log masuk
  3. Mulakan komponen dalam apl anda

    function Novu() {
      return (
        <Inbox
          applicationIdentifier="YOUR_APPLICATION_IDENTIFIER"
          subscriberId="YOUR_SUBSCRIBER_ID"
        />
      );
    }
    
    Salin selepas log masuk

Itu sahaja! Anda kini mempunyai komponen peti masuk dalam apl yang berfungsi sepenuhnya.

Di luar kotak, ia kelihatan sangat hebat:

How to Build a Notion-Like Notification Inbox with Chakra UI and Novu

Bukan untuk bermegah, tetapi Peti Masuk Novu juga merupakan yang paling fleksibel dan boleh disesuaikan di luar sana. Lihat cara menggayakannya, malah mencuba sendiri.

Jika anda berminat dengan teknologi di sebaliknya, Pengasas Bersama Novu Dima Grossman menulis siaran yang bagus tentang cara dan sebab mereka membinanya.


Menggayakan Peti Masuk anda seperti Notion

Mahu peti masuk anda kelihatan seperti panel pemberitahuan Notion? Bukan masalah! Anda boleh membungkus dan menyesuaikan pemberitahuan Novu dengan mudah agar sesuai dengan estetika minimum Notion yang bersih.

How to Build a Notion-Like Notification Inbox with Chakra UI and Novu

Bagaimana saya melakukannya

Daripada hanya mengimport komponen Peti Masuk daripada @novu/react, saya membawa masuk komponen Pemberitahuan dan Pemberitahuan untuk kawalan penuh ke atas pemaparan setiap item.

import { Inbox, Notification, Notifications } from "@novu/react";
Salin selepas log masuk

Apa yang ada di dalam pemberitahuan?

Sebelum kami mula menyesuaikan, berikut ialah struktur objek pemberitahuan:

interface Notification = {
  id: string;
  subject?: string;
  body: string;
  to: Subscriber;
  isRead: boolean;
  isArchived: boolean;
  createdAt: string;
  readAt?: string | null;
  archivedAt?: string | null;
  avatar?: string;
  primaryAction?: Action;
  secondaryAction?: Action;
  channelType: ChannelType;
  tags?: string[];
  data?: Record<string, unknown>;
  redirect?: Redirect;
};
Salin selepas log masuk

Berbekalkan ini, saya menggunakan UI Chakra (kerana kelas Tailwind yang bercakaran memenatkan) untuk mereka bentuk setiap item pemberitahuan.


Komponen item peti masuk tersuai

Begini cara saya mencipta item pemberitahuan yang diilhamkan oleh Notion:

const InboxItem = ({ notification }: { notification: Notification }) => {
    const [isHovered, setIsHovered] = useState(false);
    const notificationType = notification.tags?.[0];

    return (
        <Box
            p={2}
            bg="white"
            position="relative"
            onMouseEnter={() => setIsHovered(true)}
            onMouseLeave={() => setIsHovered(false)}
        >
            <Flex align="flex-start" position="relative">
                <VStack spacing={0} position="absolute" top="0" right="0">
                    {isHovered && (
                        <Box bg="white" display="flex" gap={1}>
                            {notification.isRead ? (
                                <IconButton
                                    aria-label="Mark as unread"
                                    icon={<PiNotificationFill />}
                                    onClick={() => notification.unread()}
                                    size="sm"
                                    variant="ghost"
                                />
                            ) : (
                                <IconButton
                                    aria-label="Mark as read"
                                    icon={<FaRegCheckSquare />}
                                    onClick={() => notification.read()}
                                    size="sm"
                                    variant="ghost"
                                />
                            )}
                            {notification.isArchived ? (
                                <IconButton
                                    aria-label="Unarchive"
                                    icon={<PiNotificationFill />}
                                    onClick={() => notification.unarchive()}
                                    size="sm"
                                    variant="ghost"
                                />
                            ) : (
                                <IconButton
                                    aria-label="Archive"
                                    icon={<FiArchive />}
                                    onClick={() => notification.archive()}
                                    size="sm"
                                    variant="ghost"
                                />
                            )}
                        </Box>
                    )}
                </VStack>

                <Box
                    position="relative"
                    display="flex"
                    alignItems="center"
                    mr="8px"
                    height="26px"
                >
                    {!notification.isRead && (
                        <Box>
                            <Box width="8px" height="8px" bg="blue.500" borderRadius="full" />
                        </Box>
                    )}
                    {notification.avatar !== undefined && (
                        <Avatar
                            width="24px"
                            height="24px"
                            marginLeft="8px"
                            name={notification.to.firstName}
                            src={notification.avatar || undefined}
                        />
                    )}
                </Box>

                <VStack align="start" spacing="8px" flex="1" mt="3px">
                    <Flex justify="space-between" width="100%">
                        <Text fontSize="14px" color="gray.800" fontWeight="600">
                            {notification.subject}
                        </Text>
                        <Text fontSize="xs" color="gray.400">
                            {formatTime(notification.createdAt)}
                        </Text>
                    </Flex>

                    {notificationType !== "Mention" &&
                        notificationType !== "Comment" &&
                        notificationType !== "Invite" && (
                            <Text fontSize="14px" color="gray.800">
                                {notification.body}
                            </Text>
                        )}

                    {(notificationType === "Mention" ||
                        notificationType === "Comment") && (
                            <Button
                                variant="ghost"
                                size="sm"
                                leftIcon={<GrDocumentText />}
                                _hover={{ bg: "rgba(0, 0, 0, 0.03)" }}
                                pl="2px"
                                pr="5px"
                                height="25px"
                            >
                                <Text
                                    fontSize="14px"
                                    color="gray.800"
                                    fontWeight="500"
                                    backgroundImage="linear-gradient(to right, rgba(55, 53, 47, 0.16) 0%, rgba(55, 53, 47, 0.16) 100%)"
                                    backgroundRepeat="repeat-x"
                                    backgroundSize="100% 1px"
                                    backgroundPosition="0 100%"
                                    mr="-2px"
                                >
                                    {notification.body}
                                </Text>
                            </Button>
                        )}

                    {notificationType === "Invite" && (
                        <Button
                            variant="outline"
                            size="md"
                            _hover={{ bg: "rgba(0, 0, 0, 0.03)" }}
                            padding="12px"
                            height="50px"
                            fontSize="14px"
                            width="100%"
                            borderRadius="8px"
                            textAlign="left"
                            border="1px solid rgba(227, 226, 224, 0.5)"
                            justifyContent="space-between"
                        >
                            {notification.body}
                        </Button>
                    )}

                    {notificationType === "Comment" && (
                        <Box>
                            <Text fontSize="12px" color="rgb(120, 119, 116)" fontWeight="400">
                                John Doe
                            </Text>
                            <Text fontSize="14px" color="rgb(55, 53, 47)" fontWeight="400">
                                This is a notification Comment made by John Doe and posted on
                                the page Top Secret Project
                            </Text>
                        </Box>
                    )}

                    <HStack spacing={3}>
                        {notification.primaryAction && (
                            <Button
                                variant="outline"
                                size="xs"
                                colorScheme="gray"
                                borderRadius="md"
                                borderColor="gray.300"
                                _hover={{ bg: "gray.100" }}
                                paddingRight="8px"
                                paddingLeft="8px"
                                lineHeight="26px"
                                height="26px"
                            >
                                {notification.primaryAction.label}
                            </Button>
                        )}
                        {notification.secondaryAction && (
                            <Button
                                variant="ghost"
                                size="xs"
                                colorScheme="gray"
                                borderRadius="md"
                                borderColor="gray.300"
                                _hover={{ bg: "gray.100" }}
                                paddingRight="8px"
                                paddingLeft="8px"
                                lineHeight="26px"
                                height="26px"
                            >
                                {notification.secondaryAction.label}
                            </Button>
                        )}
                    </HStack>
                </VStack>
            </Flex>
        </Box>
    );
};
Salin selepas log masuk

Kekunci objek pemberitahuan

Seperti yang anda lihat dalam kod, saya menggunakan kekunci pemberitahuan berikut:

  • notifikasi.tag
  • pemberitahuan.isRead
  • notification.isArchived
  • pemberitahuan.kepada.Nama pertama
  • pemberitahuan.avatar
  • pemberitahuan.subjek
  • pemberitahuan.createdAt
  • pemberitahuan.badan
  • pemberitahuan.primaryAction
  • label pemberitahuan.primaryAction.
  • pemberitahuan.secondaryAction
  • label pemberitahuan.secondaryAction.

Objek notification.data boleh mengandungi sebarang maklumat praktikal yang logik aplikasi anda ingin kaitkan dengan pengguna atau pelanggan. Struktur fleksibel ini membolehkan anda menyesuaikan pemberitahuan kepada kes penggunaan tertentu dan memberikan maklumat yang lebih kaya dan kontekstual kepada pengguna anda.

Contoh penggunaan notification.data:

  1. Kemas kini pesanan e-dagang:

    notification.data = {
      orderId: "ORD-12345",
      status: "shipped",
      trackingNumber: "1Z999AA1234567890",
      estimatedDelivery: "2023-09-25"
    };
    
    Salin selepas log masuk
  2. Interaksi media sosial:

    notification.data = {
      postId: "post-789",
      interactionType: "like",
      interactingUser: "johndoe",
      interactionTime: "2023-09-22T14:30:00Z"
    };
    
    Salin selepas log masuk
  3. Urus niaga kewangan:

    notification.data = {
      transactionId: "TRX-98765",
      amount: 150.75,
      currency: "USD",
      merchantName: "Coffee Shop",
      category: "Food & Drink"
    };
    
    Salin selepas log masuk

Dengan menggunakan objek notification.data, anda boleh membuat pemberitahuan yang lebih bermaklumat dan boleh diambil tindakan yang disepadukan dengan lancar dengan keperluan khusus aplikasi anda.

Fleksibiliti ini membolehkan anda memberikan pengguna maklumat yang mereka perlukan dengan tepat, meningkatkan pengalaman mereka dan keberkesanan keseluruhan sistem pemberitahuan anda.

Menggunakan cangkuk untuk pengurusan pemberitahuan

Jika anda telah meneliti kod itu dengan teliti, anda mungkin perasan penggunaan empat cangkuk kekunci untuk mengurus keadaan pemberitahuan:

  • notification.unread()
  • notification.read()
  • notification.unarchive()
  • notification.archive()

The novu/react package exposes these hooks, offering enhanced flexibility for managing notification states. It's important to note that these hooks not only update the local state but also synchronize changes with the backend.

These hooks provide a seamless way to:

  • Mark notifications as read or unread
  • Archive or unarchive notifications

By utilizing these hooks, you can create more interactive and responsive notification systems in your applications.

I've implemented Notion-inspired sidebar navigation to enhance the similarity to the Notion theme. This design choice aims to capture the essence and aesthetics of Notion's interface, creating a familiar and intuitive environment for users.

For the icons, I've leveraged the versatile react-icons library, which offers a wide range of icon sets to choose from.

$ npm install react-icons
Salin selepas log masuk
import { FiArchive, FiSearch, FiHome, FiInbox, FiSettings, FiChevronDown } from "react-icons/fi";
import { FaRegCheckSquare, FaUserFriends } from "react-icons/fa";
import { PiNotificationFill } from "react-icons/pi";
import { BsFillFileTextFill, BsTrash } from "react-icons/bs";
import { AiOutlineCalendar } from "react-icons/ai";
import { GrDocumentText } from "react-icons/gr";

const AppContainer = () => {
    const borderColor = useColorModeValue("gray.200", "gray.700");
    const [isInboxOpen, setIsInboxOpen] = useState(true);

    const toggleInbox = () => {
        setIsInboxOpen(!isInboxOpen);
    };

    return (
        <Flex
            width="100vw"
            height="100vh"
            bg="gray.100"
            overflow="hidden"
            justifyContent="center"
            alignItems="center"
            style={{
                fontFamily:
                    'ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI Variable Display", "Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol"',
            }}
        >
            <Box
                width="80%"
                height="80%"
                bg="white"
                borderRadius="lg"
                boxShadow="xl"
                overflow="hidden"
            >
                <Flex height="100%">
                    {/* Sidebar */}
                    <Box
                        width="240px"
                        bg="rgb(247, 247, 245)"
                        padding="8px"
                        display="flex"
                        flexDirection="column"
                        borderColor={borderColor}
                        borderRightWidth="1px"
                    >
                        <Flex alignItems="center" mb="4px" padding="0.6rem">
                            <Text
                                fontSize="1.25rem"
                                fontWeight="bold"
                                color="rgb(55, 53, 47)"
                            >
                                <Icon
                                    as={NotionIcon}
                                    sx={{
                                        width: "20px",
                                        height: "20px",
                                        marginRight: "8px",
                                        display: "inline-block",
                                    }}
                                />{" "}
                                Workspace
                            </Text>
                            <IconButton
                                aria-label="User Settings"
                                icon={<FiChevronDown />}
                                variant="ghost"
                                size="sm"
                            />
                        </Flex>

                        <VStack align="stretch" spacing={1} mb="15px">
                            <SidebarItem icon={FiSearch} label="Search" />
                            <SidebarItem icon={FiHome} label="Home" />
                            <SidebarItem icon={FiInbox} label="Inbox" isActive={isInboxOpen} onClick={toggleInbox} />
                            <SidebarItem icon={FiSettings} label="Settings & members" />
                        </VStack>

                        <Text fontSize="xs" fontWeight="bold" color="gray.500" mb={2}>
                            Favorites
                        </Text>
                        <VStack align="stretch" spacing={1} mb="15px">
                            <SidebarItem icon={FiHome} label="Teamspaces" />
                            <SidebarItem icon={BsFillFileTextFill} label="Shared" />
                        </VStack>

                        <Text fontSize="xs" fontWeight="bold" color="gray.500" mb={2}>
                            Private
                        </Text>
                        <VStack align="stretch" spacing={1} mb="15px">
                            <SidebarItem icon={AiOutlineCalendar} label="Calendar" />
                            <SidebarItem icon={FaUserFriends} label="Templates" />
                            <SidebarItem icon={BsTrash} label="Trash" />
                        </VStack>
                    </Box>

// ... (rest of the code)
Salin selepas log masuk

Another important aspect was time formatting to match how Notion does it:

function formatTime(timestamp: number | string | Date): string {
    const date = new Date(timestamp);
    const now = new Date();
    const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
    const secondsInMinute = 60;
    const secondsInHour = secondsInMinute * 60;
    const secondsInDay = secondsInHour * 24;
    const secondsInWeek = secondsInDay * 7;
    const secondsInYear = secondsInDay * 365;

    if (diffInSeconds < secondsInMinute) {
        return `${diffInSeconds} seconds`;
    } else if (diffInSeconds < secondsInHour) {
        const minutes = Math.floor(diffInSeconds / secondsInMinute);
        return `${minutes} minute${minutes !== 1 ? 's' : ''}`;
    } else if (diffInSeconds < secondsInDay) {
        const hours = Math.floor(diffInSeconds / secondsInHour);
        return `${hours} hour${hours !== 1 ? 's' : ''}`;
    } else if (diffInSeconds < secondsInWeek) {
        const days = Math.floor(diffInSeconds / secondsInDay);
        return `${days} day${days !== 1 ? 's' : ''}`;
    } else if (diffInSeconds < secondsInYear) {
        const options: Intl.DateTimeFormatOptions = { month: "short", day: "numeric" };
        return date.toLocaleDateString(undefined, options);
    } else {
        return date.getFullYear().toString();
    }
}
Salin selepas log masuk

Now that we have covered all the pieces, here is the complete code:

'use client'
import React, { useState } from 'react';
import {
    Box,
    Flex,
    Text,
    IconButton,
    VStack,
    Avatar,
    HStack,
    Link,
    Icon,
    useColorModeValue,
    Button,
    Heading,
} from "@chakra-ui/react";
import { FiArchive, FiSearch, FiHome, FiInbox, FiSettings, FiChevronDown } from "react-icons/fi";
import { FaRegCheckSquare, FaUserFriends } from "react-icons/fa";
import { PiNotificationFill } from "react-icons/pi";
import { BsFillFileTextFill, BsTrash } from "react-icons/bs";
import { AiOutlineCalendar } from "react-icons/ai";
import { GrDocumentText } from "react-icons/gr";
import { Inbox, Notification, Notifications } from "@novu/react";
import { NotionIcon } from "../icons/Notion";

const subscriberId = process.env.NEXT_PUBLIC_SUBSCRIBERID;
const applicationIdentifier = process.env.NEXT_PUBLIC_NOVU_CLIENT_APP_ID;

const AppContainer = () => {
    const borderColor = useColorModeValue("gray.200", "gray.700");
    const [isInboxOpen, setIsInboxOpen] = useState(true);

    const toggleInbox = () => {
        setIsInboxOpen(!isInboxOpen);
    };

    return (
        
            
                
                    {/* Sidebar */}
                    
                        
                            
                                {" "}
                                Workspace
                            
                            }
                                variant="ghost"
                                size="sm"
                            />
                        

                        
                            
                            
                            
                            
                        

                        
                            Favorites
                        
                        
                            
                            
                        

                        
                            Private
                        
                        
                            
                            
                            
                        
                    

                    {/* Main Content Area */}
                    
                        {/* Injected Content Behind the Inbox */}
                        
                            
                                Notion Inbox Notification Theme
                            
                            
                                Checkout the deployed version now
                            
                            
                        

                        {/* Inbox Popover */}
                        {isInboxOpen && (
                            
                                
                                     (
                                            
                                        )}
                                    />
                                

                            
                        )}
                    
                
            
        
    );
};

// Sidebar Item Component
interface SidebarItemProps {
    icon: React.ElementType;
    label: string;
    isActive?: boolean;
    external?: boolean;
    onClick?: () => void;
}

const SidebarItem: React.FC = ({
    icon,
    label,
    isActive = false,
    external = false,
    onClick,
}) => {
    return (
        
            
            {label}
        
    );
};

const InboxItem = ({ notification }: { notification: Notification }) => {
    const [isHovered, setIsHovered] = useState(false);
    const notificationType = notification.tags?.[0];

    return (
        <Box
            p={2}
            bg="white"
            position="relative"
            onMouseEnter={() => setIsHovered(true)}
            onMouseLeave={() => setIsHovered(false)}
        >
            <Flex align="flex-start" position="relative">
                <VStack spacing={0} position="absolute" top="0" right="0">
                    {isHovered && (
                        <Box bg="white" display="flex" gap={1}>
                            {notification.isRead ? (
                                <IconButton
                                    aria-label="Mark as unread"
                                    icon={<PiNotificationFill />}
                                    onClick={() => notification.unread()}
                                    size="sm"
                                    variant="ghost"
                                />
                            ) : (
                                <IconButton
                                    aria-label="Mark as read"
                                    icon={<FaRegCheckSquare />}
                                    onClick={() => notification.read()}
                                    size="sm"
                                    variant="ghost"
                                />
                            )}
                            {notification.isArchived ? (
                                <IconButton
                                    aria-label="Unarchive"
                                    icon={<PiNotificationFill />}
                                    onClick={() => notification.unarchive()}
                                    size="sm"
                                    variant="ghost"
                                />
                            ) : (
                                <IconButton
                                    aria-label="Archive"
                                    icon={<FiArchive />}
                                    onClick={() => notification.archive()}
                                    size="sm"
                                    variant="ghost"
                                />
                            )}
                        </Box>
                    )}
                </VStack>

                <Box
                    position="relative"
                    display="flex"
                    alignItems="center"
                    mr="8px"
                    height="26px"
                >
                    {!notification.isRead && (
                        <Box>
                            <Box width="8px" height="8px" bg="blue.500" borderRadius="full" />
                        </Box>
                    )}
                    {notification.avatar !== undefined && (
                        <Avatar
                            width="24px"
                            height="24px"
                            marginLeft="8px"
                            name={notification.to.firstName}
                            src={notification.avatar || undefined}
                        />
                    )}
                </Box>

                <VStack align="start" spacing="8px" flex="1" mt="3px">
                    <Flex justify="space-between" width="100%">
                        <Text fontSize="14px" color="gray.800" fontWeight="600">
                            {notification.subject}
                        </Text>
                        <Text fontSize="xs" color="gray.400">
                            {formatTime(notification.createdAt)}
                        </Text>
                    </Flex>

                    {notificationType !== "Mention" &&
                        notificationType !== "Comment" &&
                        notificationType !== "Invite" && (
                            <Text fontSize="14px" color="gray.800">
                                {notification.body}
                            </Text>
                        )}

                    {(notificationType === "Mention" ||
                        notificationType === "Comment") && (
                            <Button
                                variant="ghost"
                                size="sm"
                                leftIcon={<GrDocumentText />}
                                _hover={{ bg: "rgba(0, 0, 0, 0.03)" }}
                                pl="2px"
                                pr="5px"
                                height="25px"
                            >
                                <Text
                                    fontSize="14px"
                                    color="gray.800"
                                    fontWeight="500"
                                    backgroundImage="linear-gradient(to right, rgba(55, 53, 47, 0.16) 0%, rgba(55, 53, 47, 0.16) 100%)"
                                    backgroundRepeat="repeat-x"
                                    backgroundSize="100% 1px"
                                    backgroundPosition="0 100%"
                                    mr="-2px"
                                >
                                    {notification.body}
                                </Text>
                            </Button>
                        )}

                    {notificationType === "Invite" && (
                        <Button
                            variant="outline"
                            size="md"
                            _hover={{ bg: "rgba(0, 0, 0, 0.03)" }}
                            padding="12px"
                            height="50px"
                            fontSize="14px"
                            width="100%"
                            borderRadius="8px"
                            textAlign="left"
                            border="1px solid rgba(227, 226, 224, 0.5)"
                            justifyContent="space-between"
                        >
                            {notification.body}
                        </Button>
                    )}

                    {notificationType === "Comment" && (
                        <Box>
                            <Text fontSize="12px" color="rgb(120, 119, 116)" fontWeight="400">
                                John Doe
                            </Text>
                            <Text fontSize="14px" color="rgb(55, 53, 47)" fontWeight="400">
                                This is a notification Comment made by John Doe and posted on
                                the page Top Secret Project
                            </Text>
                        </Box>
                    )}

                    <HStack spacing={3}>
                        {notification.primaryAction && (
                            <Button
                                variant="outline"
                                size="xs"
                                colorScheme="gray"
                                borderRadius="md"
                                borderColor="gray.300"
                                _hover={{ bg: "gray.100" }}
                                paddingRight="8px"
                                paddingLeft="8px"
                                lineHeight="26px"
                                height="26px"
                            >
                                {notification.primaryAction.label}
                            </Button>
                        )}
                        {notification.secondaryAction && (
                            <Button
                                variant="ghost"
                                size="xs"
                                colorScheme="gray"
                                borderRadius="md"
                                borderColor="gray.300"
                                _hover={{ bg: "gray.100" }}
                                paddingRight="8px"
                                paddingLeft="8px"
                                lineHeight="26px"
                                height="26px"
                            >
                                {notification.secondaryAction.label}
                            </Button>
                        )}
                    </HStack>
                </VStack>
            </Flex>
        </Box>
    );
};

function formatTime(timestamp: number | string | Date): string {
    const date = new Date(timestamp);
    const now = new Date();
    const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
    const secondsInMinute = 60;
    const secondsInHour = secondsInMinute * 60;
    const secondsInDay = secondsInHour * 24;
    const secondsInWeek = secondsInDay * 7;
    const secondsInYear = secondsInDay * 365;

    if (diffInSeconds < secondsInMinute) {
        return `${diffInSeconds} seconds`;
    } else if (diffInSeconds < secondsInHour) {
        const minutes = Math.floor(diffInSeconds / secondsInMinute);
        return `${minutes} minute${minutes !== 1 ? 's' : ''}`;
    } else if (diffInSeconds < secondsInDay) {
        const hours = Math.floor(diffInSeconds / secondsInHour);
        return `${hours} hour${hours !== 1 ? 's' : ''}`;
    } else if (diffInSeconds < secondsInWeek) {
        const days = Math.floor(diffInSeconds / secondsInDay);
        return `${days} day${days !== 1 ? 's' : ''}`;
    } else if (diffInSeconds < secondsInYear) {
        const options: Intl.DateTimeFormatOptions = { month: "short", day: "numeric" };
        return date.toLocaleDateString(undefined, options);
    } else {
        return date.getFullYear().toString();
    }
}

export default AppContainer;
Salin selepas log masuk

Ready to get customizing? Here’s the source code for the Notion Inbox theme. You can also see and play with it live in our Inbox playground. I also did the same for a Reddit notifications example. Two totally different experiences, but powered by the same underlying component and notifications infrastructure.

Atas ialah kandungan terperinci Cara Membina Peti Masuk Pemberitahuan Seperti Notion dengan UI Chakra dan Novu. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

sumber:dev.to
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan