chore: add weather component c690f6e1
Steve · 2026-06-07 20:57 3 file(s) · +155 −27
src/components/now/Weather.astro (added) +83 −0
1 +
---
2 +
interface Props {
3 +
	weather: string;
4 +
	leadingSeparator?: boolean;
5 +
}
6 +
7 +
const { weather, leadingSeparator } = Astro.props;
8 +
9 +
type WeatherCategory =
10 +
	| "storm"
11 +
	| "sleet"
12 +
	| "snow"
13 +
	| "rain"
14 +
	| "fog"
15 +
	| "partlyCloudy"
16 +
	| "cloudy"
17 +
	| "clear"
18 +
	| "unknown";
19 +
20 +
const categoryRules: { category: WeatherCategory; keywords: string[] }[] = [
21 +
	{ category: "storm", keywords: ["thunder", "tstorm", "storm"] },
22 +
	{ category: "sleet", keywords: ["sleet", "freez", "frzg", "mix"] },
23 +
	{ category: "snow", keywords: ["snow", "flurr"] },
24 +
	{ category: "rain", keywords: ["rain", "shower", "drizzle"] },
25 +
	{ category: "fog", keywords: ["fog", "mist", "haze"] },
26 +
	{ category: "partlyCloudy", keywords: ["partly", "variable"] },
27 +
	{ category: "cloudy", keywords: ["overcast", "cloud"] },
28 +
	{ category: "clear", keywords: ["clear", "sunny", "sun", "fair"] },
29 +
];
30 +
31 +
function categorize(conditions: string): WeatherCategory {
32 +
	const c = conditions.toLowerCase();
33 +
	for (const rule of categoryRules) {
34 +
		for (const kw of rule.keywords) {
35 +
			if (c.includes(kw)) return rule.category;
36 +
		}
37 +
	}
38 +
	return "unknown";
39 +
}
40 +
41 +
const weatherIcons: Partial<Record<WeatherCategory, string>> = {
42 +
	partlyCloudy: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M164 76a71.9 71.9 0 0 0-22.14 3.48A51.8 51.8 0 0 0 129 63.83l11.56-16.51a4 4 0 0 0-6.56-4.59l-11.55 16.51A52 52 0 0 0 96 52c-1.71 0-3.4.09-5.06.25l-3.5-19.85a4 4 0 0 0-7.88 1.39l3.5 19.84A52.2 52.2 0 0 0 55.85 71L39.32 59.42A4 4 0 0 0 34.73 66l16.53 11.54A51.63 51.63 0 0 0 44 104c0 1.69.09 3.37.25 5l-19.85 3.5a4 4 0 0 0 .69 7.94a4 4 0 0 0 .7-.06l19.85-3.5A52.1 52.1 0 0 0 54 134.6A48 48 0 0 0 84 220h80a72 72 0 0 0 0-144M52 104a44 44 0 0 1 82.33-21.61a72.23 72.23 0 0 0-38.82 43A48.3 48.3 0 0 0 84 124a47.76 47.76 0 0 0-23.4 6.11A44 44 0 0 1 52 104m112 108H84a40 40 0 1 1 9.43-78.88A71.6 71.6 0 0 0 92 143.77a4 4 0 0 0 8 .46a64.3 64.3 0 0 1 2-12.67c0-.12.07-.24.09-.36A64.06 64.06 0 1 1 164 212"/></svg>`,
43 +
	clear: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M124 40v-8a4 4 0 0 1 8 0v8a4 4 0 0 1-8 0m64 88a60 60 0 1 1-60-60a60.07 60.07 0 0 1 60 60m-8 0a52 52 0 1 0-52 52a52.06 52.06 0 0 0 52-52M61.17 66.83a4 4 0 0 0 5.66-5.66l-8-8a4 4 0 0 0-5.66 5.66Zm0 122.34l-8 8a4 4 0 0 0 5.66 5.66l8-8a4 4 0 0 0-5.66-5.66m136-136l-8 8a4 4 0 0 0 5.66 5.66l8-8a4 4 0 1 0-5.66-5.66m-2.34 136a4 4 0 0 0-5.66 5.66l8 8a4 4 0 0 0 5.66-5.66ZM40 124h-8a4 4 0 0 0 0 8h8a4 4 0 0 0 0-8m88 88a4 4 0 0 0-4 4v8a4 4 0 0 0 8 0v-8a4 4 0 0 0-4-4m96-88h-8a4 4 0 0 0 0 8h8a4 4 0 0 0 0-8"/></svg>`,
44 +
	cloudy: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M160 44a84.11 84.11 0 0 0-76.41 49.12A60.7 60.7 0 0 0 72 92a60 60 0 0 0 0 120h88a84 84 0 0 0 0-168m0 160H72a52 52 0 1 1 8.55-103.3A83.7 83.7 0 0 0 76 128a4 4 0 0 0 8 0a76 76 0 1 1 76 76"/></svg>`,
45 +
	storm: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M156 20a72.19 72.19 0 0 0-68.49 49.39A48 48 0 1 0 76 164h44.94l-20.37 33.94A4 4 0 0 0 104 204h32.94l-20.37 33.94a4 4 0 0 0 6.86 4.12l24-40A4 4 0 0 0 144 196h-32.94l19.2-32H156a72 72 0 0 0 0-144m0 136H76a40 40 0 1 1 9.43-78.88A71.6 71.6 0 0 0 84 87.77a4 4 0 0 0 8 .46A64.06 64.06 0 1 1 156 156"/></svg>`,
46 +
	rain: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="m155.33 194.22l-32 48a4 4 0 1 1-6.66-4.44l32-48a4 4 0 0 1 6.66 4.44M228 92a72.08 72.08 0 0 1-72 72h-25.86l-30.81 46.22a4 4 0 1 1-6.66-4.44L120.53 164H76a48 48 0 1 1 11.51-94.61A72.08 72.08 0 0 1 228 92m-8 0a64.06 64.06 0 0 0-128-3.77a4 4 0 0 1-8-.46a71.6 71.6 0 0 1 1.42-10.65A40 40 0 1 0 76 156h80a64.07 64.07 0 0 0 64-64"/></svg>`,
47 +
	snow: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M84 196a8 8 0 1 1-8-8a8 8 0 0 1 8 8m32 8a8 8 0 1 0 8 8a8 8 0 0 0-8-8m48-16a8 8 0 1 0 8 8a8 8 0 0 0-8-8m-96 40a8 8 0 1 0 8 8a8 8 0 0 0-8-8m88 0a8 8 0 1 0 8 8a8 8 0 0 0-8-8m72-136a72.08 72.08 0 0 1-72 72H76a48 48 0 1 1 11.51-94.61A72.08 72.08 0 0 1 228 92m-8 0a64.06 64.06 0 0 0-128-3.77a4 4 0 0 1-8-.46a71.6 71.6 0 0 1 1.42-10.65A40 40 0 1 0 76 156h80a64.07 64.07 0 0 0 64-64"/></svg>`,
48 +
	sleet: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M84 196a8 8 0 1 1-8-8a8 8 0 0 1 8 8m32 8a8 8 0 1 0 8 8a8 8 0 0 0-8-8m48-16a8 8 0 1 0 8 8a8 8 0 0 0-8-8m-96 40a8 8 0 1 0 8 8a8 8 0 0 0-8-8m88 0a8 8 0 1 0 8 8a8 8 0 0 0-8-8m72-136a72.08 72.08 0 0 1-72 72H76a48 48 0 1 1 11.51-94.61A72.08 72.08 0 0 1 228 92m-8 0a64.06 64.06 0 0 0-128-3.77a4 4 0 0 1-8-.46a71.6 71.6 0 0 1 1.42-10.65A40 40 0 1 0 76 156h80a64.07 64.07 0 0 0 64-64"/></svg>`,
49 +
	fog: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M120 204H72a4 4 0 0 1 0-8h48a4 4 0 0 1 0 8m64-8h-24a4 4 0 0 0 0 8h24a4 4 0 0 0 0-8m-24 32h-56a4 4 0 0 0 0 8h56a4 4 0 0 0 0-8m68-128a72.08 72.08 0 0 1-72 72H76a48 48 0 1 1 11.51-94.61A72.08 72.08 0 0 1 228 100m-8 0a64.06 64.06 0 0 0-128-3.77a4 4 0 0 1-8-.46a71.6 71.6 0 0 1 1.42-10.65A40 40 0 1 0 76 164h80a64.07 64.07 0 0 0 64-64"/></svg>`,
50 +
};
51 +
52 +
const parts = weather.split(",").map((p) => p.trim());
53 +
54 +
const valid = parts.length >= 4;
55 +
56 +
const qualifierRE =
57 +
	/^(slight chance|chance|isolated|scattered|numerous|widespread|patchy|areas|periods|occasional|frequent)\s+(of\s+)?/i;
