Day hours are timezone-adjusted and taken from adjacent days as needed

This commit is contained in:
Ryan Pandya 2024-11-28 00:12:31 -05:00
parent aaf351d328
commit b2e2770e91
22 changed files with 201 additions and 114 deletions

View File

@ -17,6 +17,7 @@
}, },
"dependencies": { "dependencies": {
"@date-fns/tz": "^1.2.0", "@date-fns/tz": "^1.2.0",
"@date-fns/utc": "^2.1.0",
"@lifetracker/db": "workspace:*", "@lifetracker/db": "workspace:*",
"@lifetracker/shared": "workspace:^", "@lifetracker/shared": "workspace:^",
"@lifetracker/trpc": "workspace:^", "@lifetracker/trpc": "workspace:^",

View File

@ -10,6 +10,7 @@ import { Command } from "@commander-js/extra-typings";
import { getBorderCharacters, table } from "table"; import { getBorderCharacters, table } from "table";
import { format } from "date-fns"; import { format } from "date-fns";
import { TZDate } from "@date-fns/tz"; import { TZDate } from "@date-fns/tz";
import { utc } from "@date-fns/utc";
import { getHourFromTime, getTimeFromHour } from "@lifetracker/shared/utils/hours"; import { getHourFromTime, getTimeFromHour } from "@lifetracker/shared/utils/hours";
function moodToStars(mood: number) { function moodToStars(mood: number) {
@ -41,13 +42,14 @@ export const daysCmd = new Command()
} else { } else {
const moodStr = moodToStars(day.mood); const moodStr = moodToStars(day.mood);
const dateStr = format(day.date, "EEEE, MMMM do"); const dateStr = format(day.date, "EEEE, MMMM do", { in: utc });
const data: string[][] = [[dateStr, '', moodStr], [day.comment ?? "No comment", '', ''], const data: string[][] = [[dateStr, '', moodStr], [day.comment ?? "No comment", '', ''],
["Time", "Category", "Comment"] ["Time", "Category", "Comment"]
]; ];
day.hours.forEach((h) => { day.hours.forEach((h, i) => {
data.push([getHourFromTime(h.time), h.categoryName ?? "--", h.comment ?? ""]); data.push([getHourFromTime(h.time),
h.categoryName ?? "--", h.comment ?? ""]);
}) })
console.log(table(data, { console.log(table(data, {

View File

@ -2,7 +2,6 @@ import { notFound } from "next/navigation";
import { api } from "@/server/api/client"; import { api } from "@/server/api/client";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import DayView from "@/components/dashboard/days/DayView"; import DayView from "@/components/dashboard/days/DayView";
import LoadingSpinner from "@/components/ui/spinner";
export default async function DayPage({ export default async function DayPage({
params, params,
@ -10,9 +9,10 @@ export default async function DayPage({
params: { dateQuery: string }; params: { dateQuery: string };
}) { }) {
let day; let day;
try { try {
day = await api.days.get({ dateQuery: params.dateQuery }); day = await api.days.get({
dateQuery: params.dateQuery,
});
} catch (e) { } catch (e) {
if (e instanceof TRPCError) { if (e instanceof TRPCError) {
if (e.code == "NOT_FOUND") { if (e.code == "NOT_FOUND") {
@ -23,9 +23,6 @@ export default async function DayPage({
} }
return ( return (
params.dateQuery === undefined ?
<LoadingSpinner />
:
<DayView <DayView
day={day} day={day}
/> />

View File

@ -5,31 +5,55 @@ import { getServerAuthSession } from "@/server/auth";
import { ZDay } from "@lifetracker/shared/types/days"; import { ZDay } from "@lifetracker/shared/types/days";
import EditableDayComment from "./EditableDayComment"; import EditableDayComment from "./EditableDayComment";
import { MoodStars } from "./MoodStars"; import { MoodStars } from "./MoodStars";
import { format } from "date-fns"; import { format, addDays } from "date-fns";
import { ButtonWithTooltip } from "@/components/ui/button";
import { router } from "next/navigation";
import Link from "next/link";
import { cn } from "@/lib/utils";
import { ArrowLeftSquare, ArrowRightSquare } from "lucide-react";
import { UTCDate, utc } from "@date-fns/utc";
export default async function DayView({ export default async function DayView({
day, day,
header,
showDivider,
showEditorCard = false,
}: { }: {
day: ZDay; day: ZDay;
header?: React.ReactNode;
showDivider?: boolean;
showEditorCard?: boolean;
}) { }) {
const session = await getServerAuthSession(); const session = await getServerAuthSession();
if (!session) { if (!session) {
redirect("/"); redirect("/");
} }
const prevDay = format(addDays(day.date, -1), "yyyy-MM-dd");
const nextDay = format(addDays(day.date, 1), "yyyy-MM-dd");
return ( return (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-2xl"> <div className="flex">
{format(day.date, "EEEE, MMMM do")} <Link
href={`/dashboard/day/${prevDay}`}
className={cn(
"flex-0 items-center rounded-[inherit] px-3 py-2",
)}
>
<div className="flex w-full justify-between">
<ArrowLeftSquare size={18} />
</div>
</Link>
<span className="text-2xl flex-1">
{format(day.date, "EEEE, MMMM do", { in: utc })}
</span> </span>
<Link
href={`/dashboard/day/${nextDay}`}
className={cn(
"flex-0 items-center rounded-[inherit] px-3 py-2",
)}
>
<div className="flex w-full justify-between">
<ArrowRightSquare size={18} />
</div>
</Link>
</div>
<div> <div>
<MoodStars <MoodStars
day={day} day={day}
@ -45,7 +69,13 @@ export default async function DayView({
<Separator /> <Separator />
<ul>
{day.hours.map((hour) => (
<li key={hour.time}>
{hour.time}: {hour.categoryName} {hour.comment}
</li>
))}
</ul>
</div> </div>
); );

View File

@ -41,7 +41,6 @@ export default function EditableDayComment({
}, },
{ {
onError: (e) => { onError: (e) => {
console.log(e);
toast({ toast({
description: e.message, description: e.message,
variant: "destructive", variant: "destructive",

View File

@ -15,7 +15,7 @@ export default async function Header() {
return ( return (
<header className="sticky left-0 right-0 top-0 z-50 flex h-16 items-center justify-between overflow-x-auto overflow-y-hidden bg-background p-4 shadow"> <header className="sticky left-0 right-0 top-0 z-50 flex h-16 items-center justify-between overflow-x-auto overflow-y-hidden bg-background p-4 shadow">
<div className="hidden items-center sm:flex"> <div className="hidden items-center sm:flex">
<Link href={"/dashboard/bookmarks"} className="w-56"> <Link href={"/dashboard/day/today"} className="w-56">
<HoarderLogo height={20} gap="8px" /> <HoarderLogo height={20} gap="8px" />
</Link> </Link>
</div> </div>

View File

@ -35,7 +35,7 @@ export default async function Sidebar() {
{ {
name: "Home", name: "Home",
icon: <Home size={18} />, icon: <Home size={18} />,
path: "/dashboard/today", path: "/dashboard/day/today",
}, },
...searchItem, ...searchItem,
{ {

View File

@ -16,7 +16,7 @@ export const settingsSidebarItems: {
{ {
name: "Back To App", name: "Back To App",
icon: <ArrowLeft size={18} />, icon: <ArrowLeft size={18} />,
path: "/dashboard/today", path: "/dashboard/day/today",
}, },
{ {
name: "User Info", name: "User Info",

View File

@ -17,6 +17,7 @@
"dependencies": { "dependencies": {
"@auth/drizzle-adapter": "^1.4.2", "@auth/drizzle-adapter": "^1.4.2",
"@date-fns/tz": "^1.2.0", "@date-fns/tz": "^1.2.0",
"@date-fns/utc": "^2.1.0",
"@emoji-mart/data": "^1.1.2", "@emoji-mart/data": "^1.1.2",
"@emoji-mart/react": "^1.1.1", "@emoji-mart/react": "^1.1.1",
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",

View File

@ -105,6 +105,5 @@ CREATE UNIQUE INDEX `apiKey_name_userId_unique` ON `apiKey` (`name`,`userId`);--
CREATE UNIQUE INDEX `category_userId_code_unique` ON `category` (`userId`,`code`);--> statement-breakpoint CREATE UNIQUE INDEX `category_userId_code_unique` ON `category` (`userId`,`code`);--> statement-breakpoint
CREATE UNIQUE INDEX `color_userId_name_unique` ON `color` (`userId`,`name`);--> statement-breakpoint CREATE UNIQUE INDEX `color_userId_name_unique` ON `color` (`userId`,`name`);--> statement-breakpoint
CREATE UNIQUE INDEX `day_date_unique` ON `day` (`date`);--> statement-breakpoint CREATE UNIQUE INDEX `day_date_unique` ON `day` (`date`);--> statement-breakpoint
CREATE UNIQUE INDEX `hour_time_unique` ON `hour` (`time`);--> statement-breakpoint
CREATE UNIQUE INDEX `hour_dayId_time_unique` ON `hour` (`dayId`,`time`);--> statement-breakpoint CREATE UNIQUE INDEX `hour_dayId_time_unique` ON `hour` (`dayId`,`time`);--> statement-breakpoint
CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`); CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);

View File

@ -1,7 +1,7 @@
{ {
"version": "6", "version": "6",
"dialect": "sqlite", "dialect": "sqlite",
"id": "170be9e2-c822-4d3a-a0b1-18c8468f1c5d", "id": "ebffb4c7-5ecf-46d0-93c6-68f8e48a9fc4",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"tables": { "tables": {
"account": { "account": {
@ -524,13 +524,6 @@
} }
}, },
"indexes": { "indexes": {
"hour_time_unique": {
"name": "hour_time_unique",
"columns": [
"time"
],
"isUnique": true
},
"hour_dayId_time_unique": { "hour_dayId_time_unique": {
"name": "hour_dayId_time_unique", "name": "hour_dayId_time_unique",
"columns": [ "columns": [

View File

@ -5,8 +5,8 @@
{ {
"idx": 0, "idx": 0,
"version": "6", "version": "6",
"when": 1732706352404, "when": 1732766704666,
"tag": "0000_exotic_dakota_north", "tag": "0000_cold_golden_guardian",
"breakpoints": true "breakpoints": true
} }
] ]

View File

@ -21,7 +21,6 @@ function createdAtField() {
} }
export function calcInverseColor(hexcode: string): string { export function calcInverseColor(hexcode: string): string {
console.log(hexcode);
const hex = hexcode.replace("#", ""); const hex = hexcode.replace("#", "");
const r = parseInt(hex.substr(0, 2), 16); const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16); const g = parseInt(hex.substr(2, 2), 16);
@ -148,7 +147,7 @@ export const hours = sqliteTable(
.notNull() .notNull()
.references(() => users.id, { onDelete: "cascade" }), .references(() => users.id, { onDelete: "cascade" }),
comment: text("comment"), comment: text("comment"),
time: integer("time").unique(), time: integer("time"),
dayId: text("dayId").notNull().references(() => days.id, { onDelete: "cascade" }), dayId: text("dayId").notNull().references(() => days.id, { onDelete: "cascade" }),
categoryId: text("categoryId").references(() => categories.id), categoryId: text("categoryId").references(() => categories.id),
}, },

View File

@ -3,7 +3,6 @@ import { api } from "../trpc";
export function useUpdateUserTimezone( export function useUpdateUserTimezone(
...opts: Parameters<typeof api.users.changeTimezone.useMutation> ...opts: Parameters<typeof api.users.changeTimezone.useMutation>
) { ) {
const apiUtils = api.useUtils();
return api.users.changeTimezone.useMutation({ return api.users.changeTimezone.useMutation({
...opts[0], ...opts[0],
onSuccess: (res, req, meta) => { onSuccess: (res, req, meta) => {

View File

@ -6,6 +6,7 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@date-fns/tz": "^1.2.0", "@date-fns/tz": "^1.2.0",
"@date-fns/utc": "^2.1.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"winston": "^3.17.0", "winston": "^3.17.0",
"zod": "^3.23.8" "zod": "^3.23.8"

View File

@ -6,10 +6,10 @@ export const zHourSchema = z.object({
dayId: z.string(), dayId: z.string(),
date: z.string().optional(), date: z.string().optional(),
time: z.number(), time: z.number(),
categoryCode: z.coerce.number().nullable(), categoryCode: z.coerce.number().nullish(),
categoryId: z.string().nullable(), categoryId: z.string().nullish(),
categoryName: z.string().nullable(), categoryName: z.string().nullish(),
comment: z.string().nullable(), comment: z.string().nullish(),
}); });
export type ZHour = z.infer<typeof zHourSchema>; export type ZHour = z.infer<typeof zHourSchema>;

View File

@ -1,13 +1,32 @@
import { format } from "date-fns"; import { format, addHours } from "date-fns";
import { TZDate } from "@date-fns/tz"; import { TZDate } from "@date-fns/tz";
import { UTCDate, utc } from "@date-fns/utc";
export function dateFromInput(input: { dateQuery: string, timezone: string }) { export function dateFromInput(input: { dateQuery: string, timezone: string }) {
let t: TZDate; let t: string;
if (input.dateQuery == "today") { if (input.dateQuery == "today") {
t = TZDate.tz(input.timezone); t = new Date();
return format(t, "yyyy-MM-dd", { in: input.timezone });
} }
else { else {
t = new TZDate(input.dateQuery, input.timezone); t = new UTCDate(input.dateQuery);
return format(t, "yyyy-MM-dd", { in: utc });
} }
return format(t, "yyyy-MM-dd") + "T00:00:00";
}
function generateHour(d, t) {
const dt: TZDate = addHours(d, t);
return {
date: format(dt, 'yyyy-MM-dd'),
time: parseInt(format(dt, 'H')),
};
}
export function hoursListInTimezone(input: { dateQuery: string, timezone: string }) {
const dateStr = dateFromInput(input);
const d = new TZDate(dateStr, input.timezone);
return Array.from({ length: 24 }, (_, t) =>
generateHour(d, t)
);
} }

View File

@ -27,7 +27,6 @@ async function createCategory(
) { ) {
return ctx.db.transaction(async (trx) => { return ctx.db.transaction(async (trx) => {
console.log("Creating a category", input);
try { try {
const result = await trx const result = await trx
.insert(categories) .insert(categories)
@ -54,7 +53,6 @@ async function createCategory(
}; };
} catch (e) { } catch (e) {
console.log(e);
if (e instanceof SqliteError) { if (e instanceof SqliteError) {
if (e.code == "SQLITE_CONSTRAINT_UNIQUE") { if (e.code == "SQLITE_CONSTRAINT_UNIQUE") {
throw new TRPCError({ throw new TRPCError({
@ -196,8 +194,6 @@ export const categoriesAppRouter = router({
category.parentId = categoryId; category.parentId = categoryId;
} }
console.log(category);
return category; return category;
}), }),
create: authedProcedure create: authedProcedure

View File

@ -2,16 +2,17 @@ import { experimental_trpcMiddleware, TRPCError } from "@trpc/server";
import { and, desc, eq, inArray, notExists } from "drizzle-orm"; import { and, desc, eq, inArray, notExists } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { SqliteError } from "@lifetracker/db"; import { db, SqliteError } from "@lifetracker/db";
import { categories, days, hours, } from "@lifetracker/db/schema"; import { categories, days, hours, users } from "@lifetracker/db/schema";
import { import {
zDaySchema, ZDay zDaySchema, ZDay
} from "@lifetracker/shared/types/days"; } from "@lifetracker/shared/types/days";
import type { Context } from "../index"; import type { Context } from "../index";
import { authedProcedure, router } from "../index"; import { authedProcedure, router } from "../index";
import { dateFromInput } from "@lifetracker/shared/utils/days"; import { dateFromInput, hoursListInTimezone } from "@lifetracker/shared/utils/days";
import { format } from "date-fns"; import { closestIndexTo, format } from "date-fns";
import { TZDate } from "@date-fns/tz"; import { TZDate } from "@date-fns/tz";
import { hoursAppRouter } from "./hours";
async function createDay(date: string, ctx: Context) { async function createDay(date: string, ctx: Context) {
return await ctx.db.transaction(async (trx) => { return await ctx.db.transaction(async (trx) => {
@ -30,21 +31,8 @@ async function createDay(date: string, ctx: Context) {
comment: days.comment, comment: days.comment,
}); });
const dayId = dayRes[0].id;
// Generate 24 "hour" objects
const hoursData = Array.from({ length: 24 }, (_, hour) => ({
userId: ctx.user!.id,
dayId: dayId,
time: hour
}));
// Insert the "hour" objects
await trx.insert(hours).values(hoursData);
return dayRes; return dayRes;
} catch (e) { } catch (e) {
console.log(e);
if (e instanceof SqliteError) { if (e instanceof SqliteError) {
if (e.code == "SQLITE_CONSTRAINT_UNIQUE") { if (e.code == "SQLITE_CONSTRAINT_UNIQUE") {
throw new TRPCError({ throw new TRPCError({
@ -61,19 +49,26 @@ async function createDay(date: string, ctx: Context) {
}); });
} }
export const daysAppRouter = router({ async function createHour(day, time, ctx,) {
get: authedProcedure const newHour = (await ctx.db.insert(hours).values({
.input(z.object({ dayId: day.id,
dateQuery: z.string(), time: time,
})) userId: ctx.user!.id,
.output(zDaySchema) }).returning());
.query(async ({ input, ctx }) => { console.log(newHour);
const date = dateFromInput({ timezone: ctx.user.timezone, ...input }); return newHour[0];
console.log(ctx.user.timezone); }
// Fetch the day data
let dayRes; async function getTimezone(ctx: Context) {
dayRes = await ctx.db const dbTimezone = await ctx.db.select({
.select({ timezone: users.timezone
}).from(users).where(eq(users.id, ctx.user!.id));
return dbTimezone[0].timezone as string;
}
async function getDay(input: { dateQuery: string }, ctx: Context, date: string) {
const dayRes = await ctx.db.select({
id: days.id, id: days.id,
date: days.date, date: days.date,
mood: days.mood, mood: days.mood,
@ -82,33 +77,80 @@ export const daysAppRouter = router({
.from(days) .from(days)
.where(eq(days.date, date)); .where(eq(days.date, date));
if (dayRes.length === 0) { const day = dayRes.length == 0
dayRes = await createDay(date, ctx); ? (await createDay(date, ctx))[0]
} : (dayRes[0]);
const day = dayRes[0]; const dayHours = await Promise.all(
Array.from({ length: 24 }).map(async function (_, i) {
// Fetch the hours data for the corresponding dayId const existing = await ctx.db.select({
const hoursRes = await ctx.db
.select({
id: hours.id,
dayId: hours.dayId, dayId: hours.dayId,
time: hours.time, time: hours.time,
categoryId: hours.categoryId, userId: hours.userId,
categoryCode: categories.code, }).from(hours)
categoryName: categories.name, .where(and(
comment: hours.comment, eq(hours.userId, ctx.user!.id),
}) eq(hours.dayId, day.id),
.from(hours) eq(hours.time, i)
.where(eq(hours.dayId, day.id)) ))
.leftJoin(categories, eq(categories.id, hours.categoryId)) return existing.length == 0 ? createHour(day, i, ctx) : existing[0];
}));
// Combine the day and hours data return {
const result = { hours: dayHours,
...day, ...day
hours: hoursRes, }
}
export const daysAppRouter = router({
get: authedProcedure
.input(z.object({
dateQuery: z.string(),
}))
.output(zDaySchema)
.query(async ({ input, ctx }) => {
const timezone = await getTimezone(ctx);
const date = dateFromInput({
dateQuery: input.dateQuery,
timezone: timezone
});
const allHours = hoursListInTimezone({
timezone,
...input
});
const dayRange = [...new Set(allHours.map(({ date: date, time: _time }) => date))];
const allDayIds = await Promise.all(dayRange.map(async function (date) {
const dayObj = await getDay(input, ctx, date);
return {
id: dayObj.id,
date: dayObj.date
}
}));
const dayHours = await Promise.all(allHours.map(async function ({ date, time }) {
const dayId = allDayIds.find((_value: { id, date }) => date == date)!.id;
const hourMatch = await ctx.db.select().from(hours)
.where(and(
eq(hours.time, time),
eq(hours.dayId, dayId)));
// console.log("Search values:: ", `d: ${date}, t: ${time}, dayId: ${dayId}`)
// console.log("hourMatch", hourMatch[0]);
return hourMatch;
}));
return {
...await getDay(input, ctx, date),
hours: dayHours.flat(),
}; };
return result;
}), }),
update: authedProcedure update: authedProcedure
.input( .input(

View File

@ -22,7 +22,6 @@ export const hoursAppRouter = router({
.output(zHourSchema) .output(zHourSchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const date = dateFromInput(input); const date = dateFromInput(input);
console.log(input);
const hourRes = await ctx.db const hourRes = await ctx.db
.select({ .select({
id: hours.id, id: hours.id,
@ -37,8 +36,6 @@ export const hoursAppRouter = router({
.leftJoin(days, eq(days.id, hours.dayId)) // Ensure days table is joined first .leftJoin(days, eq(days.id, hours.dayId)) // Ensure days table is joined first
.leftJoin(categories, eq(categories.id, hours.categoryId)) .leftJoin(categories, eq(categories.id, hours.categoryId))
.where(and(eq(hours.time, input.time), eq(days.date, date))) // Use correct alias for days table .where(and(eq(hours.time, input.time), eq(days.date, date))) // Use correct alias for days table
console.log(hourRes);
return { return {
date: format(date, "yyyy-MM-dd"), date: format(date, "yyyy-MM-dd"),
...hourRes[0] ...hourRes[0]

View File

@ -198,8 +198,6 @@ export const usersAppRouter = router({
timezone: input.newTimezone, timezone: input.newTimezone,
}) })
.where(eq(users.id, ctx.user.id)); .where(eq(users.id, ctx.user.id));
console.log("changeTimezone input", input.newTimezone);
return input.newTimezone; return input.newTimezone;
}), }),
}); });

14
pnpm-lock.yaml generated
View File

@ -23,6 +23,9 @@ importers:
'@date-fns/tz': '@date-fns/tz':
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.2.0 version: 1.2.0
'@date-fns/utc':
specifier: ^2.1.0
version: 2.1.0
'@lifetracker/db': '@lifetracker/db':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/db version: link:../../packages/db
@ -148,6 +151,9 @@ importers:
'@date-fns/tz': '@date-fns/tz':
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.2.0 version: 1.2.0
'@date-fns/utc':
specifier: ^2.1.0
version: 2.1.0
'@emoji-mart/data': '@emoji-mart/data':
specifier: ^1.1.2 specifier: ^1.1.2
version: 1.2.1 version: 1.2.1
@ -478,6 +484,9 @@ importers:
'@date-fns/tz': '@date-fns/tz':
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.2.0 version: 1.2.0
'@date-fns/utc':
specifier: ^2.1.0
version: 2.1.0
date-fns: date-fns:
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.0 version: 4.1.0
@ -1935,6 +1944,9 @@ packages:
'@date-fns/tz@1.2.0': '@date-fns/tz@1.2.0':
resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==} resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==}
'@date-fns/utc@2.1.0':
resolution: {integrity: sha512-176grgAgU2U303rD2/vcOmNg0kGPbhzckuH1TEP2al7n0AQipZIy9P15usd2TKQCG1g+E1jX/ZVQSzs4sUDwgA==}
'@discoveryjs/json-ext@0.5.7': '@discoveryjs/json-ext@0.5.7':
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
@ -14384,6 +14396,8 @@ snapshots:
'@date-fns/tz@1.2.0': {} '@date-fns/tz@1.2.0': {}
'@date-fns/utc@2.1.0': {}
'@discoveryjs/json-ext@0.5.7': {} '@discoveryjs/json-ext@0.5.7': {}
'@docsearch/css@3.6.2': {} '@docsearch/css@3.6.2': {}