port to Astro

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6e1c163a147b14b92a5f6ae4de6a87206a6a6964
This commit is contained in:
raf 2026-04-05 23:24:13 +03:00
commit f25cfa3e7e
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
14 changed files with 5125 additions and 200 deletions

Binary file not shown.

View file

@ -0,0 +1,33 @@
---
import { Icon } from 'astro-icon/components';
interface Props {
members: string[];
}
const { members } = Astro.props;
const owner = "amr";
const admins = ["raf", "rocoe"];
---
<div class="section">
<p>Members</p>
<ul class="members">
{members.length > 0 ? (
members.map(username => (
<li>
<a href={`/~${username}`}>~{username}</a>
{username === owner && (
<Icon name="fa-solid:crown" class="member-icon owner" title="Owner" />
)}
{admins.includes(username) && username !== owner && (
<Icon name="fa-solid:shield-halved" class="member-icon admin" title="Admin" />
)}
</li>
))
) : (
<li class="error">Error fetching members!</li>
)}
</ul>
</div>

View file

@ -0,0 +1,32 @@
---
interface Service {
url: string;
name: string;
desc: string;
requiresMember: boolean;
special?: string;
}
interface Props {
services: Service[];
}
const { services } = Astro.props;
---
<div class="section">
<p>Services</p>
<table class="services">
{services.map(service => (
<tr>
<td>{service.url ? <a href={service.url}>{service.special || service.url}</a> : service.special}</td>
<td>{service.name}</td>
<td>{service.desc}</td>
</tr>
))}
</table>
<small>
<sup>1</sup> Only available for existing members<br>
<sup>2</sup> Not running 24/7
</small>
</div>

View file

@ -0,0 +1,21 @@
---
interface Props {
currentDate: string;
}
const { currentDate } = Astro.props;
---
<div class="section">
<p>System Info</p>
<table class="system-info">
<tr>
<td>time:</td>
<td>{currentDate}</td>
</tr>
<tr>
<td>os:</td>
<td>Ubuntu GNU/Linux 24.04 (LTS)</td>
</tr>
</table>
</div>

View file

@ -0,0 +1,12 @@
---
---
<div class="section tor-notice">
<pre>
+----------------------------------------------------------------+
| This site is accessible via Tor! |
| <a href="http://frzndev32nhnla77oozhxhz5yzo4abldr6zbc4qkdh5hcyanizlxs2ad.onion/">frzndev32nhnla77oozhxhz5yzo4abldr6zbc4qkdh5hcyanizlxs2ad.onion</a> |
+----------------------------------------------------------------+
</pre>
</div>

45
src/layouts/Layout.astro Normal file
View file

@ -0,0 +1,45 @@
---
import { Font } from "astro:assets";
import "@/styles/global.css";
interface Props {
title?: string;
currentYear?: number;
}
const {
title = "frzn.dev",
currentYear = new Date().getFullYear()
} = Astro.props;
---
<!DOCTYPE html>
<html lang="en">
<head>
<title>{title}</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<Font cssVariable="--font-fira-code" preload />
</head>
<body>
<div class="page-container">
<div class="header">
<pre>
_______ _____
| ___|.----.-----.-----.-----.-----.| \.-----.--.--.
| ___|| _| _ |-- __| -__| || -- | -__| | |
|___| |__| |_____|_____|_____|__|__||_____/|_____|\___/</pre>
</div>
<div class="content">
<slot />
</div>
<div class="footer">
<hr>
(c) frzn.dev 2018 - {currentYear} / design and "backend" by <a href="/~roscoe">~roscoe</a>
</div>
</div>
</body>
</html>

79
src/pages/index.astro Normal file
View file

