mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-24 23:19:39 -05:00
/calendar new UI (#589)
* Initial * Show popover for calendar items * Remove event search * Only show next button if events in the future * Horizontal scroll * Fix badges alignment * Show badges for tournaments with them * Showcase time until calendar date header in days * Use Chakra UI for CSS * Proper size calendar * Restore inline styles for BadgeContainer * Overflow auto works * Show time badges
This commit is contained in:
parent
a7553ffd11
commit
d462c6d0f8
|
|
@ -3,6 +3,8 @@ import Markdown from "components/common/Markdown";
|
|||
import MyLink from "components/common/MyLink";
|
||||
import OutlinedBox from "components/common/OutlinedBox";
|
||||
import UserAvatar from "components/common/UserAvatar";
|
||||
import BadgeContainer from "components/u/BadgeContainer";
|
||||
import { regularTournamentBadges } from "components/u/Badges";
|
||||
import { useMyTheme, useUser } from "hooks/common";
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
|
|
@ -59,10 +61,19 @@ const EventInfo = ({ event, edit }: EventInfoProps) => {
|
|||
|
||||
const imgSrc = eventImage(event.name);
|
||||
|
||||
const badges = regularTournamentBadges.filter(
|
||||
(badgeObj) =>
|
||||
event.name.toUpperCase().includes(badgeObj.name.toUpperCase()) ||
|
||||
badgeObj.altNames?.some((altName) =>
|
||||
event.name.toUpperCase().includes(altName.toUpperCase())
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<OutlinedBox
|
||||
my={4}
|
||||
py={4}
|
||||
id={`event-${event.id}`}
|
||||
data-cy={`event-info-section-${event.name
|
||||
.toLowerCase()
|
||||
.replace(/ /g, "-")}`}
|
||||
|
|
@ -73,22 +84,36 @@ const EventInfo = ({ event, edit }: EventInfoProps) => {
|
|||
{imgSrc && <Image src={imgSrc} width={36} height={36} alt="" />}
|
||||
<Heading size="lg">{event.name}</Heading>
|
||||
{event.tags.length > 0 && (
|
||||
<Flex flexWrap="wrap" justifyContent="center" my={2}>
|
||||
{event.tags.map((tag) => {
|
||||
const tagInfo = TAGS.find((tagObj) => tagObj.code === tag)!;
|
||||
return (
|
||||
<Badge
|
||||
key={tag}
|
||||
mx={1}
|
||||
my={1}
|
||||
bg={tagInfo.color}
|
||||
color="black"
|
||||
>
|
||||
{tagInfo.name}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
<>
|
||||
<Flex flexWrap="wrap" justifyContent="center" my={2}>
|
||||
{event.tags.map((tag) => {
|
||||
const tagInfo = TAGS.find((tagObj) => tagObj.code === tag)!;
|
||||
return (
|
||||
<Badge
|
||||
key={tag}
|
||||
mx={1}
|
||||
my={1}
|
||||
bg={tagInfo.color}
|
||||
color="black"
|
||||
>
|
||||
{tagInfo.name}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
{event.tags.some((tag) => tag === "BADGE") &&
|
||||
badges.length > 0 ? (
|
||||
<BadgeContainer
|
||||
showInfo={false}
|
||||
showBadges={true}
|
||||
badges={badges.map((badge) => ({
|
||||
src: `${badge.badgeName}.gif`,
|
||||
description: "",
|
||||
count: 1,
|
||||
}))}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
<Grid
|
||||
templateColumns={["1fr", "2fr 4fr 2fr"]}
|
||||
|
|
|
|||
141
components/common/Calendar.tsx
Normal file
141
components/common/Calendar.tsx
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import { Box, Flex, Grid, IconButton, useMediaQuery } from "@chakra-ui/react";
|
||||
import { useMyTheme } from "hooks/common";
|
||||
import { ReactNode } from "react";
|
||||
import { FiChevronLeft, FiChevronRight } from "react-icons/fi";
|
||||
|
||||
const daysInMonth = (month: number, year: number): number[] => {
|
||||
const monthZeroIndex = month - 1;
|
||||
const date = new Date(year, monthZeroIndex, 1);
|
||||
|
||||
const result = [];
|
||||
while (date.getMonth() === monthZeroIndex) {
|
||||
result.push(date.getDate());
|
||||
date.setDate(date.getDate() + 1);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const emptyDaysCount = (currentDate: Date): number => {
|
||||
return [6, 0, 1, 2, 3, 4, 5, 6][currentDate.getDay()];
|
||||
};
|
||||
|
||||
const isToday = (date: Date) => {
|
||||
const now = new Date();
|
||||
|
||||
return (
|
||||
date.getDate() === now.getDate() &&
|
||||
date.getMonth() === now.getMonth() &&
|
||||
date.getFullYear() === now.getFullYear()
|
||||
);
|
||||
};
|
||||
|
||||
type MonthYear = { month: number; year: number };
|
||||
|
||||
const Square = ({ children }: { children?: ReactNode }) => {
|
||||
const { gray } = useMyTheme();
|
||||
return (
|
||||
<Box color={gray} fontSize="small" fontWeight="bold">
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const Calendar = ({
|
||||
current,
|
||||
min,
|
||||
handleBackClick,
|
||||
handleNextClick,
|
||||
showNextButton,
|
||||
dateContents,
|
||||
}: {
|
||||
current: MonthYear;
|
||||
min: MonthYear;
|
||||
handleBackClick: () => void;
|
||||
handleNextClick: () => void;
|
||||
showNextButton: boolean;
|
||||
dateContents: Record<string, ReactNode>;
|
||||
}) => {
|
||||
const { themeColorHex } = useMyTheme();
|
||||
const [noSideNav] = useMediaQuery("(max-width: 991px)");
|
||||
const currentDate = new Date(current.year, current.month - 1, 1);
|
||||
|
||||
return (
|
||||
<Box
|
||||
display="inline-block"
|
||||
textAlign="center"
|
||||
overflowX="auto"
|
||||
width={noSideNav ? "100vw" : "calc(100vw - 250px)"}
|
||||
>
|
||||
<Flex justify="space-evenly">
|
||||
<IconButton
|
||||
aria-label="Go back a month"
|
||||
variant="ghost"
|
||||
color="current"
|
||||
icon={<FiChevronLeft />}
|
||||
borderRadius="50%"
|
||||
float="left"
|
||||
size="lg"
|
||||
onClick={handleBackClick}
|
||||
visibility={
|
||||
current.month === min.month && current.year === min.year
|
||||
? "hidden"
|
||||
: "visible"
|
||||
}
|
||||
/>
|
||||
<Box>
|
||||
{currentDate.toLocaleDateString("en", { month: "long" })}
|
||||
<br />
|
||||
<b>{current.year}</b>
|
||||
</Box>
|
||||
<IconButton
|
||||
aria-label="Go forward a month"
|
||||
variant="ghost"
|
||||
color="current"
|
||||
icon={<FiChevronRight />}
|
||||
borderRadius="50%"
|
||||
float="right"
|
||||
size="lg"
|
||||
onClick={handleNextClick}
|
||||
visibility={showNextButton ? "visible" : "hidden"}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Grid
|
||||
templateColumns="repeat(7, max(14%, 125px))"
|
||||
gridAutoRows="1fr"
|
||||
gridRowGap="0.5rem"
|
||||
gridColumnGap="0.25rem"
|
||||
mt="1rem"
|
||||
>
|
||||
<Square>Mo</Square>
|
||||
<Square>Tu</Square>
|
||||
<Square>We</Square>
|
||||
<Square>Th</Square>
|
||||
<Square>Fr</Square>
|
||||
<Square>Sa</Square>
|
||||
<Square>Su</Square>
|
||||
{new Array(emptyDaysCount(currentDate)).fill(null).map((_, i) => (
|
||||
<Square key={i}></Square>
|
||||
))}
|
||||
{daysInMonth(current.month, current.year).map((day) => (
|
||||
<Square key={day}>
|
||||
<Box
|
||||
as="span"
|
||||
display="inline-block"
|
||||
boxShadow={
|
||||
isToday(new Date(current.year, current.month - 1, day))
|
||||
? `inset 0px -2px 0px 0px ${themeColorHex}`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{day}
|
||||
</Box>
|
||||
{dateContents[`${day}-${current.month}-${current.year}`]}
|
||||
</Square>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Calendar;
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { Image as ChakraImage } from "@chakra-ui/image";
|
||||
import { Flex, Text } from "@chakra-ui/react";
|
||||
import { useMyTheme } from "hooks/common";
|
||||
import { Fragment } from "react";
|
||||
|
||||
const BadgeContainer = ({
|
||||
|
|
@ -15,6 +16,7 @@ const BadgeContainer = ({
|
|||
count: number;
|
||||
}[];
|
||||
}) => {
|
||||
const { themeColorHex } = useMyTheme();
|
||||
return (
|
||||
<Flex
|
||||
flexDir={showInfo ? "column" : "row"}
|
||||
|
|
@ -29,7 +31,8 @@ const BadgeContainer = ({
|
|||
my={3}
|
||||
>
|
||||
{showBadges &&
|
||||
badges.flatMap((badge) => {
|
||||
badges.flatMap((badge, i) => {
|
||||
const isLast = i === badges.length - 1;
|
||||
if (showInfo)
|
||||
return (
|
||||
<Fragment key={badge.src}>
|
||||
|
|
@ -54,14 +57,20 @@ const BadgeContainer = ({
|
|||
src={`/badges/${badge.src}`}
|
||||
/>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
style={{marginLeft: -5, marginTop: -25, paddingRight: 5, fontSize: '0.7rem', fontWeight: 'bold' }}
|
||||
style={{
|
||||
marginLeft: -3,
|
||||
marginTop: -25,
|
||||
paddingRight: isLast ? 0 : 5,
|
||||
fontSize: "0.7rem",
|
||||
fontWeight: "bold",
|
||||
color: themeColorHex,
|
||||
}}
|
||||
visibility={badge.count === 1 ? "hidden" : "visible"}
|
||||
>
|
||||
{`x${badge.count}`}
|
||||
</Text>
|
||||
</Fragment>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,14 +3,16 @@ import { useEffect, useMemo, useState } from "react";
|
|||
import { wonITZCount } from "utils/constants";
|
||||
import BadgeContainer from "./BadgeContainer";
|
||||
|
||||
const regularTournamentWinners: {
|
||||
export const regularTournamentBadges: {
|
||||
badgeName: string;
|
||||
name: string;
|
||||
altNames?: string[];
|
||||
winnerDiscordIds: string;
|
||||
}[] = [
|
||||
{
|
||||
badgeName: "zones",
|
||||
name: "Dapple SZ Speedladder",
|
||||
altNames: ["Dapple SZ Ladder"],
|
||||
winnerDiscordIds:
|
||||
"393411373289177098,300073469503340546,554124226915860507,417489824589676548,403345464138661888,343375632819814400,726389792237027370,570288931321413653,716227192060641281",
|
||||
},
|
||||
|
|
@ -23,12 +25,14 @@ const regularTournamentWinners: {
|
|||
{
|
||||
badgeName: "pair",
|
||||
name: "League Rush (Pair)",
|
||||
altNames: ["League Rush!!"],
|
||||
winnerDiscordIds:
|
||||
"453753483427053568,398818695608270849,776911543216111648,393908122525368331",
|
||||
},
|
||||
{
|
||||
badgeName: "quad",
|
||||
name: "League Rush (Quad)",
|
||||
altNames: ["League Rush!!"],
|
||||
winnerDiscordIds:
|
||||
"105390854063034368,151192098962407424,147036636608331779,260602342309756940,115572122482507782,109804061900992512,169184589200359424",
|
||||
},
|
||||
|
|
@ -197,7 +201,7 @@ const usersBadges = ({
|
|||
|
||||
// Other tournaments
|
||||
|
||||
for (const tournament of regularTournamentWinners) {
|
||||
for (const tournament of regularTournamentBadges) {
|
||||
const count = tournament.winnerDiscordIds
|
||||
.split(",")
|
||||
.reduce(
|
||||
|
|
|
|||
|
|
@ -1,28 +1,91 @@
|
|||
import { Button } from "@chakra-ui/button";
|
||||
import { Input, InputGroup, InputLeftElement } from "@chakra-ui/input";
|
||||
import { Box } from "@chakra-ui/layout";
|
||||
import {
|
||||
Badge,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@chakra-ui/react";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import EventInfo from "components/calendar/EventInfo";
|
||||
import { EventModal, FormData } from "components/calendar/EventModal";
|
||||
import Calendar from "components/common/Calendar";
|
||||
import MyHead from "components/common/MyHead";
|
||||
import SubText from "components/common/SubText";
|
||||
import { useMyTheme, useUser } from "hooks/common";
|
||||
import { ssr } from "pages/api/trpc/[trpc]";
|
||||
import { Fragment, useState } from "react";
|
||||
import { Fragment, ReactNode, useMemo, useState } from "react";
|
||||
import { FiSearch } from "react-icons/fi";
|
||||
import { trpc } from "utils/trpc";
|
||||
|
||||
const CalendarPage = () => {
|
||||
const { gray } = useMyTheme();
|
||||
const { gray, secondaryBgColor } = useMyTheme();
|
||||
const events = trpc.useQuery(["calendar.events"], { enabled: false });
|
||||
const [eventToEdit, setEventToEdit] = useState<
|
||||
boolean | (FormData & { id: number })
|
||||
>(false);
|
||||
const [filter, setFilter] = useState("");
|
||||
const [{ month, year }, setMonthYear] = useState({
|
||||
month: new Date().getMonth() + 1,
|
||||
year: new Date().getFullYear(),
|
||||
});
|
||||
const [user] = useUser();
|
||||
|
||||
let lastPrintedDate: [number, number, Date] | null = null;
|
||||
|
||||
const scrollToEvent = (id: number) => {
|
||||
document
|
||||
.getElementById(`event-${id}`)
|
||||
?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
const eventsInFuture = Boolean(
|
||||
events.data?.some(
|
||||
(event) =>
|
||||
event.date.getMonth() + 1 > month || event.date.getFullYear() > year
|
||||
)
|
||||
);
|
||||
|
||||
const calendarDateContents = useMemo(() => {
|
||||
return (events.data ?? []).reduce(
|
||||
(result: Record<string, ReactNode[]>, event) => {
|
||||
const key = `${event.date.getDate()}-${
|
||||
event.date.getMonth() + 1
|
||||
}-${event.date.getFullYear()}`;
|
||||
const node = (
|
||||
<>
|
||||
<Button
|
||||
display="block"
|
||||
mx="auto"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
textOverflow="ellipsis"
|
||||
maxW="150px"
|
||||
width="100%"
|
||||
height="2rem"
|
||||
mt="0.25rem"
|
||||
mb="0.5rem"
|
||||
overflow="hidden"
|
||||
onClick={() => {
|
||||
scrollToEvent(event.id);
|
||||
}}
|
||||
>
|
||||
<Badge display="block" size="xs" colorScheme="gray" mb="0.25rem">
|
||||
{event.date.toLocaleTimeString("en", { hour: "numeric" })}
|
||||
</Badge>
|
||||
{event.name}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
if (result[key]) result[key].push(node);
|
||||
else result[key] = [node];
|
||||
|
||||
return result;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyHead title={t`Calendar`} />
|
||||
|
|
@ -34,7 +97,7 @@ const CalendarPage = () => {
|
|||
/>
|
||||
)}
|
||||
{user && (
|
||||
<div>
|
||||
<Box mb={6}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setEventToEdit(true)}
|
||||
|
|
@ -42,17 +105,33 @@ const CalendarPage = () => {
|
|||
>
|
||||
<Trans>Add event</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
<InputGroup my={8} maxW="24rem" mx="auto">
|
||||
<InputLeftElement pointerEvents="none">
|
||||
<FiSearch color={gray} />
|
||||
</InputLeftElement>
|
||||
<Input value={filter} onChange={(e) => setFilter(e.target.value)} />
|
||||
</InputGroup>
|
||||
<Calendar
|
||||
current={{ month, year }}
|
||||
min={{ month: 7, year: 2021 }}
|
||||
handleNextClick={() =>
|
||||
setMonthYear(
|
||||
month === 12
|
||||
? { month: 1, year: year + 1 }
|
||||
: { month: month + 1, year }
|
||||
)
|
||||
}
|
||||
handleBackClick={() =>
|
||||
setMonthYear(
|
||||
month === 1
|
||||
? { month: 12, year: year - 1 }
|
||||
: { month: month - 1, year }
|
||||
)
|
||||
}
|
||||
showNextButton={eventsInFuture}
|
||||
dateContents={calendarDateContents}
|
||||
/>
|
||||
{(events.data ?? [])
|
||||
.filter((event) =>
|
||||
event.name.toLowerCase().includes(filter.toLowerCase().trim())
|
||||
.filter(
|
||||
(event) =>
|
||||
event.date.getMonth() + 1 === month &&
|
||||
event.date.getFullYear() === year
|
||||
)
|
||||
.map((event, i) => {
|
||||
const printDateHeader =
|
||||
|
|
@ -74,6 +153,13 @@ const CalendarPage = () => {
|
|||
lastPrintedDate![2].getDate() === now.getDate() &&
|
||||
lastPrintedDate![2].getMonth() === now.getMonth();
|
||||
|
||||
const timeUntilDateInDays = ((date: Date) => {
|
||||
const now = new Date();
|
||||
const diff = date.getTime() - now.getTime();
|
||||
const day = Math.floor(diff / (1000 * 3600 * 24));
|
||||
return day;
|
||||
})(lastPrintedDate![2]);
|
||||
|
||||
return (
|
||||
<Fragment key={event.id}>
|
||||
{printDateHeader && (
|
||||
|
|
@ -86,6 +172,9 @@ const CalendarPage = () => {
|
|||
weekday: "long",
|
||||
})}{" "}
|
||||
{isToday && <Trans>(Today)</Trans>}
|
||||
{timeUntilDateInDays > 1 && (
|
||||
<>(In {timeUntilDateInDays} days)</>
|
||||
)}
|
||||
</SubText>
|
||||
</Box>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user