feat: added /cellar 190ea561
Steve · 2026-04-24 23:01 4 file(s) · +184 −0
packages/client/src/components/page/WineCard.astro (added) +23 −0
1 +
---
2 +
const { wine, cellarApiUrl } = Astro.props;
3 +
---
4 +
5 +
<a
6 +
	href={`/cellar/${wine.short_id}`}
7 +
	class="flex items-center gap-4 py-3 border-b border-[#333] no-underline hover:opacity-70 transition-opacity"
8 +
>
9 +
	<div class="flex-shrink-0 w-20 h-20">
10 +
		<img
11 +
			src={`${cellarApiUrl}/api/wines/${wine.short_id}/pentagon.svg`}
12 +
			alt={`${wine.name} taste profile`}
13 +
			width="80"
14 +
			height="80"
15 +
			style="width:80px;height:80px;object-fit:contain;"
16 +
			loading="lazy"
17 +
		/>
18 +
	</div>
19 +
	<div class="flex flex-col gap-0.5">
20 +
		<span class="text-base">{wine.name}</span>
21 +
		<span class="text-xs opacity-50">{wine.origin}{wine.grape ? ` · ${wine.grape}` : ""}</span>
22 +
	</div>
23 +
</a>
packages/client/src/data/constants.ts +4 −0
28 28
    path: "/now",
29 29
  },
30 30
  {
31 +
    title: "Cellar",
32 +
    path: "/cellar",
33 +
  },
34 +
  {
31 35
    title: "Feeds",
32 36
    path: "/feeds",
33 37
  },
packages/client/src/pages/cellar/[short_id].astro (added) +114 −0
1 +
---
2 +
export const prerender = false;
3 +
4 +
import PageLayout from "@/layouts/Base.astro";
5 +
6 +
const { short_id } = Astro.params;
7 +
const CELLAR_API_URL = import.meta.env.CELLAR_API_URL ?? "https://cellar.stevedylan.dev";
8 +
9 +
let wine: any = null;
10 +
let fetchError: string | null = null;
11 +
try {
12 +
	const res = await fetch(`${CELLAR_API_URL}/api/wines/${short_id}`);
13 +
	if (res.status === 404) {
14 +
		return Astro.redirect("/404");
15 +
	} else if (!res.ok) {
16 +
		fetchError = `API returned ${res.status}`;
17 +
	} else {
18 +
		wine = await res.json();
19 +
	}
20 +
} catch (e) {
21 +
	fetchError = e instanceof Error ? e.message : "Failed to reach cellar API";
22 +
}
23 +
24 +
const meta = {
25 +
	title: wine?.name ?? "Wine",
26 +
	description: wine ? `${wine.origin} · ${wine.grape}` : "",
27 +
};
28 +
---
29 +
30 +
<PageLayout meta={meta}>
31 +
	<div class="flex flex-col gap-6 w-full pb-16">
