Custom app launcher, browser home page, and service monitor. cute.haus
homelab react typescript
0

Configure Feed

Select the types of activity you want to include in your feed.

add lobsters and move around searchbar/weather

Aly Raffauf (Jan 29, 2026, 8:54 PM EST) b58ef023 2a46da5b

+76 -6
_img/screenshot.png

This is a binary file and will not be displayed.

+13 -5
src/App.tsx
··· 1 1 import Layout from "./Layout"; 2 2 import ServiceGrid from "./components/ServiceGrid"; 3 3 import { HackerNews } from "./components/HackerNews"; 4 + import { Lobsters } from "./components/Lobsters"; 4 5 import { Weather } from "./components/Weather"; 5 6 import { SearchBar } from "./components/SearchBar.tsx"; 6 7 import { apps } from "./data/apps"; ··· 10 11 export function App() { 11 12 return ( 12 13 <Layout> 13 - <div className="mb-8"> 14 - <SearchBar /> 14 + <div className="mb-8 grid gap-4 md:grid-cols-4"> 15 + <div> 16 + <Weather /> 17 + </div> 18 + 19 + <div className="col-span-3"> 20 + <SearchBar /> 21 + </div> 15 22 </div> 16 23 17 24 <div className="grid gap-4 md:grid-cols-2"> 18 25 <div> 19 - {" "} 20 - <h2 className="text-2xl font-semibold text-white/70 mb-4">Weather</h2> 21 - <Weather /> 26 + <h2 className="text-2xl font-semibold text-white/70 mb-4"> 27 + Lobsters 28 + </h2> 29 + <Lobsters /> 22 30 </div> 23 31 24 32 <div>
+42
src/components/Lobsters.tsx
··· 1 + import { useState, useEffect } from "react"; 2 + 3 + export function Lobsters() { 4 + const [stories, setStories] = useState<any[]>([]); 5 + 6 + const [error, setError] = useState<string | null>(null); 7 + 8 + useEffect(() => { 9 + const fetchLobsters = () => { 10 + fetch(`/api/lobsters`) 11 + .then((response) => response.json()) 12 + .then((data) => setStories(data)); 13 + }; 14 + 15 + fetchLobsters(); 16 + }, []); 17 + 18 + if (stories.length == 0) { 19 + return <div>Loading stories...</div>; 20 + } 21 + 22 + return ( 23 + <div className="p-4 rounded-xl bg-white/5 border border-rose-400/20"> 24 + <ul className="space-y-2"> 25 + {stories.map((story) => ( 26 + <li key={story.short_id} className="flex gap-3 text-sm"> 27 + <span className="text-zinc-500 w-8 shrink-0">{story.score}↑</span> 28 + 29 + <a 30 + href={story.url} 31 + target="_blank" 32 + rel="noopener noreferrer" 33 + className="text-zinc-200 hover:text-white hover:underline truncate" 34 + > 35 + {story.title} 36 + </a> 37 + </li> 38 + ))} 39 + </ul> 40 + </div> 41 + ); 42 + }
+1 -1
src/components/SearchBar.tsx
··· 14 14 15 15 return ( 16 16 <form 17 - className="p-4 rounded-xl bg-white/5 border border-rose-400/20" 17 + className="h-full flex-1 p-4 rounded-xl bg-white/5 border border-rose-400/20 flex flex-col justify-center" 18 18 onSubmit={(e) => { 19 19 e.preventDefault(); 20 20 if (query.trim()) {
+20
src/index.ts
··· 4 4 import { websites } from "./data/websites"; 5 5 6 6 let hnCache: { stories: any[]; fetchedAt: number } | null = null; 7 + let lobstersCache: { stories: any[]; fetchedAt: number } | null = null; 7 8 8 9 async function checkStatuses(items: { name: string; url: string }[]) { 9 10 const results = await Promise.all( ··· 62 63 ); 63 64 64 65 hnCache = { stories, fetchedAt: Date.now() }; 66 + 67 + return Response.json(stories); 68 + }, 69 + }, 70 + 71 + "/api/lobsters": { 72 + async GET(req) { 73 + if ( 74 + lobstersCache && 75 + Date.now() - lobstersCache.fetchedAt < 5 * 60 * 1000 76 + ) { 77 + return Response.json(lobstersCache.stories); 78 + } 79 + 80 + const stories = await fetch("https://lobste.rs/hottest.json") 81 + .then((res) => res.json()) 82 + .then((data) => data.slice(0, 5)); 83 + 84 + lobstersCache = { stories, fetchedAt: Date.now() }; 65 85 66 86 return Response.json(stories); 67 87 },