@ -0,0 +1,79 @@
---
import Layout from '../layouts/Layout.astro';
import TorNotice from '../components/TorNotice.astro';
import MemberList from '../components/MemberList.astro';
import ServicesTable from '../components/ServicesTable.astro';
import SystemInfo from '../components/SystemInfo.astro';
import { readdirSync, statSync } from "node:fs";
function getUsers(): string[] {
const usernames: string[] = [];
let users;
try {
users = readdirSync("/home", { withFileTypes: true });
} catch {
return usernames;
}
for (const user of users) {
if (user.isDirectory()) {
try {
statSync(`/home/${user.name}/public_html`);
usernames.push(user.name);
} catch {
// public_html doesn't exist
}
}
}
return usernames;
}
const users = getUsers();
// Services data
const services = [
{ url: "https://git.frzn.dev", name: "Forgejo", desc: "A lightweight git server (which is better than Gogs)", requiresMember: true },
{ url: "https://p.frzn.dev", name: "fiche", desc: "A command line pastebin (similar to termbin)", requiresMember: false },
{ url: "https://pb.frzn.dev", name: "PrivateBin", desc: "A minimalist, open source online pastebin", requiresMember: false },
{ url: "https://bitwarden.frzn.dev/", name: "Vaultwarden", desc: "A Bitwarden-compatible server written in Rust", requiresMember: true },
{ url: "https://snowflake.torproject.org/", name: "", desc: "A web proxy server", requiresMember: false },
{ url: "https://crypt.frzn.dev", name: "CryptPad", desc: "An open-source web-based encrypted suite of realtime collaborative editors", requiresMember: false },
{ url: "", name: "Mumble", desc: "A VoIP server", requiresMember: true, special: "frzn.dev:64738 (Not running 24/7)" },
{ url: "https://irc.frozenelectronics.ca:8088/", name: "ZNC", desc: "An IRC bouncer", requiresMember: true, special: "frzn.dev:6697/6667" },
];
function formatDate(date: Date): string {
const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
const dayName = days[date.getUTCDay()];
const day = date.getUTCDate();
const monthName = months[date.getUTCMonth()];
const year = date.getUTCFullYear();
let hours = date.getUTCHours();
const ampm = hours >= 12 ? "PM" : "AM";
hours = hours % 12 || 12;
const minutes = date.getUTCMinutes().toString().padStart(2, "0");
const seconds = date.getUTCSeconds().toString().padStart(2, "0");
return `${dayName}, ${day} ${monthName} ${year} ${hours}:${minutes}:${seconds} ${ampm} (UTC)`;
}
const currentDate = formatDate(new Date());
const currentYear = new Date().getFullYear();
---
<Layout title="frzn.dev" currentYear={currentYear}>
<div>
<TorNotice />
<MemberList members={users} />
<ServicesTable services={services} />
</div>
<div>
<SystemInfo currentDate={currentDate} />
</div>
</Layout>

189
src/styles/global.css Normal file
View file

@ -0,0 +1,189 @@
:root {
--background: #11111b;
--foreground: #cdd6f4;
--links: #89b4fa;
--font-fira-code: 'Fira Code', monospace;
}
html,
body {
height: 98%;
}
body {
background-color: var(--background);
color: var(--foreground);
margin: 20px 75px 20px 75px;
line-height: 1.5em;
font-size: 10pt;
letter-spacing: -0.015em;
min-width: 520px;
}
body,
pre {
font-family: var(--font-fira-code);
}
a {
color: var(--links);
text-decoration: underline dotted;
}
a:hover {
text-decoration: underline solid;
}
ul {
list-style: none;
margin-left: 0;
padding: 0;
}
li {
padding-left: 1em;
text-indent: -1em;
}
li:before {
content: '-';
padding-right: 5px;
}
p,
ul,
pre {
margin: 0;
}
.header,
.section {
padding-bottom: 1em;
}
.content {
display: grid;
grid-template-columns: auto max-content;
grid-template-rows: 1fr;
grid-column-gap: 0px;
grid-row-gap: 0px;
}
.section {
min-width: 20em;
width: max-content;
}
.section:last-child {
padding-bottom: 0;
}
ul.members li {
text-indent: 0;
}
ul.members li:before {
content: none;
}
ul.members li.error {
color: lightcoral;
}
ul.members li {
display: flex;
align-items: center;
gap: 0.5em;
}
ul.members li a {
text-decoration: none;
}
.member-icon {
width: 1em;
height: 1em;
vertical-align: middle;
}
.member-icon.owner {
color: #f9e2af;
}
.member-icon.admin {
color: #a6e3a1;
}
ul.sidebar-links li:before {
content: '>';
}
body > div {
padding-bottom: 1em;
}
body > div:nth-last-child(-n + 3) {
padding-bottom: 0;
}
.footer {
text-align: center;
}
table.services {
padding-left: 1em;
}
table.services td {
padding: 0 1em 0 0;
}
table.services tr td:first-child {
padding-right: 0.5em;
}
table.services tr td:nth-child(2)::before {
content: '- ';
}
table.services tr td:last-child {
font-style: italic;
}
table.system-info {
padding-left: 1em;
}
table.system-info td {
padding: 0 1em 0 0;
}
table.system-info tr td:first-child {
padding-right: 0.5em;
min-width: 100px;
}
.page-container {
min-height: 100%;
display: grid;
grid-template-rows: auto 1fr auto;
}
.page-container > div {
padding: 5px;
}
.page-container > div.sidebar {
border-left: 2px solid gray;
padding-left: 10px;
}
.footer hr {
margin-top: 0;
border: none;
border-top: 2px solid gray;
}
small {
line-height: 0;
}