|
| 1 | +/* eslint-disable @next/next/no-img-element */ |
| 2 | +import { GetStaticPropsContext, GetStaticPropsResult } from "next" |
| 3 | +import Head from "next/head" |
| 4 | +import { useEffect, useState } from "react" |
| 5 | +import FormattedLink from "../components/FormattedLink" |
| 6 | +import Main from "../components/Main" |
| 7 | +import { getEvents } from "../utils/data-cache" |
| 8 | +import { Event as GenshinEvent, EventType } from "../utils/types" |
| 9 | +import { formatTime, getDate, timeLeft } from "../utils/utils" |
| 10 | +import styles from "./style.module.css" |
| 11 | + |
| 12 | + |
| 13 | +interface Props { |
| 14 | + events: GenshinEvent[] |
| 15 | +} |
| 16 | + |
| 17 | +type Server = "Asia" | "Europe" | "North America" |
| 18 | + |
| 19 | +const timezones: Record<Server, string> = { |
| 20 | + "Asia": "+08:00", |
| 21 | + "Europe": "+01:00", |
| 22 | + "North America": "-05:00", |
| 23 | +} |
| 24 | + |
| 25 | +export default function Events(props: Props & { location: string }) { |
| 26 | + const [now, setNow] = useState(() => Date.now()) |
| 27 | + |
| 28 | + const [server, setServer] = useState("Europe" as Server) |
| 29 | + const [serverTimezone, setServerTimezone] = useState("+08:00") |
| 30 | + |
| 31 | + const [showPast, setShowPast] = useState(false) |
| 32 | + |
| 33 | + useEffect(() => { |
| 34 | + const server = localStorage.getItem("server") |
| 35 | + |
| 36 | + if (server && Object.keys(timezones).includes(server)) |
| 37 | + setServer(server as Server) |
| 38 | + else { |
| 39 | + const clientOffset = new Date().getTimezoneOffset() / 60 |
| 40 | + if (clientOffset > 5) |
| 41 | + setServer("Asia") |
| 42 | + else if (clientOffset > -2 || clientOffset < -11) |
| 43 | + setServer("Europe") |
| 44 | + else |
| 45 | + setServer("North America") |
| 46 | + } |
| 47 | + }, []) |
| 48 | + |
| 49 | + useEffect(() => { |
| 50 | + setServerTimezone(timezones[server]) |
| 51 | + localStorage.setItem("server", server) |
| 52 | + }, [server]) |
| 53 | + |
| 54 | + useEffect(() => { |
| 55 | + const timer = setTimeout(() => { |
| 56 | + setNow(Date.now()) |
| 57 | + }, 10050 - (Date.now() % 10000)) |
| 58 | + |
| 59 | + return () => clearTimeout(timer) |
| 60 | + }, [now]) |
| 61 | + |
| 62 | + |
| 63 | + const ongoing = props.events |
| 64 | + .filter(e => { |
| 65 | + const start = getStartTime(e, serverTimezone) |
| 66 | + const end = getEndTime(e, serverTimezone) |
| 67 | + |
| 68 | + return start && start.getTime() <= now && |
| 69 | + ( |
| 70 | + (end && end.getTime() >= now) || |
| 71 | + (!end && e.reminder == "daily") |
| 72 | + ) |
| 73 | + }).sort((a, b) => { |
| 74 | + const endA = getEndTime(a, serverTimezone) |
| 75 | + const endB = getEndTime(b, serverTimezone) |
| 76 | + |
| 77 | + if (!endA) return 1 |
| 78 | + if (!endB) return -1 |
| 79 | + |
| 80 | + return endA.getTime() - endB.getTime() |
| 81 | + }) |
| 82 | + |
| 83 | + const upcoming = props.events |
| 84 | + .filter(e => { |
| 85 | + const start = getStartTime(e, serverTimezone) |
| 86 | + return start == false || start.getTime() > now |
| 87 | + }) |
| 88 | + .sort((a, b) => { |
| 89 | + const startA = getStartTime(a, serverTimezone) |
| 90 | + const startB = getStartTime(b, serverTimezone) |
| 91 | + |
| 92 | + if (!startA) return 1 |
| 93 | + if (!startB) return -1 |
| 94 | + |
| 95 | + return startA.getTime() - startB.getTime() |
| 96 | + }) |
| 97 | + |
| 98 | + const past = props.events |
| 99 | + .filter(e => !ongoing.includes(e) && !upcoming.includes(e)) |
| 100 | + .sort((a, b) => { |
| 101 | + const startA = getStartTime(a, serverTimezone) |
| 102 | + const startB = getStartTime(b, serverTimezone) |
| 103 | + |
| 104 | + if (!startA) return -1 |
| 105 | + if (!startB) return 1 |
| 106 | + |
| 107 | + return startB.getTime() - startA.getTime() |
| 108 | + }) |
| 109 | + |
| 110 | + const desc = "View ongoing and future events of Genshin Impact." |
| 111 | + return ( |
| 112 | + <Main> |
| 113 | + <Head> |
| 114 | + <title>Events | Hu Tao</title> |
| 115 | + <meta name="twitter:card" content="summary" /> |
| 116 | + <meta property="og:title" content="Events | Hu Tao" /> |
| 117 | + <meta property="og:description" content={desc} /> |
| 118 | + <meta property="description" content={desc} /> |
| 119 | + </Head> |
| 120 | + |
| 121 | + <h1 className="text-5xl font-bold pb-2"> |
| 122 | + Events |
| 123 | + </h1> |
| 124 | + <div> |
| 125 | + Select server: |
| 126 | + <div> |
| 127 | + {Object.keys(timezones).map((t) => <span |
| 128 | + key={t} |
| 129 | + className={`${server == t ? "bg-green-500" : "bg-blue-500"} text-slate-50 text-center rounded-lg mr-2 px-1 py-0.5 cursor-pointer`} |
| 130 | + onClick={() => setServer(t as Server)} |
| 131 | + > |
| 132 | + {t} |
| 133 | + </span>)} |
| 134 | + </div> |
| 135 | + </div> |
| 136 | + |
| 137 | + <h3 className="text-xl mt-3">Ongoing event{ongoing.length == 1 ? "" : "s"}</h3> |
| 138 | + <div className="flex flex-row flex-wrap pt-2 gap-2"> |
| 139 | + {ongoing |
| 140 | + .map((event, i) => <EventCard key={i} e={event} now={now} serverTimezone={serverTimezone} hoverClass="hover:text-blue-800" className="bg-green-500 text-black" />)} |
| 141 | + </div> |
| 142 | + |
| 143 | + <h3 className="text-xl mt-3">Upcoming event{upcoming.length == 1 ? "" : "s"}</h3> |
| 144 | + <div className="flex flex-row flex-wrap pt-2 gap-2"> |
| 145 | + {upcoming |
| 146 | + .map((event, i) => <EventCard key={i} e={event} now={now} serverTimezone={serverTimezone} className="dark:bg-slate-800 bg-slate-300" />)} |
| 147 | + </div> |
| 148 | + |
| 149 | + <h3 className="text-xl mt-3">Past event{past.length == 1 ? "" : "s"}</h3> |
| 150 | + {showPast ? <div className="flex flex-row flex-wrap pt-2 gap-2"> |
| 151 | + {past |
| 152 | + .map((event, i) => <EventCard key={i} e={event} now={now} serverTimezone={serverTimezone} hoverClass="hover:text-blue-800" className="bg-red-400 text-black" />)} |
| 153 | + </div> : <span className="bg-blue-500 text-slate-50 text-center rounded-lg mr-2 mt-1 px-1 py-0.5 cursor-pointer" onClick={() => setShowPast(true)}>Show past events</span>} |
| 154 | + </Main> |
| 155 | + ) |
| 156 | +} |
| 157 | + |
| 158 | +function EventCard({ e, className, now, serverTimezone, hoverClass = "" }: { e: GenshinEvent, className: string, hoverClass?: string, now: number, serverTimezone: string }) { |
| 159 | + const start = getStartTime(e, serverTimezone) |
| 160 | + const end = getEndTime(e, serverTimezone) |
| 161 | + const child = <div className={`text-center max-w-md w-full rounded-xl pb-1 ${className} ${e.link ? `${hoverClass} transition`:""}`}> |
| 162 | + <div className={`${styles.mask} w-full`}> |
| 163 | + <img loading="lazy" className="rounded-t-xl" src={e.img ?? "/img/Event_No_Img.png"} width={448} alt={e.name} /> |
| 164 | + </div> |
| 165 | + <div> |
| 166 | + <h3 className="font-semibold">{e.name}</h3> |
| 167 | + {start && <div className={e.prediction ? "opacity-60" : ""}>{e.prediction && <span className="font-bold cursor-help" title="Predicted based on past events like this">Predicted</span>} {e.type == EventType.Unlock ? "Unlock Time" : "Start Time"}: {formatTime(start)} ({timeLeft(start.getTime() - now)})</div>} |
| 168 | + {end && <div className={e.prediction ? "opacity-60" : ""}>{e.prediction && <span className="font-bold cursor-help" title="Predicted based on past events like this">Predicted</span>} End Time {formatTime(end)} ({timeLeft(end.getTime() - now)})</div>} |
| 169 | + |
| 170 | + </div> |
| 171 | + </div> |
| 172 | + return e.link ? <FormattedLink href={e.link} className="font-normal">{child}</FormattedLink> : child |
| 173 | +} |
| 174 | + |
| 175 | +function getStartTime(event: GenshinEvent, serverTimezone: string) { |
| 176 | + const serverTimeStart = event.start_server ?? (event.type == EventType.Banner || event.type == EventType.InGame || event.type == EventType.Unlock) |
| 177 | + const startTime = event.start != undefined && getDate(event.start, event.timezone ?? serverTimeStart ? serverTimezone : undefined) |
| 178 | + |
| 179 | + return startTime |
| 180 | +} |
| 181 | + |
| 182 | +function getEndTime(event: GenshinEvent, serverTimezone: string) { |
| 183 | + const serverTimeEnd = event.end_server ?? (event.type == EventType.Banner || event.type == EventType.InGame || event.type == EventType.Web) |
| 184 | + const endTime = event.end != undefined && getDate(event.end, event.timezone ?? serverTimeEnd ? serverTimezone : undefined) |
| 185 | + |
| 186 | + return endTime |
| 187 | +} |
| 188 | + |
| 189 | + |
| 190 | +export async function getStaticProps(context: GetStaticPropsContext): Promise<GetStaticPropsResult<Props>> { |
| 191 | + const events = await getEvents() |
| 192 | + |
| 193 | + if (!events) { |
| 194 | + return { |
| 195 | + notFound: true, |
| 196 | + revalidate: 15 * 60 |
| 197 | + } |
| 198 | + } |
| 199 | + |
| 200 | + return { |
| 201 | + props: { |
| 202 | + events |
| 203 | + }, |
| 204 | + revalidate: 60 * 60 * 4 |
| 205 | + } |
| 206 | +} |
0 commit comments