September 18
25 °C
Cloudy
Max. 26 Min. 22
today
- 18:00
Grocery Shopping
tomorrow
- 07:30
Yoga Class
- 20:00
Dinner with Friends
1import { useMemo } from "react";2
3import { WidgetArea } from "@/components/ui/pxl/widget-area";4import { DayView } from "@/components/widgets/pxl/day-view";5import type { OpenMeteo } from "@/lib/schemas/pxl/openmeteo";6
7export default function DayViewPreview() {8 const hourlyTimeRange = useMemo(() => {9 // sv-SE returns date in correct ISO format (YYYY-MM-DD)10 const isoDate = new Date().toLocaleDateString("sv-SE");11
12 return Array(24)13 .fill(null)14 .map((_, idx) => `${isoDate}T${idx.toString().padStart(2, "0")}:00`);15 }, []);16
17 const events = useMemo(() => {18 const ref = new Date();19 const today = ref.toLocaleDateString("sv-SE");20 const tomorrowRef = new Date(ref);21 tomorrowRef.setDate(ref.getDate() + 1);22 const tomorrow = tomorrowRef.toLocaleDateString("sv-SE");23
24 return [25 {26 id: "3",27 summary: "Grocery Shopping",28 start: {29 date: today,30 dateTime: `${today}T18:00`,31 },32 },33 {34 id: "4",35 summary: "Yoga Class",36 start: {37 date: tomorrow,38 dateTime: `${tomorrow}T07:30`,39 },40 },41 {42 id: "6",43 summary: "Dinner with Friends",44 start: {45 date: tomorrow,46 dateTime: `${tomorrow}T20:00`,47 },48 },49 ];50 }, []);51
52 return (53 <WidgetArea size="md">54 <DayView55 status="success"56 className="size-full"57 forecast={58 {59 hourly_units: {60 temperature_2m: "°C",61 },62 hourly: {63 time: hourlyTimeRange,64 temperature_2m: [65 23.6, 23.3, 23, 23, 23, 22.5, 22.4, 22.4, 22.4, 23.3, 24.6,66 25.6, 25.4, 25.4, 25.4, 25.3, 25.3, 25.5, 25.1, 25.4, 25.3,67 24.5, 25, 24.7,68 ],69 weather_code: [70 0, 0, 3, 3, 3, 0, 0, 3, 2, 0, 0, 0, 0, 1, 3, 3, 3, 1, 3, 3, 3,71 3, 51, 3,72 ],73 is_day: [74 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,75 1, 0, 0,76 ],77 },78 daily: {79 temperature_2m_max: [25.6],80 temperature_2m_min: [22.4],81 },82 } as OpenMeteo.Forecast83 }84 events={events}85 />86 </WidgetArea>87 );88}Installation
Section titled “Installation”pnpm dlx shadcn@latest add pxl-ui/registry/day-view
Install the following dependencies:
pnpm add weather-i18n
Copy and paste the following code into your project.
1import {2 type ComponentProps,3 useCallback,4 useEffect,5 useMemo,6 useState,7} from "react";8import type { DayPhase, Language, WMO4677Code } from "weather-i18n/wmo_4677";9import getWeatherCodeI18n from "weather-i18n/wmo_4677/i18n";10
11import AnimatedIcon from "@/components/ui/pxl/animated-icon";12import { Badge } from "@/components/ui/pxl/badge";13import { Button } from "@/components/ui/pxl/button";14import {15 Card,16 CardContent,17 CardHeader,18 CardTitle,19} from "@/components/ui/pxl/card";20import { ScrollArea } from "@/components/ui/pxl/scroll-area";21import { WeatherIcon } from "@/components/ui/pxl/weather-icon";22import type { GCalendar } from "@/lib/schemas/pxl/gcalendar";23import type { OpenMeteo } from "@/lib/schemas/pxl/openmeteo";24import { cn } from "@/lib/utils";25
26function Weather({27 forecast,28 language = "en",29 texts,30}: {31 forecast?: OpenMeteo.Forecast;32 language?: Language;33 texts: {34 maxLabel: string;35 minLabel: string;36 };37}) {38 const title = useMemo(() => {39 return new Intl.DateTimeFormat(language, {40 day: "numeric",41 month: "long",42 }).format(new Date());43 }, [language]);44
45 const [now, setNow] = useState(Date.now());46
47 useEffect(function tick() {48 const interval = setInterval(() => {49 setNow(Date.now());50 }, 60_000); // each minute51
52 return () => clearInterval(interval);53 }, []);54
55 const index = useMemo(56 function evaluateIndex() {57 if (!forecast) {58 return 0;59 }60
61 const date = new Date(now);62 const currentTime =63 [64 date.getFullYear(),65 String(date.getMonth() + 1).padStart(2, "0"),66 String(date.getDate()).padStart(2, "0"),67 ].join("-") +68 "T" +69 String(date.getHours()).padStart(2, "0") +70 ":00";71
72 return forecast.hourly.time.indexOf(currentTime);73 },74 [forecast, now],75 );76
77 const weather = useMemo(() => {78 if (!forecast) {79 return null;80 }81
82 const code = forecast.hourly.weather_code[index] as WMO4677Code;83 const dayPhase: DayPhase = forecast.hourly.is_day[index] ? "day" : "night";84
85 const unit = forecast.hourly_units.temperature_2m ?? "°C";86 const current = forecast.hourly.temperature_2m[index]87 ? `${Math.round(forecast.hourly.temperature_2m[index])} ${unit}`88 : undefined;89 const summary = code90 ? getWeatherCodeI18n(language)(code, dayPhase)91 : undefined;92 const max = forecast.daily.temperature_2m_max[0]93 ? `${texts.maxLabel} ${Math.round(forecast.daily.temperature_2m_max[0])}`94 : undefined;95 const min = forecast.daily.temperature_2m_min[0]96 ? `${texts.minLabel} ${Math.round(forecast.daily.temperature_2m_min[0])}`97 : undefined;98
99 return {100 code,101 dayPhase,102 current,103 summary,104 max,105 min,106 };107 }, [index, forecast, texts, language]);108
109 return (110 <div className="flex flex-col justify-between">111 {weather && (112 <div className="absolute inset-0 flex items-center justify-start ml-[13%]">113 <WeatherIcon114 className="size-24 fill-foreground/20"115 code={weather.code}116 dayPhase={weather.dayPhase}117 />118 </div>119 )}120 <div>121 <h2 className="font-sans text-lg leading-3">{title}</h2>122 </div>123
124 <div className="flex flex-col">125 <div className="flex flex-row gap-1 items-end">126 <span className="font-heading text-xl leading-6">127 {weather?.current}128 </span>129 </div>130 <div className="flex flex-col">131 <h3 className="leading-4 text-xs">{weather?.summary}</h3>132 <p className="leading-4 text-xs">133 {weather?.max} {weather?.min}134 </p>135 </div>136 </div>137 </div>138 );139}140
141function Schedule({142 events,143 language,144}: {145 events?: GCalendar.Event[];146 language: Language;147}) {148 const formatGroupLabel = useCallback(149 function formatGroupLabel(dateString: string) {150 const date = new Date(dateString);151
152 if (Number.isNaN(date)) {153 return dateString;154 }155
156 const now = new Date();157
158 const MS_PER_DAY = 1000 * 60 * 60 * 24;159 const dateUtc = Date.UTC(160 date.getFullYear(),161 date.getMonth(),162 date.getDate(),163 );164 const nowUtc = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());165 const diff = Math.round((dateUtc - nowUtc) / MS_PER_DAY);166
167 const rtfFormatter = new Intl.RelativeTimeFormat(language, {168 numeric: "auto",169 });170
171 if (diff >= -1 && diff <= 1) {172 return rtfFormatter.format(diff, "day");173 }174
175 const weekdayFormatter = new Intl.DateTimeFormat(language, {176 weekday: "long",177 });178 const dateFormatter = new Intl.DateTimeFormat(language, {179 day: "numeric",180 month: "long",181 });182
183 const nowWeekday = now.getDay();184 const startOfWeek = new Date(now);185 startOfWeek.setDate(now.getDate() - nowWeekday);186
187 const endOfWeek = new Date(startOfWeek);188 endOfWeek.setDate(startOfWeek.getDate() + 7);189
190 const weekDay = weekdayFormatter.format(date);191 const monthDate = dateFormatter.format(date);192
193 return `${weekDay}, ${monthDate}`;194 },195 [language],196 );197
198 const eventGroups = useMemo(() => {199 if (!events) {200 return [];201 }202
203 const map = new Map<204 string,205 {206 id?: string | null;207 summary?: string | null;208 start?: string | null;209 sortKey?: string | null;210 }[]211 >();212
213 for (const event of events) {214 const start =215 event.start?.dateTime ??216 event.start?.date ??217 event.end?.dateTime ??218 event.end?.date;219
220 if (!start) {221 continue;222 }223
224 try {225 const date = new Date(start);226 const key = date.toISOString().slice(0, 10);227
228 if (!map.has(key)) {229 map.set(key, []);230 }231
232 map.get(key)?.push({233 id: event.id,234 summary: event.summary,235 start: event.start?.dateTime236 ? `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`237 : undefined,238 sortKey: event.start?.dateTime,239 });240 } catch {}241 }242
243 const result = Array.from(map.entries())244 .map(([key, items]) => ({245 key,246 label: formatGroupLabel(key),247 items: items.sort((aI, bI) => {248 const a = aI.sortKey;249 const b = bI.sortKey;250
251 if (a && b) {252 return a.toString().localeCompare(b.toString());253 }254
255 if (!a) {256 return -1;257 }258
259 if (!b) {260 return 1;261 }262
263 return 0;264 }),265 }))266 .sort((aG, bG) => {267 const a = aG.key;268 const b = bG.key;269
270 if (a && b) {271 return a.toString().localeCompare(b.toString());272 }273
274 if (a) {275 return -1;276 }277
278 if (b) {279 return 1;280 }281
282 return 0;283 });284
285 return result;286 }, [formatGroupLabel, events]);287
288 return (289 <ScrollArea className="flex-1 min-h-0 min-w-0">290 <div className="flex flex-col gap-2 mr-2">291 {eventGroups.map((group) => (292 <div className="flex flex-col" key={group.key}>293 <Badge className="w-fit mb-1" border="notch" size="sm">294 {group.label}295 </Badge>296 <ul className="flex flex-col gap-0.5">297 {group.items.map((item, idx) => (298 <li299 key={item.id ?? idx.toString()}300 className="flex items-center leading-4"301 title={item.summary ?? ""}302 >303 {item.start && (304 <span className="text-muted-foreground mr-2 font-mono text-2xs">305 {item.start}306 </span>307 )}308 <p className="truncate text-2xs leading-4">{item.summary}</p>309 </li>310 ))}311 </ul>312 </div>313 ))}314 </div>315 </ScrollArea>316 );317}318
319export function DayView({320 className,321 errorMessage,322 events,323 forecast,324 language = "en",325 status = "pending",326 texts = {327 errorTitle: "Error",328 emptyState: "No data available",329 loadingTitle: "Loading...",330 maxLabel: "Max.",331 minLabel: "Min.",332 retry: "Retry",333 },334 onRetry,335}: {336 className?: string;337 errorMessage?: string;338 language?: "en" | "es";339 showEmptyState?: boolean;340 status?: "pending" | "success" | "error";341 events?: ComponentProps<typeof Schedule>["events"];342 forecast?: ComponentProps<typeof Weather>["forecast"];343 onRetry?: () => void;344 texts?: {345 emptyState: string;346 errorTitle: string;347 maxLabel: string;348 minLabel: string;349 loadingTitle: string;350 retry: string;351 };352}) {353 return (354 <Card size="lg" className={cn(className)}>355 {status !== "success" && (356 <CardHeader>357 <CardTitle>358 {status === "pending" && (359 <>360 <AnimatedIcon361 icon={() => (362 <svg363 xmlns="http://www.w3.org/2000/svg"364 fill="currentColor"365 viewBox="0 0 24 24"366 >367 <path d="M14 23H10V19H14V23ZM7 21H3L3 17H7V21ZM21 20H18V17H21V20ZM6 9V14H1L1 9H6ZM23 13H20V10H23V13ZM3 11V12H4V11H3ZM13 7H7L7 1L13 1V7ZM20 6H18V4L20 4V6ZM9 5H11V3H9V5Z"></path>368 </svg>369 )}370 animation="spin"371 data-slot="icon"372 />373 {texts.loadingTitle}374 </>375 )}376 {status === "error" && (377 <>378 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">379 <path d="M14 22h-4v-2h4v2Zm-4-2H8v-2h2v2Zm6 0h-2v-2h2v2Zm-8-2H6v-2h2v2Zm10 0h-2v-2h2v2Zm-5-1h-2v-2h2v2Zm-7-1H4v-2h2v2Zm14 0h-2v-2h2v2ZM4 14H2v-4h2v4Zm18 0h-2v-4h2v4Zm-9-7v6h-2V7h2Zm-7 3H4V8h2v2Zm14 0h-2V8h2v2ZM8 8H6V6h2v2Zm10 0h-2V6h2v2Zm-8-2H8V4h2v2Zm6 0h-2V4h2v2Zm-2-2h-4V2h4v2Z"></path>380 </svg>381 {texts.errorTitle}382 </>383 )}384 </CardTitle>385 </CardHeader>386 )}387 <CardContent>388 {status === "error" && (389 <div className="h-30 flex flex-col gap-1 items-center justify-center px-4 text-center">390 <p className="text-sm text-muted-foreground">{errorMessage}</p>391 <Button size="xs" onClick={onRetry}>392 <svg393 xmlns="http://www.w3.org/2000/svg"394 fill="currentColor"395 viewBox="0 0 24 24"396 >397 <path d="M13 20H9V18H13V20ZM19 16H21V18H19V20H17V18H15V16H17V8H19V16ZM9 18H7V16H9V18ZM7 6H9V8H7V16H5V8H3V6H5V4H7V6ZM15 16H13V14H15V16ZM23 16H21V14H23V16ZM3 10H1V8H3V10ZM11 10H9V8H11V10ZM17 8H15V6H17V8ZM15 6H11V4H15V6Z"></path>398 </svg>399 {texts.retry}400 </Button>401 </div>402 )}403
404 {status === "success" && (405 <div className="grid grid-cols-2 gap-(--card-spacing) h-full">406 <Weather language={language} texts={texts} forecast={forecast} />407
408 <Schedule language={language} events={events} />409 </div>410 )}411 </CardContent>412 </Card>413 );414}Update the import paths to match your project setup.
import { DayView } from "@/components/ui/pxl/day-view"<DayView status={status} forecast={forecast} events={events} />States
Section titled “States”Pending
Section titled “Pending”The request is loading
Loading...
1import { WidgetArea } from "@/components/ui/pxl/widget-area";2import { DayView } from "@/components/widgets/pxl/day-view";3
4export default function DayViewPendingExample() {5 return (6 <WidgetArea size="md">7 <DayView status="pending" className="size-full" />8 </WidgetArea>9 );10}The request failed
Error
Something Failed
1import { WidgetArea } from "@/components/ui/pxl/widget-area";2import { DayView } from "@/components/widgets/pxl/day-view";3
4export default function DayViewErrorExample() {5 return (6 <WidgetArea size="md">7 <DayView8 status="error"9 errorMessage="Something Failed"10 className="size-full"11 />12 </WidgetArea>13 );14}Success
Section titled “Success”Data is available
September 18
25 °C
Cloudy
Max. 26 Min. 22
today
- 09:00
Team Standup
- 14:30
Dentist Appointment
- 18:00
Grocery Shopping
tomorrow
Project Deadline
- 07:30
Yoga Class
- 20:00
Dinner with Friends
Sunday, September 20
- 10:00
Car Maintenance
Friday, September 25
- 19:00
Book Club Meeting
1import { useMemo } from "react";2
3import { WidgetArea } from "@/components/ui/pxl/widget-area";4import { DayView } from "@/components/widgets/pxl/day-view";5import type { OpenMeteo } from "@/lib/schemas/pxl/openmeteo";6
7export default function DayViewSuccessExample() {8 const hourlyTimeRange = useMemo(() => {9 // sv-SE returns date in correct ISO format (YYYY-MM-DD)10 const isoDate = new Date().toLocaleDateString("sv-SE");11
12 return Array(24)13 .fill(null)14 .map((_, idx) => `${isoDate}T${idx.toString().padStart(2, "0")}:00`);15 }, []);16
17 const events = useMemo(() => {18 const ref = new Date();19 const today = ref.toLocaleDateString("sv-SE");20 const tomorrowRef = new Date(ref);21 tomorrowRef.setDate(ref.getDate() + 1);22 const tomorrow = tomorrowRef.toLocaleDateString("sv-SE");23 const inTwoDaysRef = new Date(ref);24 inTwoDaysRef.setDate(ref.getDate() + 2);25 const inTwoDays = inTwoDaysRef.toLocaleDateString("sv-SE");26 const inSevenDaysRef = new Date(ref);27 inSevenDaysRef.setDate(ref.getDate() + 7);28 const inSevenDays = inSevenDaysRef.toLocaleDateString("sv-SE");29
30 return [31 {32 id: "1",33 summary: "Team Standup",34 start: {35 date: today,36 dateTime: `${today}T09:00`,37 },38 },39 {40 id: "3",41 summary: "Grocery Shopping",42 start: {43 date: today,44 dateTime: `${today}T18:00`,45 },46 },47 {48 id: "2",49 summary: "Dentist Appointment",50 start: {51 date: today,52 dateTime: `${today}T14:30`,53 },54 },55 {56 id: "7",57 summary: "Car Maintenance",58 start: {59 date: inTwoDays,60 dateTime: `${inTwoDays}T10:00`,61 },62 },63 {64 id: "4",65 summary: "Yoga Class",66 start: {67 date: tomorrow,68 dateTime: `${tomorrow}T07:30`,69 },70 },71 {72 id: "5",73 summary: "Project Deadline",74 start: {75 date: tomorrow,76 },77 },78 {79 id: "6",80 summary: "Dinner with Friends",81 start: {82 date: tomorrow,83 dateTime: `${tomorrow}T20:00`,84 },85 },86 {87 id: "8",88 summary: "Book Club Meeting",89 start: {90 date: inSevenDays,91 dateTime: `${inSevenDays}T19:00`,92 },93 },94 ];95 }, []);96
97 return (98 <WidgetArea size="md">99 <DayView100 status="success"101 className="size-full"102 forecast={103 {104 hourly_units: {105 temperature_2m: "°C",106 },107 hourly: {108 time: hourlyTimeRange,109 temperature_2m: [110 23.6, 23.3, 23, 23, 23, 22.5, 22.4, 22.4, 22.4, 23.3, 24.6,111 25.6, 25.4, 25.4, 25.4, 25.3, 25.3, 25.5, 25.1, 25.4, 25.3,112 24.5, 25, 24.7,113 ],114 weather_code: [115 0, 0, 3, 3, 3, 0, 0, 3, 2, 0, 0, 0, 0, 1, 3, 3, 3, 1, 3, 3, 3,116 3, 51, 3,117 ],118 is_day: [119 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,120 1, 0, 0,121 ],122 },123 daily: {124 temperature_2m_max: [25.6],125 temperature_2m_min: [22.4],126 },127 } as OpenMeteo.Forecast128 }129 events={events}130 />131 </WidgetArea>132 );133}