58 +
const trailingQualifierRE = /\s+(likely)$/i;
59 +
60 +
const conditions = valid
61 +
	? parts[0].replace(qualifierRE, "").replace(trailingQualifierRE, "").trim()
62 +
	: "";
63 +
const temperature = valid ? `${parts[1]}°F` : "";
64 +
const location = valid ? `${parts[2]}, ${parts[3]}` : "";
65 +
const icon = valid ? weatherIcons[categorize(conditions)] : undefined;
66 +
---
67 +
68 +
{valid && (
69 +
	<>
70 +
    {leadingSeparator && 
71 +
		<span>•</span>
72 +
    }
73 +
		<span class="inline-flex items-center gap-1.5">
74 +
			{icon && <span class="inline-flex items-center [&>svg]:size-4" set:html={icon} />}
75 +
        {conditions}{" "}{temperature}
76 +
		</span>
77 +
		<span>•</span>
78 +
		<span class="inline-flex items-center gap-1.5">
79 +
			<svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M128 68a36 36 0 1 0 36 36a36 36 0 0 0-36-36m0 64a28 28 0 1 1 28-28a28 28 0 0 1-28 28m0-112a84.09 84.09 0 0 0-84 84c0 30.42 14.17 62.79 41 93.62a250 250 0 0 0 40.73 37.66a4 4 0 0 0 4.58 0A250 250 0 0 0 171 197.62c26.81-30.83 41-63.2 41-93.62a84.09 84.09 0 0 0-84-84m37.1 172.23A254.6 254.6 0 0 1 128 227a254.6 254.6 0 0 1-37.1-34.81C73.15 171.8 52 139.9 52 104a76 76 0 0 1 152 0c0 35.9-21.15 67.8-38.9 88.23"/></svg>
80 +
			{location}
81 +
		</span>
82 +
	</>
83 +
)}
src/pages/now/[slug].astro +8 −1
1 1
---
2 2
import PageLayout from "@/layouts/Base.astro";
3 +
import Weather from "@/components/now/Weather.astro";
3 4
import { POSTS_API } from "@/data/constants";
4 5
import { createMarkdownRenderer } from "@/utils";
5 6
27 28
	content: string;