32 +
33 +
		{fetchError ? (
34 +
			<p class="text-red-400 text-sm">Could not load wine: {fetchError}</p>
35 +
		) : wine && (
36 +
			<>
37 +
				<h1 class="text-2xl font-bold" style="letter-spacing:-0.5px">{wine.name}</h1>
38 +
39 +
				<div class="grid grid-cols-2 gap-6 items-center wine-detail-top">
40 +
					{wine.has_image && (
41 +
						<div class="w-full">
42 +
							<img
43 +
								src={`${CELLAR_API_URL}/wines/${wine.short_id}/image`}
44 +
								alt={wine.name}
45 +
								class="w-full object-cover rounded wine-image"
46 +
								loading="eager"
47 +
							/>
48 +
						</div>
49 +
					)}
50 +
					{!wine.wishlist && (
51 +
						<div class="flex flex-col items-center gap-4 p-3">
52 +
							<img
53 +
								src={`${CELLAR_API_URL}/api/wines/${wine.short_id}/pentagon.svg`}
54 +
								alt="Taste profile"
55 +
								width="250"
56 +
								height="250"
57 +
								loading="eager"
58 +
							/>
59 +
							<img
60 +
								src={`${CELLAR_API_URL}/api/wines/${wine.short_id}/bars.svg`}
61 +
								alt="Appearance and nose"
62 +
								width="250"
63 +
								loading="eager"
64 +
							/>
65 +
						</div>
66 +
					)}
67 +
				</div>
68 +
69 +
				<div class="flex flex-col gap-1">
70 +
					{wine.origin && (
71 +
						<div class="flex gap-3 text-sm">
72 +
							<span class="text-xs opacity-50">origin</span>
73 +
							<span>{wine.origin}</span>
74 +
						</div>
75 +
					)}
76 +
					{wine.grape && (
77 +
						<div class="flex gap-3 text-sm">
78 +
							<span class="text-xs opacity-50">grape</span>
79 +
							<span>{wine.grape}</span>
80 +
						</div>
81 +
					)}
82 +
				</div>
83 +
84 +
				{wine.notes && (
85 +
					<div class="flex flex-col gap-1">
86 +
						<span class="text-xs opacity-50">notes</span>
87 +
						<p class="whitespace-pre-wrap">{wine.notes}</p>
88 +
					</div>
89 +
				)}
90 +
91 +
				{wine.background && (
92 +
					<div class="flex flex-col gap-1">
93 +
						<span class="text-xs opacity-50">background</span>
94 +
						<p class="whitespace-pre-wrap">{wine.background}</p>
95 +
					</div>
96 +
				)}
97 +
			</>
98 +
		)}
99 +
	</div>
100 +
  		<a href="/cellar" class="text-sm">← cellar</a>
101 +
102 +
</PageLayout>
103 +
104 +
<style>
105 +
	@media (max-width: 480px) {
106 +
		.wine-detail-top {
107 +
			grid-template-columns: 1fr;
108 +
		}
109 +
		.wine-image {
110 +
			max-height: none;
111 +
			width: 100%;
112 +
		}
113 +
	}
114 +
</style>
packages/client/src/pages/cellar/index.astro (added) +43 −0
1 +
---
2 +
export const prerender = false;
3 +
4 +
import PageLayout from "@/layouts/Base.astro";
5 +
import WineCard from "@/components/page/WineCard.astro";
6 +
7 +
const meta = {
8 +
	title: "Cellar",
9 +
	description: "My wine tasting log",
10 +
};
11 +
12 +
const CELLAR_API_URL = import.meta.env.CELLAR_API_URL ?? "https://cellar.stevedylan.dev";
13 +
14 +
let wines: any[] = [];
15 +
let error: string | null = null;
16 +
try {
17 +
	const res = await fetch(`${CELLAR_API_URL}/api/wines`);
18 +
	if (res.ok) {
19 +
		wines = await res.json();
20 +
	} else {
21 +
		error = `API returned ${res.status}`;
22 +
	}
23 +
} catch (e) {
24 +
	error = e instanceof Error ? e.message : "Failed to reach cellar API";
25 +
}
26 +
---
27 +
28 +
<PageLayout meta={meta}>
29 +
	<div class="flex min-h-screen flex-col items-start justify-start gap-6">
30 +
		<h1 class="title">Cellar</h1>
31 +
		{error ? (
32 +
			<p class="text-red-400 text-sm">Could not load wines: {error}</p>
33 +
		) : wines.length === 0 ? (
34 +
			<p class="text-gray-400 text-sm">no wines yet</p>
35 +
		) : (
36 +
			<div class="flex flex-col w-full">
37 +
				{wines.map((wine: any) => (
38 +
					<WineCard wine={wine} cellarApiUrl={CELLAR_API_URL} />
39 +
				))}
40 +
			</div>
41 +
		)}
42 +
	</div>
43 +
</PageLayout>