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

46
.gitignore vendored
View file

@ -1,42 +1,4 @@
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global'
# Ignore bundler config.
/.bundle
# Ignore all environment files (except templates).
/.env*
!/.env*.erb
# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep
# Ignore pidfiles, but keep the directory.
/tmp/pids/*
!/tmp/pids/
!/tmp/pids/.keep
# Ignore storage (uploaded files in development and any SQLite databases).
/storage/*
!/storage/.keep
/tmp/storage/*
!/tmp/storage/
!/tmp/storage/.keep
/public/assets
# Ignore master key for decrypting credentials and more.
/config/master.key
# JetBrains can you not
.idea/*
/tmp
# VScode can you not
.vscode/*
/.astro
/.direnv
dist/
result*

34
astro.config.ts Normal file
View file

@ -0,0 +1,34 @@
import { defineConfig, fontProviders } from 'astro/config';
import node from '@astrojs/node';
import icon from 'astro-icon';
export default defineConfig({
output: 'server',
adapter: node({
mode: 'standalone',
}),
server: {
host: true,
},
integrations: [icon()],
fonts: [
{
name: 'Fira Code',
cssVariable: '--font-fira-code',
provider: fontProviders.local(),
options: {
variants: [
{
src: ['./src/assets/fonts/FiraCode-Regular.woff2'],
weight: 'normal',
style: 'normal',
},
],
fallbacks: ['monospace'],
},
},
],
});

146
index.php
View file

@ -1,146 +0,0 @@
<?php
function getUsers()
{
$usernames = array();
// Get all users in /home directory
$users = glob("/home/*");
foreach ($users as $user) {
if (is_dir($user)) {
if (file_exists("$user/public_html")) {
$username = basename($user);
$usernames[] = $username;
}
}
}
return $usernames;
}
// Execute the function and display results
$usernames = getUsers();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<title>frzn.dev</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="stylesheet" href="/assets/master.css">
</head>
<body>
<div class="page-container">
<div class="header">
<pre>
_______ _____
| ___|.----.-----.-----.-----.-----.| \.-----.--.--.
| ___|&#8203;| _| _ |-- __| -__| |&#8203;| -- | -__| | |
|___| |__| |_____|_____|_____|__|__|&#8203;|_____/|_____|\___/</pre>
</div>
<div class="content">
<div>
<div class="section tor-notice">
<pre>
+----------------------------------------------------------------+
| This site is accessible via Tor! |
| <a href="http://frzndev32nhnla77oozhxhz5yzo4abldr6zbc4qkdh5hcyanizlxs2ad.onion/">frzndev32nhnla77oozhxhz5yzo4abldr6zbc4qkdh5hcyanizlxs2ad.onion</a> |
+----------------------------------------------------------------+
</pre>
</div>
<div class="section">
<p>Members</p>
<ul class="members">
<?php if (count($usernames) > 0) { ?>
<?php foreach ($usernames as $username) { ?>
<li><a href="/~<?= $username ?>">~<?= $username ?></a><?= $username == "amr" ? "*" : "" ?></li>
<?php } ?>
<?php } else { ?>
<li class="error">Error fetching members!</li>
<?php } ?>
</ul>
<small>
* owner &amp; admin
</small>
</div>
<div class="section">
<p>Services</p>
<table class="services">
<tr>
<td><a href="https://git.frzn.dev">git.frzn.dev</a></td>
<td>Forgejo<sup>1</sup></td>
<td>A lightweight git server (which is better than Gogs)</td>
</tr>
<tr>
<td><a href="https://p.frzn.dev">p.frzn.dev</a></td>
<td>fiche</td>
<td>A command line pastebin (similar to termbin)</td>
</tr>
<tr>
<td><a href="https://pb.frzn.dev">pb.frzn.dev</a></td>
<td>PrivateBin</td>
<td>A minimalist, open source online pastebin</td>
</tr>
<tr>
<td><a href="https://bitwarden.frzn.dev/">bitwarden.frzn.dev</a></td>
<td>Vaultwarden<sup>1</sup></td>
<td>A Bitwarden-compatible server written in Rust</td>
</tr>
<tr>
<td><a href="https://snowflake.torproject.org/">Snowflake</a></td>
<td></td>
<td>A web proxy server</td>
</tr>
<tr>
<td><a href="https://crypt.frzn.dev">crypt.frzn.dev</a></td>
<td>CryptPad</td>
<td>An open-source web-based encrypted suite of realtime collaborative editors</td>
</tr>
<tr>
<td>frzn.dev:64738</td>
<td>Mumble<sup>1,2</sup></td>
<td>A VoIP server</td>
</tr>
<tr>
<td><a href="https://irc.frozenelectronics.ca:8088/">frzn.dev:6697/6667</a></td>
<td>ZNC<sup>1</sup></td>
<td>An IRC bouncer</td>
</tr>
</table>
<small>
<sup>1</sup> Only available for existing members<br>
<sup>2</sup> Not running 24/7
</small>
</div>
</div>
<div>
<div class="section">
<p>System Info</p>
<table class="system-info">
<tr>
<td>time:</td>
<td><?= gmdate("l, j F Y g:i:s A (e)") ?></td>
</tr>
<tr>
<td>os:</td>
<td>Ubuntu GNU/Linux 24.04 (LTS)</td>
</tr>
</table>
</div>
</div>
</div>
<div class="footer">
<hr>
(c) frzn.dev 2018 - <?= date('Y') ?> / design and "backend" by <a href="/~roscoe">~roscoe</a>
</div>
</div>
</body>
</html>

23
package.json Normal file
View file

@ -0,0 +1,23 @@
{
"name": "frzn.dev",
"type": "module",
"version": "1.0.0",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"fmt": "prettier --write .",
"fmt:check": "prettier --check ."
},
"dependencies": {
"@astrojs/node": "^10.0.4",
"@iconify-json/fa": "^1.2.2",
"@iconify-json/fa-solid": "^1.2.2",
"astro": "^6.1.5",
"astro-icon": "^1.1.5"
},
"devDependencies": {
"prettier": "^3.8.1"
}
}

4785
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

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>

View file

@ -1,15 +1,12 @@
@font-face {
font-family: "FiraCode";
src: url("/assets/FiraCode-Regular.woff2") format("woff2");
}
:root {
--background: #11111b;
--foreground: #cdd6f4;
--links: #89b4fa;
--font-fira-code: 'Fira Code', monospace;
}
html, body {
html,
body {
height: 98%;
}
@ -25,7 +22,7 @@ body {
body,
pre {
font-family: "FiraCode", monospace;
font-family: var(--font-fira-code);
}
a {
@ -49,7 +46,7 @@ li {
}
li:before {
content: "-";
content: '-';
padding-right: 5px;
}
@ -70,7 +67,7 @@ pre {
grid-template-rows: 1fr;
grid-column-gap: 0px;
grid-row-gap: 0px;
}
}
.section {
min-width: 20em;
@ -90,11 +87,35 @@ ul.members li:before {
}
ul.members li.error {
color: lightcoral
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: ">";
content: '>';
}
body > div {
@ -122,7 +143,7 @@ table.services tr td:first-child {
}
table.services tr td:nth-child(2)::before {
content: "- ";
content: '- ';
}
table.services tr td:last-child {

24
tsconfig.json Normal file
View file

@ -0,0 +1,24 @@
{
"compilerOptions": {
"baseUrl": ".",
"outDir": "dist/",
"declaration": true,
"composite": true,
"noEmit": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"jsx": "react-jsx",
"jsxImportSource": "react",
"verbatimModuleSyntax": true,
"plugins": [
{
"name": "@astrojs/ts-plugin"
}
],
"paths": {
"@/*": ["src/*"]
}
},
"include": [".astro/types.d.ts", "src/**/*.d.ts"],
"exclude": ["dist", "result", "node_modules"]
}