diff --git a/src/app/(dashboard)/_components/waitlist-form.tsx b/src/app/(dashboard)/_components/waitlist-form.tsx index 4a0c7f74..9382e05b 100644 --- a/src/app/(dashboard)/_components/waitlist-form.tsx +++ b/src/app/(dashboard)/_components/waitlist-form.tsx @@ -27,14 +27,11 @@ export const WaitListInvitationForm = ({ const { mutateAsync, isLoading } = trpc.waitlist.sendUserInvitation.useMutation({ - async onSuccess(data) { - console.log(data); + async onSuccess() { await utils.waitlist.invalidate(); }, }); - console.log(form.formState.errors); - const onSubmit = async (values: z.infer) => { try { await mutateAsync(values); diff --git a/src/app/(dashboard)/app/_components/weather.tsx b/src/app/(dashboard)/app/_components/weather.tsx index 8e1d0ba9..60be5970 100644 --- a/src/app/(dashboard)/app/_components/weather.tsx +++ b/src/app/(dashboard)/app/_components/weather.tsx @@ -2,10 +2,11 @@ import { type FC } from "react"; +import { Icon } from "@/components/icon"; import { useCoords } from "@/hooks/useCoords"; import { trpc } from "@/trpc/client"; import { getFormattedWeatherDescription } from "@/utils/getFormattedWeatherDescription"; -import { Skeleton } from "@nextui-org/react"; +import { Skeleton, Tooltip } from "@nextui-org/react"; export const WeatherData: FC = () => { const coords = useCoords(); @@ -39,11 +40,31 @@ export const WeatherData: FC = () => { if (!weatherData) return null; return ( - - You can expect a ๐Ÿ‘† high of {weatherData.temp_max.toFixed()}ยบ and a ๐Ÿ‘‡ low - of {weatherData.temp_min.toFixed()}ยบ - {getFormattedWeatherDescription(weatherData.summary)} for today's - weather. - + + + Info + + + The weather conditions are until the end of the day, so the high and + low temps are until midnight. + + + } + placement="left-start" + delay={1000} + > + + ); }; diff --git a/src/server/api/routers/waitlist.ts b/src/server/api/routers/waitlist.ts index 9e8f832f..5a99cd3e 100644 --- a/src/server/api/routers/waitlist.ts +++ b/src/server/api/routers/waitlist.ts @@ -47,8 +47,6 @@ export const waitlistRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const { invitationId } = input; - console.log("invitationId", invitationId); - const invitation = await ctx.db .select() .from(waitlist) diff --git a/src/server/api/routers/weather.ts b/src/server/api/routers/weather.ts index f261fb10..b4b8e5c0 100644 --- a/src/server/api/routers/weather.ts +++ b/src/server/api/routers/weather.ts @@ -1,84 +1,124 @@ import { TRPCError } from "@trpc/server"; -import fetch from "node-fetch"; import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "../trpc"; -type RawWeatherData = { - properties: { - timeseries: { - data: { - next_12_hours: { - summary: { - symbol_code: string; - }; - }; - instant: { - details: { - air_temperature: number; - }; - }; - }; - }[]; +const hours = z + .object({ + summary: z.object({ + symbol_code: z.string(), + }), + }) + .optional(); + +const timeseriesSchema = z.array( + z.object({ + time: z.string(), + data: z.object({ + next_12_hours: hours, + next_6_hours: hours, + next_1_hours: hours, + instant: z.object({ + details: z.object({ + air_temperature: z.number(), + }), + }), + }), + }), +); + +const weatherDataSchema = z.object({ + temp_max: z.number(), + temp_min: z.number(), + summary: z.string().optional(), +}); + +const input = z.object({ + latitude: z.number(), + longitude: z.number(), +}); + +const getCurrentWeatherData = async ({ + latitude, + longitude, +}: z.infer) => { + const date = new Date().toISOString().slice(0, 10); + const response = await fetch( + `https://api.met.no/weatherapi/locationforecast/2.0/compact?lat=${latitude}&lon=${longitude}`, + { + headers: { + "User-Agent": `noodle.run (https://github.com/noodle-run/noodle)`, + }, + }, + ); + + const data = (await response.json()) as { + properties: { timeseries: unknown }; + }; + + const timeseries = timeseriesSchema + .parse(data.properties.timeseries as z.infer) + .filter((one) => one.time.includes(date)); + + const temperatures = timeseries.map( + (t) => t.data.instant.details.air_temperature, + ); + + let summary; + if (timeseries[0]) { + const { next_12_hours, next_6_hours, next_1_hours } = timeseries[0].data; + const nextData = next_12_hours ?? next_6_hours ?? next_1_hours; + summary = nextData?.summary.symbol_code; + } + + const weatherData = { + summary, + temp_max: Math.max(...temperatures), + temp_min: Math.min(...temperatures), }; -}; -type WeatherData = { - temp_max: number; - temp_min: number; - summary: string; + return weatherDataSchema.parse(weatherData); }; export const weatherRouter = createTRPCRouter({ getWeatherData: protectedProcedure - .input( - z.object({ - latitude: z.number(), - longitude: z.number(), - }), - ) - .query(async ({ input }) => { - const { latitude, longitude } = input; - - const response = await fetch( - `https://api.met.no/weatherapi/locationforecast/2.0/compact?lat=${latitude}&lon=${longitude}`, - { - headers: { - "User-Agent": `noodle.run (https://github.com/noodle-run/noodle)`, - }, - }, - ); - - if (!response.ok) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to fetch weather data", - }); - } + .input(input) + .output(weatherDataSchema) + .query(async ({ input, ctx }) => { + const date = new Date().toISOString().slice(0, 10); + const cacheKey = `weather:${date}:${ctx.auth.userId}`; - const rawWeatherData: RawWeatherData = - (await response.json()) as RawWeatherData; + if (typeof ctx.redis !== "undefined" && typeof ctx.redis !== "string") { + try { + const cachedWeatherData = await ctx.redis.get(cacheKey); - if (rawWeatherData.properties.timeseries.length < 12) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Partial weather data", - }); - } + if (!cachedWeatherData) { + const weatherData = await getCurrentWeatherData(input); + const secondsUntilMidnight = Math.round( + (new Date().setHours(24, 0, 0, 0) - Date.now()) / 1000, + ); - const temperatures = []; + await ctx.redis.set(cacheKey, JSON.stringify(weatherData), { + ex: secondsUntilMidnight, + }); + return weatherData; + } - for (const timeseries of rawWeatherData.properties.timeseries) { - temperatures.push(timeseries.data.instant.details.air_temperature); + return weatherDataSchema.parse(cachedWeatherData); + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to fetch cached weather data", + }); + } } - const weatherData: WeatherData = { - summary: - rawWeatherData.properties.timeseries[0]!.data.next_12_hours.summary - .symbol_code, - temp_max: Math.max(...temperatures), - temp_min: Math.min(...temperatures), - }; - - return weatherData; + try { + return getCurrentWeatherData(input); + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to fetch weather data", + }); + } }), }); diff --git a/src/server/api/trpc.ts b/src/server/api/trpc.ts index 4a47887d..748c49c7 100644 --- a/src/server/api/trpc.ts +++ b/src/server/api/trpc.ts @@ -1,6 +1,8 @@ import { db } from "@/db"; +import { env } from "@/env.mjs"; import { getAuth } from "@clerk/nextjs/server"; import { TRPCError, initTRPC } from "@trpc/server"; +import { Redis } from "@upstash/redis"; import { type NextRequest } from "next/server"; import superjson from "superjson"; import { ZodError } from "zod"; @@ -10,9 +12,12 @@ type CreateContextOptions = { }; const createInnerTRPCContext = ({ auth }: CreateContextOptions) => { + const redis = env.UPSTASH_REDIS_REST_URL && Redis.fromEnv(); + return { auth, db, + redis, }; };