From b97f3b42e1bad5753728315b5c7ebdacf6f81172 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Mon, 6 Jan 2025 23:48:56 -0800 Subject: initial commit --- app/components/DateWeatherLinks.tsx | 47 +++++++++++++++ app/components/QuickLinks.tsx | 49 ++++++++++++++++ app/components/WhoisChart.tsx | 111 ++++++++++++++++++++++++++++++++++++ app/globals.css | 35 ++++++++++++ app/layout.tsx | 26 +++++++++ app/page.tsx | 74 ++++++++++++++++++++++++ 6 files changed, 342 insertions(+) create mode 100644 app/components/DateWeatherLinks.tsx create mode 100644 app/components/QuickLinks.tsx create mode 100644 app/components/WhoisChart.tsx create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/page.tsx (limited to 'app') 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 ( +
+
+
+

+ {date.toLocaleDateString(undefined, { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + })} +

+

+ {date.toLocaleTimeString()} +

+
+
+

+ {data?.weather + ? Math.round(data.weather.temperature * (9 / 5) + 32) + "°F" + : "--"} +

+

+ {data?.weather ? data.weather.description : "--"} +

+
+
+
+ +
+ ); +} 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 ( +
+ {links.map((link) => { + const Icon = link.icon; + return ( + + + {link.name} + + ); + })} +
+ ); +} 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 = { + 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 ( +
+
+

+ {greet(data.whois ? data.whois[0].name : "Friend", new Date())} +

+
+ +
+ + + + new Date(tick).toLocaleTimeString()} + stroke="rgba(var(--foreground), 0.6)" + /> + (tick === 0 ? "" : `${tick} hrs`)} + /> + {uniqueNames.map((uniqueName, index) => ( + name === uniqueName)} + name={uniqueName} + stroke={colors[uniqueName] ?? "#ff0000"} + strokeWidth={3} + dot={false} + isAnimationActive={false} + /> + ))} + + +
+
+ {Object.keys(groupedData).map((name) => ( + + {name} + + ))} +
+
+ ); +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..172ed1e --- /dev/null +++ b/app/globals.css @@ -0,0 +1,35 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --background: 255, 255, 255; + --background-blend: rgba(234, 144, 206, 0.5); + --foreground: 0, 0, 0; + --accent: 255, 192, 203; + --accent-foreground: 33, 33, 33; +} + +.dark { + --background: 18, 18, 18; + --background-blend: rgba(127, 1, 131, 0.483); + --foreground: 255, 255, 255; + --accent: 255, 182, 193; + --accent-foreground: 240, 240, 240; +} + +body { + color: rgb(var(--foreground)); + background-image: url("https://static.simponic.xyz/static/penguins.png"); + background-repeat: repeat; + background-size: 900px; + background-color: var(--background-blend); + background-blend-mode: multiply; + backdrop-filter: blur(3px); +} + +.glass { + background: rgba(var(--background), 0.7); + backdrop-filter: blur(10px); + border: 1px solid rgba(var(--foreground), 0.1); +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..bb57e86 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,26 @@ +import "./globals.css"; +import { Inter } from "next/font/google"; +import { ThemeProvider } from "next-themes"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata = { + title: "Penguin New Tab", + description: "A beautiful penguin-themed new tab page", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + {children} + + + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..8584df0 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useTheme } from "next-themes"; +import DateWeatherLinks from "./components/DateWeatherLinks"; +import WhoisChart from "./components/WhoisChart"; +import { + Data, + DataContext, + fetchWeather, + fetchWhois, +} from "@/lib/data-context"; + +export default function Home() { + // god i hate next. + const [mounted, setMounted] = useState(false); + const { theme, setTheme } = useTheme(); + const [data, setData] = useState({}); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + const update = () => + fetchWeather().then((weather) => + setData((data) => data && { ...data, weather }) + ); + update(); + const interval = setInterval(update, 5 * 60_000); // 5 mins + return () => clearInterval(interval); + }, []); + + useEffect(() => { + const update = () => + fetchWhois().then((whois) => + setData((data) => data && { ...data, whois }) + ); + update(); + const interval = setInterval(update, 5 * 1_000); // 5 seconds + return () => clearInterval(interval); + }, []); + + if (!mounted) return null; + + return ( + +
+
+
+

+ 🐧 Penguin New Tab +

+ +
+
+ + +
+ +
+
+
+ ); +} -- cgit v1.2.3-70-g09d2