Skip to content

Commit

Permalink
Merge branch 'main' into feat/task-management
Browse files Browse the repository at this point in the history
  • Loading branch information
ixahmedxi authored Nov 8, 2023
2 parents fc5721b + 7923b72 commit f48c5dd
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 86 deletions.
3 changes: 0 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ env:
UPSTASH_REDIS_REST_URL: ${{secrets.UPSTASH_REDIS_REST_URL}}
UPSTASH_REDIS_REST_TOKEN: ${{secrets.UPSTASH_REDIS_REST_TOKEN}}

# Openweather
OPENWEATHER_API_KEY: ${{secrets.OPENWEATHER_API_KEY}}

jobs:
main:
name: CI
Expand Down
5 changes: 1 addition & 4 deletions src/app/(dashboard)/_components/waitlist-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof formSchema>) => {
try {
await mutateAsync(values);
Expand Down
35 changes: 28 additions & 7 deletions src/app/(dashboard)/app/_components/weather.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -39,11 +40,31 @@ export const WeatherData: FC = () => {
if (!weatherData) return null;

return (
<span className="text-tiny text-default-500">
You can expect a 👆 high of {weatherData.main.temp_max.toFixed()}º and a
👇 low of {weatherData.main.temp_min.toFixed()}º with{" "}
{getFormattedWeatherDescription(weatherData.weather[0]?.description)}{" "}
today.
</span>
<Tooltip
color="default"
showArrow
classNames={{
content: "border dark:border-default-100 border-default-200",
}}
content={
<div className="max-w-[30ch] px-1 py-2">
<span className="flex items-center gap-3 pb-2">
<Icon name="BadgeInfo" size={16} /> Info
</span>
<span className="text-tiny leading-5 text-default-500">
The weather conditions are until the end of the day, so the high and
low temps are until midnight.
</span>
</div>
}
placement="left-start"
delay={1000}
>
<button className="cursor-default text-left text-tiny leading-5 text-default-500">
You can expect a 👆 high of {weatherData.temp_max.toFixed()}º and a 👇
low of {weatherData.temp_min.toFixed()}º{" "}
{getFormattedWeatherDescription(weatherData.summary)}.
</button>
</Tooltip>
);
};
2 changes: 0 additions & 2 deletions src/env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export const env = createEnv({
UPSTASH_REDIS_REST_URL: z.string().optional(),
UPSTASH_REDIS_REST_TOKEN: z.string().optional(),
CLERK_SECRET_KEY: z.string().min(1),
OPENWEATHER_API_KEY: z.string().min(1),
},
client: {
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1),
Expand All @@ -26,7 +25,6 @@ export const env = createEnv({
process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN,
UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL,
OPENWEATHER_API_KEY: process.env.OPENWEATHER_API_KEY,
},
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
});
2 changes: 0 additions & 2 deletions src/server/api/routers/waitlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
129 changes: 105 additions & 24 deletions src/server/api/routers/weather.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,124 @@
import { TRPCError } from "@trpc/server";
import fetch from "node-fetch";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";

import { env } from "@/env.mjs";
const hours = z
.object({
summary: z.object({
symbol_code: z.string(),
}),
})
.optional();

import { createTRPCRouter, protectedProcedure } from "../trpc";
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<typeof input>) => {
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<typeof timeseriesSchema>)
.filter((one) => one.time.includes(date));

const temperatures = timeseries.map(
(t) => t.data.instant.details.air_temperature,
);

type WeatherData = {
main: {
temp_max: number;
temp_min: number;
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),
};
weather: {
description: 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;
.input(input)
.output(weatherDataSchema)
.query(async ({ input, ctx }) => {
const date = new Date().toISOString().slice(0, 10);
const cacheKey = `weather:${date}:${ctx.auth.userId}`;

const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${env.OPENWEATHER_API_KEY}&units=metric`,
);
if (typeof ctx.redis !== "undefined" && typeof ctx.redis !== "string") {
try {
const cachedWeatherData = await ctx.redis.get(cacheKey);

if (!response.ok) {
if (!cachedWeatherData) {
const weatherData = await getCurrentWeatherData(input);
const secondsUntilMidnight = Math.round(
(new Date().setHours(24, 0, 0, 0) - Date.now()) / 1000,
);

await ctx.redis.set(cacheKey, JSON.stringify(weatherData), {
ex: secondsUntilMidnight,
});
return weatherData;
}

return weatherDataSchema.parse(cachedWeatherData);
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch cached weather data",
});
}
}

try {
return getCurrentWeatherData(input);
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch weather data",
});
}

return response.json() as Promise<WeatherData>;
}),
});
5 changes: 5 additions & 0 deletions src/server/api/trpc.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -10,9 +12,12 @@ type CreateContextOptions = {
};

const createInnerTRPCContext = ({ auth }: CreateContextOptions) => {
const redis = env.UPSTASH_REDIS_REST_URL && Redis.fromEnv();

return {
auth,
db,
redis,
};
};

Expand Down
92 changes: 48 additions & 44 deletions src/utils/getFormattedWeatherDescription.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,52 @@
const weatherCodeToEnglish: Record<string, string> = {
clearsky: "🌞 a clear sky",
cloudy: "☁️ clouds",
fair: "⛅ fair",
fair_day: "🌤️ fair day",
fog: "🌫️ fog",
heavyrain: "🌧️ heavy rain",
heavyrainandthunder: "⛈️ heavy rain and thunder",
heavyrainshowers: "🌧️ heavy rain showers",
heavyrainshowersandthunder: "⛈️ heavy rain showers and thunder",
heavysleet: "🌨️ heavy sleet",
heavysleetandthunder: "⛈️ heavy sleet and thunder",
heavysleetshowers: "🌨️ heavy sleet showers",
heavysleetshowersandthunder: "⛈️ heavy sleet showers and thunder",
heavysnow: "❄️ heavy snow",
heavysnowandthunder: "⛈️ heavy snow and thunder",
heavysnowshowers: "❄️ heavy snow showers",
heavysnowshowersandthunder: "⛈️ heavy snow showers and thunder",
lightrain: "🌦️ light rain",
lightrainandthunder: "⛈️ light rain and thunder",
lightrainshowers: "🌦️ light rain showers",
lightrainshowersandthunder: "⛈️ light rain showers and thunder",
lightsleet: "🌧️ light sleet",
lightsleetandthunder: "⛈️ light sleet and thunder",
lightsleetshowers: "🌧️ light sleet showers",
lightsnow: "🌨️ light snow",
lightsnowandthunder: "⛈️ light snow and thunder",
lightsnowshowers: "🌨️ light snow showers",
lightssleetshowersandthunder: "⛈️ light sleet showers and thunder",
lightssnowshowersandthunder: "⛈️ light snow showers and thunder",
partlycloudy: "🌥️ some clouds",
rain: "🌧️ rain",
rainandthunder: "⛈️ rain and thunder",
rainshowers: "🌧️ rain showers",
rainshowersandthunder: "⛈️ rain showers and thunder",
sleet: "🌨️ sleet",
sleetandthunder: "⛈️ sleet and thunder",
sleetshowers: "🌨️ sleet showers",
sleetshowersandthunder: "⛈️ sleet showers and thunder",
snow: "❄️ snow",
snowandthunder: "⛈️ snow and thunder",
snowshowers: "❄️ snow showers",
snowshowersandthunder: "⛈️ snow showers and thunder",
};

export const getFormattedWeatherDescription = (
condition: string | undefined,
) => {
if (!condition) return;

const weatherToEmoji: Record<string, string> = {
"clear sky": "☀️",
"few clouds": "🌤️",
"scattered clouds": "⛅",
"broken clouds": "☁️",
"overcast clouds": "☁️",
rain: "🌧️",
"light rain": "🌧️",
"moderate rain": "🌧️",
"heavy intensity rain": "🌧️",
"very heavy rain": "🌧️",
"extreme rain": "🌧️",
"freezing rain": "🌨️",
"light intensity shower rain": "🌦️",
"shower rain": "🌧️",
"heavy intensity shower rain": "🌧️",
"ragged shower rain": "🌧️",
"light snow": "❄️",
snow: "❄️",
"heavy snow": "❄️",
sleet: "🌨️",
"shower sleet": "🌨️",
"light rain and snow": "🌨️",
"rain and snow": "🌨️",
"light shower snow": "🌨️",
"shower snow": "🌨️",
"heavy shower snow": "🌨️",
mist: "🌫️",
smoke: "🌫️",
haze: "🌫️",
"sand/ dust whirls": "🌪️",
fog: "🌫️",
sand: "🌫️",
dust: "🌫️",
"volcanic ash": "🌫️",
squalls: "🌬️",
tornado: "🌪️",
clear: "☀️",
clouds: "☁️",
};

return `${weatherToEmoji[condition.toLowerCase()] ?? ""} ${condition}`;
if (!condition || !weatherCodeToEnglish[condition]) return;
const weatherDescription = weatherCodeToEnglish[condition];
return `with ${weatherDescription}`;
};

0 comments on commit f48c5dd

Please sign in to comment.