summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorElizabeth Hunt <elizabeth.hunt@simponic.xyz>2025-01-06 23:48:56 -0800
committerElizabeth Hunt <elizabeth.hunt@simponic.xyz>2025-01-06 23:48:56 -0800
commitb97f3b42e1bad5753728315b5c7ebdacf6f81172 (patch)
treea07cbc723346503792a70ca7c923a8838e64fdff /app
downloadpenguin-new-tab-b97f3b42e1bad5753728315b5c7ebdacf6f81172.tar.gz
penguin-new-tab-b97f3b42e1bad5753728315b5c7ebdacf6f81172.zip
initial commit
Diffstat (limited to 'app')
-rw-r--r--app/components/DateWeatherLinks.tsx47
-rw-r--r--app/components/QuickLinks.tsx49
-rw-r--r--app/components/WhoisChart.tsx111
-rw-r--r--app/globals.css35
-rw-r--r--app/layout.tsx26
-rw-r--r--app/page.tsx74
6 files changed, 342 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>
+ );
+}
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 (
+ <html lang="en">
+ <body className={inter.className}>
+ <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
+ {children}
+ </ThemeProvider>
+ </body>
+ </html>
+ );
+}
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<boolean>(false);
+ const { theme, setTheme } = useTheme();
+ const [data, setData] = useState<Data>({});
+
+ 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 (
+ <DataContext.Provider value={data}>
+ <div className="min-h-screen p-8">
+ <div className="max-w-4xl mx-auto">
+ <header className="glass p-6 flex justify-between items-center mb-8">
+ <h1 className="text-4xl font-bold text-gray-800 dark:text-gray-100">
+ 🐧 Penguin New Tab
+ </h1>
+ <button
+ onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
+ className="p-2 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200"
+ >
+ {theme === "dark" ? "🌙" : "☀️"}
+ </button>
+ </header>
+ <main className="space-y-8">
+ <DateWeatherLinks />
+ <WhoisChart />
+ </main>
+ <footer className="mt-8 text-center">
+ <p className="text-sm text-gray-600 dark:text-gray-400">
+ Made with 💖 by Penguin Lovers
+ </p>
+ </footer>
+ </div>
+ </div>
+ </DataContext.Provider>
+ );
+}