diff options
author | Elizabeth Hunt <elizabeth.hunt@simponic.xyz> | 2025-01-06 23:48:56 -0800 |
---|---|---|
committer | Elizabeth Hunt <elizabeth.hunt@simponic.xyz> | 2025-01-06 23:48:56 -0800 |
commit | b97f3b42e1bad5753728315b5c7ebdacf6f81172 (patch) | |
tree | a07cbc723346503792a70ca7c923a8838e64fdff /app/components | |
download | penguin-new-tab-b97f3b42e1bad5753728315b5c7ebdacf6f81172.tar.gz penguin-new-tab-b97f3b42e1bad5753728315b5c7ebdacf6f81172.zip |
initial commit
Diffstat (limited to 'app/components')
-rw-r--r-- | app/components/DateWeatherLinks.tsx | 47 | ||||
-rw-r--r-- | app/components/QuickLinks.tsx | 49 | ||||
-rw-r--r-- | app/components/WhoisChart.tsx | 111 |
3 files changed, 207 insertions, 0 deletions
diff --git a/app/components/DateWeatherLinks.tsx b/app/components/DateWeatherLinks.tsx new file mode 100644 index 0000000..1c004fa --- /dev/null +++ b/app/components/DateWeatherLinks.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { useState, useEffect, useContext } from "react"; +import QuickLinks from "./QuickLinks"; +import { DataContext } from "@/lib/data-context"; + +export default function DateWeatherLinksWelcome() { + const [date, setDate] = useState(new Date()); + const data = useContext(DataContext); + + useEffect(() => { + const timer = setInterval(() => setDate(new Date()), 1000); + return () => clearInterval(timer); + }, []); + + return ( + <div className="glass rounded-lg p-6 shadow-lg"> + <div className="flex justify-between items-center"> + <div> + <h2 className="text-3xl font-bold text-gray-800 dark:text-gray-100"> + {date.toLocaleDateString(undefined, { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + })} + </h2> + <p className="text-xl text-gray-600 dark:text-gray-300"> + {date.toLocaleTimeString()} + </p> + </div> + <div className="text-right"> + <p className="text-4xl font-bold text-gray-800 dark:text-gray-100"> + {data?.weather + ? Math.round(data.weather.temperature * (9 / 5) + 32) + "°F" + : "--"} + </p> + <p className="text-xl text-gray-600 dark:text-gray-300"> + {data?.weather ? data.weather.description : "--"} + </p> + </div> + </div> + <br /> + <QuickLinks /> + </div> + ); +} diff --git a/app/components/QuickLinks.tsx b/app/components/QuickLinks.tsx new file mode 100644 index 0000000..8a853cf --- /dev/null +++ b/app/components/QuickLinks.tsx @@ -0,0 +1,49 @@ +import { + Skull, + Search, + GitPullRequest, + Mail, + Fish, + ChartArea, +} from "lucide-react"; + +export default function QuickLinks() { + const links = [ + { name: "Google", url: "https://www.google.com", icon: Search }, + { name: "Gitea", url: "https://git.simponic.xyz", icon: GitPullRequest }, + { + name: "Mail", + url: "https://roundcube.internal.simponic.xyz", + icon: Mail, + }, + { + name: "Jellyfin", + url: "https://jellyfin.internal.simponic.xyz", + icon: Fish, + }, + { + name: "Uptime Kuma", + url: "https://uptime.internal.simponic.xyz", + icon: ChartArea, + }, + ]; + + return ( + <div className="rounded-lg p-6 justify-center flex gap-16"> + {links.map((link) => { + const Icon = link.icon; + return ( + <a + key={link.name} + href={link.url} + className="text-rose-700 hover:text-rose-800 transition-colors" + title={link.name} + > + <Icon className="w-8 h-8" /> + <span className="sr-only">{link.name}</span> + </a> + ); + })} + </div> + ); +} diff --git a/app/components/WhoisChart.tsx b/app/components/WhoisChart.tsx new file mode 100644 index 0000000..43f5eb9 --- /dev/null +++ b/app/components/WhoisChart.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { useContext } from "react"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + ResponsiveContainer, +} from "recharts"; +import { DataContext } from "@/lib/data-context"; +import { greet } from "@/lib/utils"; + +const colors: Record<string, string> = { + lizzy: "#d985e6", + alex: "#66d1c3", +}; + +export default function UpdateChart() { + const data = useContext(DataContext); + if (!data.whois) return null; + + const groupedData: { [name: string]: { time: number; value: number }[] } = {}; + data.whois.forEach((update, i, arr) => { + if (!(update.name in groupedData)) groupedData[update.name] = []; + const msPerHour = 60 * 60 * 1000; + + if (i === arr.length - 1) { + const now = new Date(); + groupedData[update.name].push({ time: update.time, value: 0 }); + groupedData[update.name].push({ + time: now.getTime(), + value: (now.getTime() - update.time) / msPerHour, + }); + } + + if (i === 0) return; + const prev = arr[i - 1]; + + groupedData[prev.name].push({ time: prev.time, value: 0 }); + groupedData[prev.name].push({ + time: update.time, + value: (update.time - prev.time) / msPerHour, + }); + groupedData[prev.name].push({ time: update.time, value: 0 }); + }); + + const uniqueNames = Object.keys(groupedData); + const chartData = Object.entries(groupedData) + .flatMap(([name, dataPoints]) => + dataPoints.map((data) => ({ time: data.time, name, [name]: data.value })) + ) + .sort((a, b) => a.time - b.time); + + return ( + <div className="glass rounded-lg p-6 shadow-lg"> + <div className="flex justify-center items-center mb-8"> + <h2 className="text-4xl font-bold"> + {greet(data.whois ? data.whois[0].name : "Friend", new Date())} + </h2> + </div> + + <div className="h-64"> + <ResponsiveContainer width="100%" height="100%"> + <LineChart data={chartData}> + <CartesianGrid + strokeDasharray="3 3" + stroke="rgba(var(--foreground), 0.1)" + /> + <XAxis + dataKey="time" + type="number" + domain={["dataMin", "dataMax"]} + tickFormatter={(tick) => new Date(tick).toLocaleTimeString()} + stroke="rgba(var(--foreground), 0.6)" + /> + <YAxis + stroke="rgba(var(--foreground), 0.6)" + tickFormatter={(tick) => (tick === 0 ? "" : `${tick} hrs`)} + /> + {uniqueNames.map((uniqueName, index) => ( + <Line + key={uniqueName} + type="linear" + dataKey={uniqueName} + data={chartData.filter(({ name }) => name === uniqueName)} + name={uniqueName} + stroke={colors[uniqueName] ?? "#ff0000"} + strokeWidth={3} + dot={false} + isAnimationActive={false} + /> + ))} + </LineChart> + </ResponsiveContainer> + </div> + <div className="mt-4 flex flex-wrap gap-2"> + {Object.keys(groupedData).map((name) => ( + <span + key={name} + className="px-3 py-1 rounded-full text-sm bg-pink-100 text-pink-800 dark:bg-gray-700 dark:text-pink-200" + style={{ color: colors[name] }} + > + {name} + </span> + ))} + </div> + </div> + ); +} |