28 29
	created_at: string;
29 30
	updated_at: string;
31 +
	weather: string | null;
30 32
}
31 33
32 34
let title: string | null = "Post";
34 36
let contentHTML = "";
35 37
let publishedAt = "";
36 38
let errorMessage = "";
39 +
let weatherRaw = "";
37 40
let isError = false;
38 41
39 42
try {
56 59
			? new Date(post.published_date).toLocaleDateString()
57 60
			: "";
58 61
		contentHTML = md.render(post.content || "");
62 +
		weatherRaw = post.weather || "";
59 63
	}
60 64
} catch (err) {
61 65
	console.error("Error fetching post:", err);
82 86
		) : (
83 87
			<>
84 88
				{title && <h1 class="title mb-2">{title}</h1>}
85 -
				<time class="text-sm text-zinc-400">{publishedAt}</time>
89 +
				<div class="flex text-sm text-zinc-400 items-center gap-x-3 gap-y-1 flex-wrap">
90 +
					<time>{publishedAt}</time>
91 +
					{weatherRaw && <Weather leadingSeparator weather={weatherRaw} />}
92 +
				</div>
86 93
				<div class="prose prose-invert max-w-none my-4">
87 94
					<Fragment set:html={contentHTML} />
88 95
				</div>
src/pages/now/index.astro +64 −26
3 3
4 4
import PageLayout from "@/layouts/Base.astro";
5 5
import NowUpdates from "@/components/now/NowUpdates.astro";
6 +
import Weather from "@/components/now/Weather.astro";
6 7
7 8
const meta = {
8 9
	title: "/now",
9 10
	description: "What I'm up to recently",
10 11
};
12 +
13 +
const STATION = "MRX";
14 +
const GRID_X = "23";
15 +
const GRID_Y = "5";
16 +
const LOCATION = "Lookout Mountain, TN";
17 +
18 +
const forecastRequest = await fetch(
19 +
	`https://api.weather.gov/gridpoints/${STATION}/${GRID_X},${GRID_Y}/forecast/hourly`,
20 +
	{
21 +
		method: "GET",
22 +
		headers: {
23 +
			accept: "application/geo+json",
24 +
			"User-Agent": "(stevedylan.dev, contact@stevedylan.dev)",
25 +
		},
26 +
	},
27 +
);
28 +
const forecast = await forecastRequest.json();
29 +
const { shortForecast, temperature } = forecast.properties.periods[0];
30 +
const weather = `${shortForecast},${temperature},${LOCATION}`;
11 31
---
32 +
12 33
<PageLayout meta={meta}>
13 34
  <div class="space-y-6">
14 35
    <h1 class="title">/now</h1>
36 +
    <div class="flex text-sm text-zinc-400 items-center gap-x-3 gap-y-1 flex-wrap">
37 +
      <Weather weather={weather} />
38 +
    </div>
15 39
    <p>What I'm up to recently</p>
16 40
    <ul class="list-disc pl-4 space-y-4">
17 41
      <li>Working as Senior Solutions Engineer at Stablecore</li>
18 -
      <li>Building personal software with <a class="style-link" target="_blank" rel="noreferrer" href="https://andromeda.build">Andromeda</a></li>
42 +
      <li>
43 +
        Building personal software with <a
44 +
          class="style-link"
45 +
          target="_blank"
46 +
          rel="noreferrer"
47 +
          href="https://andromeda.build">Andromeda</a>
48 +
      </li>
19 49
      <li>Updating this site with fun stuff</li>
20 50
      <li>Learning how to build HTTP from TCP in Go</li>
21 51
      <li>Exploring Chinese loose leaf tea</li>
23 53
      <li>Reading Dubliners</li>
24 54
    </ul>
25 55
    <p class="text-zinc-400">Last updated: May 9th, 2026</p>
26 -
  <div class="flex flex-row justify-between items-center w-full pt-12 pb-6">
27 -
    <h2 class="title">Updates</h2>
28 -
    <div class="flex gap-2 items-center">
29 -
    <a
30 -
				class="inline-block p-2 sm:hover:text-link"
31 -
        href="https://posts.stevedylan.dev/feed.xml"
32 -
				target="_blank"
33 -
				rel="noopener noreferrer"
34 -
			>
35 -
				<svg
36 -
					xmlns="http://www.w3.org/2000/svg"
37 -
					class="h-6 w-6"
38 -
					viewBox="0 0 24 24"
39 -
					stroke-width="1.5"
40 -
					stroke="currentColor"
41 -
					fill="none"
42 -
					stroke-linecap="round"
43 -
					stroke-linejoin="round"
44 -
				>
45 -
				<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M4 11a9 9 0 0 1 9 9M4 4a16 16 0 0 1 16 16"/><circle cx="5" cy="19" r="1"/></g>
46 -
				</svg>
47 -
				<span class="sr-only">RSS</span>
48 -
  			</a>
56 +
    <div class="flex flex-row justify-between items-center w-full pt-12 pb-6">
57 +
      <h2 class="title">Updates</h2>
58 +
      <div class="flex gap-2 items-center">
59 +
        <a
60 +
          class="inline-block p-2 sm:hover:text-link"
61 +
          href="https://posts.stevedylan.dev/feed.xml"
62 +
          target="_blank"
63 +
          rel="noopener noreferrer"
64 +
        >
65 +
          <svg
66 +
            xmlns="http://www.w3.org/2000/svg"
67 +
            class="h-6 w-6"
68 +
            viewBox="0 0 24 24"
69 +
            stroke-width="1.5"
70 +
            stroke="currentColor"
71 +
            fill="none"
72 +
            stroke-linecap="round"
73 +
            stroke-linejoin="round"
74 +
          >
75 +
            <g
76 +
              fill="none"
77 +
              stroke="currentColor"
78 +
              stroke-linecap="round"
79 +
              stroke-linejoin="round"
80 +
              stroke-width="2"
81 +
              ><path d="M4 11a9 9 0 0 1 9 9M4 4a16 16 0 0 1 16 16"
82 +
              ></path><circle cx="5" cy="19" r="1"></circle></g
83 +
            >
84 +
          </svg>
85 +
          <span class="sr-only">RSS</span>
86 +
        </a>
87 +
      </div>
49 88
    </div>
50 -
  </div>
51 -
  <NowUpdates />
89 +
    <NowUpdates />
52 90
  </div>
53 91
</PageLayout>