refactor ItemPage components to be more reusable
this is because I'm about to make trades pages!
This commit is contained in:
parent
3b4d2ac390
commit
1af034e4e7
2 changed files with 295 additions and 273 deletions
|
@ -2,12 +2,10 @@ import React from "react";
|
|||
import { css } from "emotion";
|
||||
import {
|
||||
AspectRatio,
|
||||
Badge,
|
||||
Button,
|
||||
Box,
|
||||
HStack,
|
||||
IconButton,
|
||||
Skeleton,
|
||||
SkeletonText,
|
||||
Tooltip,
|
||||
VisuallyHidden,
|
||||
|
@ -19,7 +17,6 @@ import {
|
|||
} from "@chakra-ui/core";
|
||||
import {
|
||||
CheckIcon,
|
||||
ExternalLinkIcon,
|
||||
ChevronRightIcon,
|
||||
StarIcon,
|
||||
WarningIcon,
|
||||
|
@ -29,12 +26,8 @@ import gql from "graphql-tag";
|
|||
import { useQuery, useMutation } from "@apollo/client";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import {
|
||||
ItemBadgeList,
|
||||
ItemKindBadge,
|
||||
ItemThumbnail,
|
||||
} from "./components/ItemCard";
|
||||
import { Delay, Heading1, usePageTitle } from "./util";
|
||||
import ItemPageLayout, { SubtleSkeleton } from "./ItemPageLayout";
|
||||
import { Delay, usePageTitle } from "./util";
|
||||
import {
|
||||
itemAppearanceFragment,
|
||||
petAppearanceFragment,
|
||||
|
@ -57,19 +50,6 @@ function ItemPage() {
|
|||
export function ItemPageContent({ itemId, isEmbedded }) {
|
||||
const { isLoggedIn } = useCurrentUser();
|
||||
|
||||
return (
|
||||
<VStack spacing="8">
|
||||
<ItemPageHeader itemId={itemId} isEmbedded={isEmbedded} />
|
||||
<VStack spacing="4">
|
||||
{isLoggedIn && <ItemPageOwnWantButtons itemId={itemId} />}
|
||||
<ItemPageTradeLinks itemId={itemId} />
|
||||
</VStack>
|
||||
{!isEmbedded && <ItemPageOutfitPreview itemId={itemId} />}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemPageHeader({ itemId, isEmbedded }) {
|
||||
const { error, data } = useQuery(
|
||||
gql`
|
||||
query ItemPage($itemId: ID!) {
|
||||
|
@ -89,12 +69,6 @@ function ItemPageHeader({ itemId, isEmbedded }) {
|
|||
|
||||
usePageTitle(data?.item?.name, { skip: isEmbedded });
|
||||
|
||||
// Show 2 lines of description text placeholder on small screens, or when
|
||||
// embedded in the wardrobe page's narrow drawer. In larger contexts, show
|
||||
// just 1 line.
|
||||
const viewportNumDescriptionLines = useBreakpointValue({ base: 2, md: 1 });
|
||||
const numDescriptionLines = isEmbedded ? 2 : viewportNumDescriptionLines;
|
||||
|
||||
if (error) {
|
||||
return <Box color="red.400">{error.message}</Box>;
|
||||
}
|
||||
|
@ -102,32 +76,32 @@ function ItemPageHeader({ itemId, isEmbedded }) {
|
|||
const item = data?.item;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="flex-start"
|
||||
width="100%"
|
||||
>
|
||||
<SubtleSkeleton isLoaded={item?.thumbnailUrl} marginRight="4">
|
||||
<ItemThumbnail item={item} size="lg" isActive flex="0 0 auto" />
|
||||
</SubtleSkeleton>
|
||||
<Box>
|
||||
<SubtleSkeleton isLoaded={item?.name}>
|
||||
<Heading1
|
||||
lineHeight="1.1"
|
||||
// Nudge down the size a bit in the embed case, to better fit the
|
||||
// tighter layout!
|
||||
size={isEmbedded ? "xl" : "2xl"}
|
||||
>
|
||||
{item?.name || "Item name here"}
|
||||
</Heading1>
|
||||
</SubtleSkeleton>
|
||||
<ItemPageBadges item={item} isEmbedded={isEmbedded} />
|
||||
</Box>
|
||||
</Box>
|
||||
<ItemPageLayout item={item} isEmbedded={isEmbedded}>
|
||||
<VStack spacing="8" marginTop="4">
|
||||
<ItemPageDescription
|
||||
description={item?.description}
|
||||
isEmbedded={isEmbedded}
|
||||
/>
|
||||
<VStack spacing="4">
|
||||
{isLoggedIn && <ItemPageOwnWantButtons itemId={itemId} />}
|
||||
<ItemPageTradeLinks itemId={itemId} />
|
||||
</VStack>
|
||||
{!isEmbedded && <ItemPageOutfitPreview itemId={itemId} />}
|
||||
</VStack>
|
||||
</ItemPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemPageDescription({ description, isEmbedded }) {
|
||||
// Show 2 lines of description text placeholder on small screens, or when
|
||||
// embedded in the wardrobe page's narrow drawer. In larger contexts, show
|
||||
// just 1 line.
|
||||
const viewportNumDescriptionLines = useBreakpointValue({ base: 2, md: 1 });
|
||||
const numDescriptionLines = isEmbedded ? 2 : viewportNumDescriptionLines;
|
||||
|
||||
return (
|
||||
<Box width="100%" alignSelf="flex-start">
|
||||
{item?.description || (
|
||||
{description || (
|
||||
<Box
|
||||
maxWidth="40em"
|
||||
minHeight={numDescriptionLines * 1.5 + "em"}
|
||||
|
@ -142,173 +116,6 @@ function ItemPageHeader({ itemId, isEmbedded }) {
|
|||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemPageBadges({ item, isEmbedded }) {
|
||||
const searchBadgesAreLoaded = item?.name != null && item?.isNc != null;
|
||||
|
||||
return (
|
||||
<ItemBadgeList marginTop="1">
|
||||
<SubtleSkeleton isLoaded={item?.isNc != null}>
|
||||
<ItemKindBadge isNc={item.isNc} isPb={item.isPb} />
|
||||
</SubtleSkeleton>
|
||||
{
|
||||
// If the createdAt date is null (loaded and empty), hide the badge.
|
||||
item.createdAt !== null && (
|
||||
<SubtleSkeleton
|
||||
// Distinguish between undefined (still loading) and null (loaded and
|
||||
// empty).
|
||||
isLoaded={item.createdAt !== undefined}
|
||||
>
|
||||
<Badge
|
||||
display="block"
|
||||
minWidth="5.25em"
|
||||
boxSizing="content-box"
|
||||
textAlign="center"
|
||||
>
|
||||
{item.createdAt && <ShortTimestamp when={item.createdAt} />}
|
||||
</Badge>
|
||||
</SubtleSkeleton>
|
||||
)
|
||||
}
|
||||
<SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
|
||||
<LinkBadge
|
||||
href={`https://impress.openneo.net/items/${item.id}`}
|
||||
isEmbedded={isEmbedded}
|
||||
>
|
||||
Classic DTI
|
||||
</LinkBadge>
|
||||
</SubtleSkeleton>
|
||||
<SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
|
||||
<LinkBadge
|
||||
href={
|
||||
"https://items.jellyneo.net/search/?name=" +
|
||||
encodeURIComponent(item.name) +
|
||||
"&name_type=3"
|
||||
}
|
||||
isEmbedded={isEmbedded}
|
||||
>
|
||||
Jellyneo
|
||||
</LinkBadge>
|
||||
</SubtleSkeleton>
|
||||
<SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
|
||||
{!item?.isNc && !item?.isPb && (
|
||||
<LinkBadge
|
||||
href={
|
||||
"http://www.neopets.com/market.phtml?type=wizard&string=" +
|
||||
encodeURIComponent(item.name)
|
||||
}
|
||||
isEmbedded={isEmbedded}
|
||||
>
|
||||
Shop Wiz
|
||||
</LinkBadge>
|
||||
)}
|
||||
</SubtleSkeleton>
|
||||
<SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
|
||||
{!item?.isNc && !item?.isPb && (
|
||||
<LinkBadge
|
||||
href={
|
||||
"http://www.neopets.com/portal/supershopwiz.phtml?string=" +
|
||||
encodeURIComponent(item.name)
|
||||
}
|
||||
isEmbedded={isEmbedded}
|
||||
>
|
||||
Super Wiz
|
||||
</LinkBadge>
|
||||
)}
|
||||
</SubtleSkeleton>
|
||||
<SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
|
||||
{!item?.isNc && !item?.isPb && (
|
||||
<LinkBadge
|
||||
href={
|
||||
"http://www.neopets.com/island/tradingpost.phtml?type=browse&criteria=item_exact&search_string=" +
|
||||
encodeURIComponent(item.name)
|
||||
}
|
||||
isEmbedded={isEmbedded}
|
||||
>
|
||||
Trade Post
|
||||
</LinkBadge>
|
||||
)}
|
||||
</SubtleSkeleton>
|
||||
<SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
|
||||
{!item?.isNc && !item?.isPb && (
|
||||
<LinkBadge
|
||||
href={
|
||||
"http://www.neopets.com/genie.phtml?type=process_genie&criteria=exact&auctiongenie=" +
|
||||
encodeURIComponent(item.name)
|
||||
}
|
||||
isEmbedded={isEmbedded}
|
||||
>
|
||||
Auctions
|
||||
</LinkBadge>
|
||||
)}
|
||||
</SubtleSkeleton>
|
||||
</ItemBadgeList>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkBadge({ children, href, isEmbedded }) {
|
||||
return (
|
||||
<Badge
|
||||
as="a"
|
||||
href={href}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
// Normally we want to act like a normal webpage, and treat links as
|
||||
// normal. But when we're on the wardrobe page, we want to avoid
|
||||
// disrupting the outfit, and open in a new window instead.
|
||||
target={isEmbedded ? "_blank" : undefined}
|
||||
>
|
||||
{children}
|
||||
{
|
||||
// We also change the icon to signal whether this will launch in a new
|
||||
// window or not!
|
||||
isEmbedded ? <ExternalLinkIcon marginLeft="1" /> : <ChevronRightIcon />
|
||||
}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
const fullDateFormatter = new Intl.DateTimeFormat("en-US", {
|
||||
dateStyle: "long",
|
||||
});
|
||||
const monthYearFormatter = new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
const monthDayYearFormatter = new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
function ShortTimestamp({ when }) {
|
||||
const date = new Date(when);
|
||||
|
||||
// To find the start of last month, take today, then set its date to the 1st
|
||||
// and its time to midnight (the start of this month), and subtract one
|
||||
// month. (JS handles negative months and rolls them over correctly.)
|
||||
const startOfLastMonth = new Date();
|
||||
startOfLastMonth.setDate(1);
|
||||
startOfLastMonth.setHours(0);
|
||||
startOfLastMonth.setMinutes(0);
|
||||
startOfLastMonth.setSeconds(0);
|
||||
startOfLastMonth.setMilliseconds(0);
|
||||
startOfLastMonth.setMonth(startOfLastMonth.getMonth() - 1);
|
||||
|
||||
const dateIsOlderThanLastMonth = date < startOfLastMonth;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
label={`First seen on ${fullDateFormatter.format(date)}`}
|
||||
placement="top"
|
||||
openDelay={400}
|
||||
>
|
||||
{dateIsOlderThanLastMonth
|
||||
? monthYearFormatter.format(date)
|
||||
: monthDayYearFormatter.format(date)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -860,43 +667,4 @@ function ItemPageOutfitPreview({ itemId }) {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* SubtleSkeleton hides the skeleton animation until a second has passed, and
|
||||
* doesn't fade in the content if it loads near-instantly. This helps avoid
|
||||
* flash-of-content stuff!
|
||||
*
|
||||
* For plain Skeletons, we often use <Delay><Skeleton /></Delay> instead. But
|
||||
* that pattern doesn't work as well for wrapper skeletons where we're using
|
||||
* placeholder content for layout: we don't want the delay if the content
|
||||
* really _is_ present!
|
||||
*/
|
||||
function SubtleSkeleton({ isLoaded, ...props }) {
|
||||
const [shouldFadeIn, setShouldFadeIn] = React.useState(false);
|
||||
const [shouldShowSkeleton, setShouldShowSkeleton] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const t = setTimeout(() => {
|
||||
if (!isLoaded) {
|
||||
setShouldFadeIn(true);
|
||||
}
|
||||
}, 150);
|
||||
return () => clearTimeout(t);
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const t = setTimeout(() => setShouldShowSkeleton(true), 500);
|
||||
return () => clearTimeout(t);
|
||||
});
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
fadeDuration={shouldFadeIn ? undefined : 0}
|
||||
startColor={shouldShowSkeleton ? undefined : "transparent"}
|
||||
endColor={shouldShowSkeleton ? undefined : "transparent"}
|
||||
isLoaded={isLoaded}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ItemPage;
|
||||
|
|
254
src/app/ItemPageLayout.js
Normal file
254
src/app/ItemPageLayout.js
Normal file
|
@ -0,0 +1,254 @@
|
|||
import React from "react";
|
||||
import { Badge, Box, Skeleton, Tooltip } from "@chakra-ui/core";
|
||||
import { ExternalLinkIcon, ChevronRightIcon } from "@chakra-ui/icons";
|
||||
|
||||
import {
|
||||
ItemBadgeList,
|
||||
ItemKindBadge,
|
||||
ItemThumbnail,
|
||||
} from "./components/ItemCard";
|
||||
import { Heading1 } from "./util";
|
||||
|
||||
function ItemPageLayout({ children, item, isEmbedded }) {
|
||||
return (
|
||||
<Box>
|
||||
<ItemPageHeader item={item} isEmbedded={isEmbedded} />
|
||||
<Box>{children}</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemPageHeader({ item, isEmbedded }) {
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="flex-start"
|
||||
width="100%"
|
||||
>
|
||||
<SubtleSkeleton isLoaded={item?.thumbnailUrl} marginRight="4">
|
||||
<ItemThumbnail item={item} size="lg" isActive flex="0 0 auto" />
|
||||
</SubtleSkeleton>
|
||||
<Box>
|
||||
<SubtleSkeleton isLoaded={item?.name}>
|
||||
<Heading1
|
||||
lineHeight="1.1"
|
||||
// Nudge down the size a bit in the embed case, to better fit the
|
||||
// tighter layout!
|
||||
size={isEmbedded ? "xl" : "2xl"}
|
||||
>
|
||||
{item?.name || "Item name here"}
|
||||
</Heading1>
|
||||
</SubtleSkeleton>
|
||||
<ItemPageBadges item={item} isEmbedded={isEmbedded} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* SubtleSkeleton hides the skeleton animation until a second has passed, and
|
||||
* doesn't fade in the content if it loads near-instantly. This helps avoid
|
||||
* flash-of-content stuff!
|
||||
*
|
||||
* For plain Skeletons, we often use <Delay><Skeleton /></Delay> instead. But
|
||||
* that pattern doesn't work as well for wrapper skeletons where we're using
|
||||
* placeholder content for layout: we don't want the delay if the content
|
||||
* really _is_ present!
|
||||
*/
|
||||
export function SubtleSkeleton({ isLoaded, ...props }) {
|
||||
const [shouldFadeIn, setShouldFadeIn] = React.useState(false);
|
||||
const [shouldShowSkeleton, setShouldShowSkeleton] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const t = setTimeout(() => {
|
||||
if (!isLoaded) {
|
||||
setShouldFadeIn(true);
|
||||
}
|
||||
}, 150);
|
||||
return () => clearTimeout(t);
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const t = setTimeout(() => setShouldShowSkeleton(true), 500);
|
||||
return () => clearTimeout(t);
|
||||
});
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
fadeDuration={shouldFadeIn ? undefined : 0}
|
||||
startColor={shouldShowSkeleton ? undefined : "transparent"}
|
||||
endColor={shouldShowSkeleton ? undefined : "transparent"}
|
||||
isLoaded={isLoaded}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemPageBadges({ item, isEmbedded }) {
|
||||
const searchBadgesAreLoaded = item?.name != null && item?.isNc != null;
|
||||
|
||||
return (
|
||||
<ItemBadgeList marginTop="1">
|
||||
<SubtleSkeleton isLoaded={item?.isNc != null}>
|
||||
<ItemKindBadge isNc={item.isNc} isPb={item.isPb} />
|
||||
</SubtleSkeleton>
|
||||
{
|
||||
// If the createdAt date is null (loaded and empty), hide the badge.
|
||||
item.createdAt !== null && (
|
||||
<SubtleSkeleton
|
||||
// Distinguish between undefined (still loading) and null (loaded and
|
||||
// empty).
|
||||
isLoaded={item.createdAt !== undefined}
|
||||
>
|
||||
<Badge
|
||||
display="block"
|
||||
minWidth="5.25em"
|
||||
boxSizing="content-box"
|
||||
textAlign="center"
|
||||
>
|
||||
{item.createdAt && <ShortTimestamp when={item.createdAt} />}
|
||||
</Badge>
|
||||
</SubtleSkeleton>
|
||||
)
|
||||
}
|
||||
<SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
|
||||
<LinkBadge
|
||||
href={`https://impress.openneo.net/items/${item.id}`}
|
||||
isEmbedded={isEmbedded}
|
||||
>
|
||||
Classic DTI
|
||||
</LinkBadge>
|
||||
</SubtleSkeleton>
|
||||
<SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
|
||||
<LinkBadge
|
||||
href={
|
||||
"https://items.jellyneo.net/search/?name=" +
|
||||
encodeURIComponent(item.name) +
|
||||
"&name_type=3"
|
||||
}
|
||||
isEmbedded={isEmbedded}
|
||||
>
|
||||
Jellyneo
|
||||
</LinkBadge>
|
||||
</SubtleSkeleton>
|
||||
<SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
|
||||
{!item?.isNc && !item?.isPb && (
|
||||
<LinkBadge
|
||||
href={
|
||||
"http://www.neopets.com/market.phtml?type=wizard&string=" +
|
||||
encodeURIComponent(item.name)
|
||||
}
|
||||
isEmbedded={isEmbedded}
|
||||
>
|
||||
Shop Wiz
|
||||
</LinkBadge>
|
||||
)}
|
||||
</SubtleSkeleton>
|
||||
<SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
|
||||
{!item?.isNc && !item?.isPb && (
|
||||
<LinkBadge
|
||||
href={
|
||||
"http://www.neopets.com/portal/supershopwiz.phtml?string=" +
|
||||
encodeURIComponent(item.name)
|
||||
}
|
||||
isEmbedded={isEmbedded}
|
||||
>
|
||||
Super Wiz
|
||||
</LinkBadge>
|
||||
)}
|
||||
</SubtleSkeleton>
|
||||
<SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
|
||||
{!item?.isNc && !item?.isPb && (
|
||||
<LinkBadge
|
||||
href={
|
||||
"http://www.neopets.com/island/tradingpost.phtml?type=browse&criteria=item_exact&search_string=" +
|
||||
encodeURIComponent(item.name)
|
||||
}
|
||||
isEmbedded={isEmbedded}
|
||||
>
|
||||
Trade Post
|
||||
</LinkBadge>
|
||||
)}
|
||||
</SubtleSkeleton>
|
||||
<SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
|
||||
{!item?.isNc && !item?.isPb && (
|
||||
<LinkBadge
|
||||
href={
|
||||
"http://www.neopets.com/genie.phtml?type=process_genie&criteria=exact&auctiongenie=" +
|
||||
encodeURIComponent(item.name)
|
||||
}
|
||||
isEmbedded={isEmbedded}
|
||||
>
|
||||
Auctions
|
||||
</LinkBadge>
|
||||
)}
|
||||
</SubtleSkeleton>
|
||||
</ItemBadgeList>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkBadge({ children, href, isEmbedded }) {
|
||||
return (
|
||||
<Badge
|
||||
as="a"
|
||||
href={href}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
// Normally we want to act like a normal webpage, and treat links as
|
||||
// normal. But when we're on the wardrobe page, we want to avoid
|
||||
// disrupting the outfit, and open in a new window instead.
|
||||
target={isEmbedded ? "_blank" : undefined}
|
||||
>
|
||||
{children}
|
||||
{
|
||||
// We also change the icon to signal whether this will launch in a new
|
||||
// window or not!
|
||||
isEmbedded ? <ExternalLinkIcon marginLeft="1" /> : <ChevronRightIcon />
|
||||
}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
const fullDateFormatter = new Intl.DateTimeFormat("en-US", {
|
||||
dateStyle: "long",
|
||||
});
|
||||
const monthYearFormatter = new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
const monthDayYearFormatter = new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
function ShortTimestamp({ when }) {
|
||||
const date = new Date(when);
|
||||
|
||||
// To find the start of last month, take today, then set its date to the 1st
|
||||
// and its time to midnight (the start of this month), and subtract one
|
||||
// month. (JS handles negative months and rolls them over correctly.)
|
||||
const startOfLastMonth = new Date();
|
||||
startOfLastMonth.setDate(1);
|
||||
startOfLastMonth.setHours(0);
|
||||
startOfLastMonth.setMinutes(0);
|
||||
startOfLastMonth.setSeconds(0);
|
||||
startOfLastMonth.setMilliseconds(0);
|
||||
startOfLastMonth.setMonth(startOfLastMonth.getMonth() - 1);
|
||||
|
||||
const dateIsOlderThanLastMonth = date < startOfLastMonth;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
label={`First seen on ${fullDateFormatter.format(date)}`}
|
||||
placement="top"
|
||||
openDelay={400}
|
||||
>
|
||||
{dateIsOlderThanLastMonth
|
||||
? monthYearFormatter.format(date)
|
||||
: monthDayYearFormatter.format(date)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default ItemPageLayout;
|
Loading…
Reference in a new issue