hello worlds

This commit is contained in:
Holden
2026-06-26 18:11:52 -05:00
commit e208f1ad5a
61 changed files with 15566 additions and 0 deletions

29
.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# Cloudflare Workers
.wrangler/
*.vars
tools/
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/

4
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

22
AGENTS.md Normal file
View File

@@ -0,0 +1,22 @@
## Development
When starting the dev server, use background mode:
```
astro dev --background
```
Manage the background server with `astro dev stop`, `astro dev status`, and `astro dev logs`.
## Documentation
Full documentation: https://docs.astro.build
Consult these guides before working on related tasks:
- [Adding pages, dynamic routes, or middleware](https://docs.astro.build/en/guides/routing/)
- [Working with Astro components](https://docs.astro.build/en/basics/astro-components/)
- [Using React, Vue, Svelte, or other framework components](https://docs.astro.build/en/guides/framework-components/)
- [Adding or managing content](https://docs.astro.build/en/guides/content-collections/)
- [Adding styles or using Tailwind](https://docs.astro.build/en/guides/styling/)
- [Supporting multiple languages](https://docs.astro.build/en/guides/internationalization/)

43
README.md Normal file
View File

@@ -0,0 +1,43 @@
# Astro Starter Kit: Minimal
```sh
npm create astro@latest -- --template minimal
```
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
├── src/
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

15
astro.config.mjs Normal file
View File

@@ -0,0 +1,15 @@
// @ts-check
import { defineConfig, envField } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
// https://astro.build/config
export default defineConfig({
adapter: cloudflare(),
env: {
schema: {
TWY_API_URL: envField.string({ context: "server", access: "public", default: "https://api.recroom.baby" }),
TWY_CDN_URL: envField.string({ context: "server", access: "public", default: "https://cdn.recroom.baby" })
}
}
});

4751
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "tenwholeyears-web",
"type": "module",
"version": "0.0.1",
"engines": {
"node": ">=22.12.0"
},
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"resize:image": "powershell -ExecutionPolicy Bypass -File ./tools/resize-images.ps1",
"generate-types": "wrangler types"
},
"dependencies": {
"@astrojs/cloudflare": "^14.0.1",
"astro": "^7.0.3",
"wrangler": "^4.105.0"
}
}

1109
public/css/bootstrap-responsive.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

6167
public/css/bootstrap.css vendored Normal file

File diff suppressed because it is too large Load Diff

9
public/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

340
public/css/styles.css Normal file
View File

@@ -0,0 +1,340 @@
/* Jumbotrons
-------------------------------------------------- */
/* Base class
------------------------- */
.jumbotron {
position: relative;
padding: 40px 0;
color: #fff;
text-align: center;
text-shadow: 0 1px 3px rgba(0, 0, 0, .4), 0 0 30px rgba(0, 0, 0, .075);
background: #000;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#020031', endColorstr='#6d3353', GradientType=1);
/* IE6-9 fallback on horizontal gradient */
-webkit-box-shadow: inset 0 3px 7px rgba(0, 0, 0, .2), inset 0 -3px 7px rgba(0, 0, 0, .2);
-moz-box-shadow: inset 0 3px 7px rgba(0, 0, 0, .2), inset 0 -3px 7px rgba(0, 0, 0, .2);
box-shadow: inset 0 3px 7px rgba(0, 0, 0, .2), inset 0 -3px 7px rgba(0, 0, 0, .2);
}
.jumbotron h1 {
font-size: 80px;
font-weight: bold;
letter-spacing: -1px;
line-height: 1;
}
.jumbotron p {
font-size: 24px;
font-weight: 300;
line-height: 1.25;
margin-bottom: 30px;
}
/* Link styles (used on .masthead-links as well) */
.jumbotron a {
color: #fff;
color: rgba(255, 255, 255, .5);
-webkit-transition: all .2s ease-in-out;
-moz-transition: all .2s ease-in-out;
transition: all .2s ease-in-out;
}
.jumbotron a:hover {
color: #fff;
text-shadow: 0 0 10px rgba(255, 255, 255, .25);
}
/* Download button */
.masthead .btn {
padding: 19px 24px;
font-size: 24px;
font-weight: 200;
color: #fff;
/* redeclare to override the `.jumbotron a` */
border: 0;
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
border-radius: 6px;
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 5px rgba(0, 0, 0, .25);
-moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 5px rgba(0, 0, 0, .25);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 5px rgba(0, 0, 0, .25);
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.masthead .btn:hover {
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 5px rgba(0, 0, 0, .25);
-moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 5px rgba(0, 0, 0, .25);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 5px rgba(0, 0, 0, .25);
}
.masthead .btn:active {
-webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, .1), 0 1px 0 rgba(255, 255, 255, .1);
-moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, .1), 0 1px 0 rgba(255, 255, 255, .1);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, .1), 0 1px 0 rgba(255, 255, 255, .1);
}
/* Pattern overlay
------------------------- */
.jumbotron .container {
position: relative;
z-index: 2;
}
.jumbotron:after {
content: '';
display: block;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-image: url(/img/banner.gif);
background-repeat: no-repeat;
background-position: center;
background-size: cover;
opacity: .6;
}
@media only screen and (-webkit-min-device-pixel-ratio: 2),
only screen and (min--moz-device-pixel-ratio: 2),
only screen and (-o-min-device-pixel-ratio: 2/1) {
.jumbotron:after {
background-size: 150px 150px;
}
}
/* Masthead (docs home)
------------------------- */
.masthead {
padding: 70px 0 80px;
margin-bottom: 20px;
color: #fff;
}
.masthead h1 {
font-size: 120px;
line-height: 1;
letter-spacing: -2px;
}
.masthead-logo {
display: block;
width: 430px;
max-width: 100%;
height: auto;
margin: 0 auto 12px;
}
.masthead-title {
margin: 0 0 18px;
font-size: 42px;
line-height: 1;
letter-spacing: -1px;
}
.masthead p {
font-weight: 200;
line-height: 1.25;
}
.masthead-download {
margin-top: 30px;
text-align: center;
}
.masthead-download .btn {
display: inline-block;
margin-bottom: 5px;
}
.masthead-download .subtext {
display: block;
}
.masthead-download .subtext span {
margin-right: 4px;
}
.masthead-platforms {
display: inline;
}
/* Home page
------------------------- */
.home-section {
padding: 30px 0;
}
.intro-section {
padding-top: 40px;
}
.feature-photo {
margin-top: 10px;
}
.archive-feature {
margin-bottom: 20px;
}
.archive-feature h3 {
margin-top: 12px;
}
.archive-feature .thumbnail {
display: block;
max-height: 200px;
margin-right: auto;
margin-left: auto;
}
.gallery-section .thumbnail img {
max-height: 150px;
}
.gallery-section .caption {
margin-top: 8px;
color: #777;
text-align: center;
}
.credit-entry {
display: flex;
align-items: center;
margin-bottom: 24px;
}
.credit-avatar {
width: 72px;
height: 72px;
margin-right: 16px;
object-fit: cover;
}
.credit-entry h3 {
margin-top: 0;
margin-bottom: 4px;
}
.credit-entry p {
margin-bottom: 0;
color: #777;
}
.closing-section {
padding-bottom: 50px;
text-align: center;
}
.closing-section p {
max-width: 720px;
margin-right: auto;
margin-left: auto;
}
/* Tablet to desktop
------------------------- */
@media (min-width: 768px) and (max-width: 979px) {
.jumbotron {
margin-top: -20px; /* Offset bottom margin on .navbar */
}
.jumbotron .container {
width: initial !important;
}
}
/* Tablet
------------------------- */
@media (max-width: 767px) {
.jumbotron {
padding: 40px 20px;
margin-top: -20px; /* Offset bottom margin on .navbar */
margin-right: -20px;
margin-left: -20px;
}
.masthead h1 {
font-size: 90px;
}
.masthead-logo {
width: 360px;
}
.masthead-title {
font-size: 36px;
word-break: break-all;
}
.masthead p,
.masthead .btn {
font-size: 24px;
}
.archive-feature .thumbnail,
.gallery-section .thumbnail img {
max-height: none;
}
}
/* Landscape phones
------------------------- */
@media (max-width: 480px) {
/* Downsize the jumbotrons */
.jumbotron h1 {
font-size: 45px;
}
.masthead-logo {
width: 260px;
}
.masthead-title {
font-size: 28px;
}
.jumbotron p,
.jumbotron .btn {
font-size: 18px;
}
.jumbotron .btn {
display: block;
margin: 0 auto;
}
.archive-feature .thumbnail,
.gallery-section .thumbnail img {
max-height: none;
}
}
/* Footer */
footer {
margin-bottom: 20px;
}
footer .links * {
margin-right: 3px;
}
footer .links {
margin-bottom: 5px;
}
footer .about * {
display: block;
}
.download-card h3 {
margin-bottom: 0;
}
.download-patch-notes {
padding: 20px 0 40px;
}
.patch-note h3 {
margin-top: 0;
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
public/img/banner.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 MiB

BIN
public/img/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
public/img/credits/mia.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

BIN
public/img/credits/nex.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

BIN
public/img/home/feature.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
public/img/videos/hide.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

BIN
public/img/videos/wave.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

2280
public/js/bootstrap.js vendored Normal file

File diff suppressed because it is too large Load Diff

6
public/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
public/js/jquery-4.0.0.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,15 @@
<footer class="container">
<hr />
<div class="links">
<a href="https://ko-fi.com/recroomarchive">Donate</a>
<span class="mx-1.5">|</span>
<a href="https://discord.gg/Mh725mfC4j" class="underline">Discord</a>
</div>
<div class="about">
<span>&copy; {new Date().getFullYear()} RecRoomArchive</span>
<span>
TenWholeYears is not affiliated with Rec Room Inc. All rights
belong to their respective owners.
</span>
</div>
</footer>

25
src/layouts/Layout.astro Normal file
View File

@@ -0,0 +1,25 @@
---
import Footer from "../components/Footer.astro";
---
<!doctype html>
<html>
<head>
<title>TenWholeYears</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="/css/bootstrap.min.css" rel="stylesheet" media="screen" />
<link
href="/css/bootstrap-responsive.min.css"
rel="stylesheet"
media="screen"
/>
<link href="/css/styles.css" rel="stylesheet" />
<link rel="icon" type="image/x-icon" href="/favicon.ico">
</head>
<body>
<slot />
<Footer />
<script src="/js/jquery-4.0.0.min.js" is:inline></script>
<script src="/js/bootstrap.min.js" is:inline></script>
</body>
</html>

53
src/lib/api.ts Normal file
View File

@@ -0,0 +1,53 @@
import { TWY_API_URL } from 'astro:env/server';
import type { LatestBuildsResponse, PatchNote } from './types/api/builds';
import { ApiResult } from './types/api';
const BUILDS_V1 = "api/builds/v1";
export const api = {
builds: {
v1: {
async getLatestVersions(): Promise<ApiResult<LatestBuildsResponse>> {
const response = await fetch(`${TWY_API_URL}/${BUILDS_V1}/latest`, {
headers: {
"Content-Type": "application/json",
},
cache: "no-store",
});
if (!response.ok) {
console.error("Failed to load latest versions.", {
status: response.status,
statusText: response.statusText,
});
return ApiResult.failure<LatestBuildsResponse>("Failed to load latest versions.");
}
var obj = (await response.json()) as LatestBuildsResponse;
return ApiResult.success(obj);
},
async getPatchNotes(): Promise<ApiResult<PatchNote[]>> {
const response = await fetch(`${TWY_API_URL}/${BUILDS_V1}/patchNotes`, {
headers: {
"Content-Type": "application/json",
},
cache: "no-store",
});
if (!response.ok) {
console.error("Failed to load account.", {
status: response.status,
statusText: response.statusText,
});
return ApiResult.failure<PatchNote[]>("Failed to load latest versions.");
}
var obj = (await response.json()) as PatchNote[];
return ApiResult.success(obj);
}
}
}
}

View File

@@ -0,0 +1,11 @@
export interface LatestBuildsResponse {
latestWindowsVersion: string;
latestLinuxVersion: string;
latestMetaVersion: string;
}
export interface PatchNote {
version: string;
date: string;
changes: string[];
}

View File

@@ -0,0 +1,13 @@
export type ApiResult<T> =
| { success: true; data: T }
| { success: false; error: string };
export const ApiResult = {
success<T>(data: T): ApiResult<T> {
return { success: true, data };
},
failure<T>(error: string): ApiResult<T> {
return { success: false, error };
}
};

98
src/pages/downloads.astro Normal file
View File

@@ -0,0 +1,98 @@
---
import Layout from "../layouts/Layout.astro";
const downloads = [
{
platform: "Windows",
version: "v1.0.3",
href: "#",
icon: "/img/platforms/platform-windows.png",
checksum: "checksum: REPLACE_ME",
},
{
platform: "Linux",
version: "v1.0.3",
href: "#",
icon: "/img/platforms/platform-linux.png",
checksum: "checksum: REPLACE_ME",
},
{
platform: "Oculus Quest",
version: "v1.0.3",
href: "#",
icon: "/img/platforms/platform-oculus.png",
checksum: "checksum: REPLACE_ME",
},
];
const patchNotes = [
{
version: "v1.0.3",
date: "6/26/2026",
changes: [
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"Nunc at augue eu nulla aliquet consectetur et in magna.",
],
}
];
---
<Layout>
<div class="container">
<div class="page-header">
<p><a href="/">&larr; Back to home</a></p>
<h1>Downloads</h1>
</div>
<p class="lead">
Choose the build for your platform.
</p>
<div class="row">
{downloads.map((download) => (
<div class="span4">
<div class="well text-center download-card">
<p>
<img
src={download.icon}
alt=""
width="72"
height="72"
/>
</p>
<h3>{download.platform}</h3>
<p>{download.version}</p>
{/* <p class="muted">
<code>{download.checksum}</code>
</p> */}
<p>
<a
href={download.href}
class="btn btn-primary"
aria-label={`Download for ${download.platform}`}
>
Download
</a>
</p>
</div>
</div>
))}
</div>
<section class="download-patch-notes">
<div class="page-header">
<h2>Patch Notes</h2>
</div>
{patchNotes.map((patchNote) => (
<div class="well patch-note">
<h3>{patchNote.version}</h3>
<p class="muted">{patchNote.date}</p>
<ul>
{patchNote.changes.map((change) => (
<li>{change}</li>
))}
</ul>
</div>
))}
</section>
</div>
</Layout>

278
src/pages/index.astro Normal file
View File

@@ -0,0 +1,278 @@
---
import Layout from "../layouts/Layout.astro";
const credits = [
{
name: "splootybean",
role: "Project Lead",
image: "/img/credits/splooty.png",
},
{
name: "fexlar",
role: "Server Developer",
image: "/img/credits/fexlar.png",
},
{
name: "nex",
role: "Developer",
image: "/img/credits/nex.png",
},
{
name: "Magix",
role: "Developer",
image: "/img/credits/magix.png",
},
{
name: "Milky",
role: "Developer",
image: "/img/credits/milky.png",
},
{
name: "SoCoolMan",
role: "Developer",
image: "/img/credits/socoolman.png",
},
{
name: "historyeraser",
role: "Logo Designer",
image: "/img/credits/historyeraser.png",
},
{
name: "Wayne",
role: "Artist / Logo Designer",
image: "/img/credits/wayne.png",
},
{
name: "RyanCR",
role: "Artist / Composer",
image: "/img/credits/ryancr.png",
},
{
name: "Funko",
role: "Artist",
image: "/img/credits/funko.png",
},
{
name: "Cheezit",
role: "Artist",
image: "/img/credits/cheezit.png",
},
{
name: "9021007",
role: "Artist",
image: "/img/credits/9021007.gif",
},
{
name: "Star Centurion",
role: "Artist",
image: "/img/credits/starcenturion.gif",
},
{
name: "GabeTheFirst",
role: "Artist",
image: "/img/credits/gabethefirst.png",
},
{
name: "HappyVR",
role: "Leaf Shader",
image: "/img/credits/happyvr.png",
},
{
name: "Magolor",
role: "Test Squad",
image: "/img/credits/magolor.png",
},
{
name: "TagComputing",
role: "Test Squad",
image: "/img/credits/tagcomputing.png",
},
{
name: "mia",
role: "Test Squad",
image: "/img/credits/mia.png",
},
];
---
<Layout>
<div class="jumbotron masthead">
<div class="container">
<img
src="/img/logo-tenwholeyears.png"
alt="asdasd"
class="masthead-logo"
/>
<h1 class="masthead-title">TenWholeYears</h1>
<p>A love letter to Rec Room on its true 10th anniversary</p>
<div class="masthead-download">
<a href="/downloads/" class="btn btn-primary btn-large">
Download for Windows x64
</a>
<div class="subtext">
<span>or</span>
<a href="/downloads/" class="masthead-platforms"
>Other Platforms</a
>
</div>
</div>
</div>
</div>
<div class="container">
<section class="home-section intro-section">
<div class="row">
<div class="span6">
<h2>What is TenWholeYears?</h2>
<p class="lead">
TenWholeYears is a love letter to Rec Room and the
community that made it special. Created to celebrate Rec
Room's true tenth anniversary, we invite everyone to
visit the first release of Rec Room, explore activities
that never made it into the game, and reflect on how far
Rec Room and it's community has progressed over the last
ten years.
</p>
<p class="lead">
Read about the inspiration behind TenWholeYears <a
href="#">here</a
>.
</p>
</div>
<div class="span6">
<img
src="/img/home/feature.png"
alt="Beta Frisbee Activity"
class="thumbnail feature-photo"
/>
</div>
</div>
</section>
<section class="home-section">
<div class="page-header">
<h2>Explore the Game</h2>
</div>
<div class="row">
<div class="span4">
<div class="archive-feature">
<img
src="/img/home/card-horseshoes.png"
alt="Scrapped Horseshoes Activity"
class="thumbnail"
/>
<h3>Scrapped Activities</h3>
<p>
Explore prototype activities and unfinished ideas
that never made it into Rec Room. These activities
have been restored to show a glimpse into
what could of existed.
</p>
</div>
</div>
<div class="span4">
<div class="archive-feature">
<img
src="/img/home/card-recroom.png"
alt="Beta LockerRoom (RecRoom)"
class="thumbnail"
/>
<h3>Lorem Ipsum</h3>
<p>
Nam fermentum ut lectus et placerat. Nulla nibh
nulla, rutrum at neque at, vulputate consequat.
</p>
</div>
</div>
<div class="span4">
<div class="archive-feature">
<img
src="/img/home/card-horseshoes.png"
alt="Placeholder screenshots and media image"
class="thumbnail"
/>
<h3>Lorem Ipsum</h3>
<p>
Duis efficitur ligula non sem hendrerit interdum.
Donec facilisis efficitur malesuada. Nullam
venenatis.
</p>
</div>
</div>
</div>
</section>
<section class="home-section gallery-section">
<div class="page-header">
<h2>Community Handheld Videos</h2>
</div>
<ul class="thumbnails">
<li class="span3">
<a href="/img/videos/boombox.gif" class="thumbnail">
<img
src="/img/videos/boombox.gif"
alt="splootybean grooving to the boombox"
loading="lazy"
/>
</a>
<p class="caption">Filmed by <b>@fexlar</b></p>
</li>
<li class="span3">
<a href="/img/videos/hide.gif" class="thumbnail">
<img
src="/img/videos/hide.gif"
alt="guy hiding"
loading="lazy"
/>
</a>
<p class="caption">Filmed by <b>@splootybean</b></p>
</li>
<li class="span3">
<a href="#" class="thumbnail">
<img
src="/img/videos/trickshot.gif"
alt="dodgeball trickshot"
loading="lazy"
/>
</a>
<p class="caption">Filmed by <b>@_.golden_.</b></p>
</li>
<li class="span3">
<a href="#" class="thumbnail">
<img
src="/img/videos/wave.gif"
alt="hello there!"
loading="lazy"
/>
</a>
<p class="caption">Filmed by <b>@splootybean</b></p>
</li>
</ul>
</section>
<section class="home-section credits-section">
<div class="page-header">
<h2>Credits</h2>
</div>
<div class="row">
{
credits.map((credit) => (
<div class="span4">
<div class="credit-entry">
<img
src={credit.image}
alt={`${credit.name} pfp`}
class="credit-avatar"
loading="lazy"
/>
<div>
<h3>{credit.name}</h3>
<p>{credit.role}</p>
</div>
</div>
</div>
))
}
</div>
</section>
</div>
</Layout>

56
tools/README.md Normal file
View File

@@ -0,0 +1,56 @@
# Image Tools
Use `resize-images.ps1` to make web-sized PNGs for the index page.
Run commands from the project root.
## Presets
| Preset | Size | Use |
| --- | --- | --- |
| `logo` | 1200px wide | Source logo used in the masthead |
| `masthead` | 860px wide | Large feature image or decorative logo |
| `feature` | 900x506 | Main "What is TenWholeYears?" image |
| `card` | 600x338 | Three-column Explore section images |
| `gallery` | 480x270 | Gallery thumbnails |
| `banner` | 1920x1080 | Large background or hero screenshots |
| `platform-icon` | 256x256 | Square padded platform logos for the downloads page |
| `profile` | 512x512 | Square cropped profile pictures for credits |
## Examples
Resize one image for the intro feature section:
```powershell
.\tools\resize-images.ps1 -InputPath "C:\Users\Holden\Desktop\screenshot.png" -Preset feature
```
Resize one image and choose the output filename:
```powershell
.\tools\resize-images.ps1 -InputPath "C:\Users\Holden\Desktop\screenshot.png" -Preset gallery -OutputName "gallery-museum.png"
```
Resize every PNG/JPG/BMP in a folder:
```powershell
.\tools\resize-images.ps1 -InputPath "C:\Users\Holden\Desktop\recroom-screenshots" -Preset gallery
```
Resize and crop a profile picture for the credits section:
```powershell
.\tools\resize-images.ps1 -InputPath "C:\Users\Holden\Desktop\person.png" -Preset profile -OutputDir "public/img/profiles" -OutputName "person.png"
```
By default, output files go here:
```text
public/img/generated/
```
Then use them in Astro like this:
```astro
<img src="/img/generated/gallery-museum.png" alt="Describe the screenshot" />
```

174
tools/resize-images.ps1 Normal file
View File

@@ -0,0 +1,174 @@
param(
[Parameter(Mandatory = $true)]
[string]$InputPath,
[ValidateSet("logo", "masthead", "feature", "card", "gallery", "banner", "platform-icon", "profile")]
[string]$Preset = "gallery",
[string]$OutputDir = "public/img/generated",
[string]$OutputName = ""
)
$ErrorActionPreference = "Stop"
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$resolvedInput = Resolve-Path $InputPath
if ([System.IO.Path]::IsPathRooted($OutputDir)) {
$resolvedOutputDir = $OutputDir
}
else {
$resolvedOutputDir = Join-Path $repoRoot $OutputDir
}
New-Item -ItemType Directory -Path $resolvedOutputDir -Force | Out-Null
$presets = @{
logo = @{
Width = 1200
Height = 0
Mode = "fit"
Suffix = "logo"
}
masthead = @{
Width = 860
Height = 0
Mode = "fit"
Suffix = "masthead"
}
feature = @{
Width = 900
Height = 506
Mode = "crop"
Suffix = "feature"
}
card = @{
Width = 600
Height = 338
Mode = "crop"
Suffix = "card"
}
gallery = @{
Width = 480
Height = 270
Mode = "crop"
Suffix = "gallery"
}
banner = @{
Width = 1920
Height = 1080
Mode = "crop"
Suffix = "banner"
}
"platform-icon" = @{
Width = 256
Height = 256
Mode = "pad"
Suffix = "platform-icon"
}
profile = @{
Width = 512
Height = 512
Mode = "crop"
Suffix = "profile"
}
}
function Get-ImageFiles {
param([string]$Path)
$item = Get-Item -LiteralPath $Path
if ($item.PSIsContainer) {
return Get-ChildItem -LiteralPath $Path -File |
Where-Object { $_.Extension -match "^\.(png|jpg|jpeg|bmp)$" }
}
return @($item)
}
function Resize-Image {
param(
[string]$SourcePath,
[string]$DestinationPath,
[int]$TargetWidth,
[int]$TargetHeight,
[string]$Mode
)
Add-Type -AssemblyName System.Drawing
$image = [System.Drawing.Image]::FromFile($SourcePath)
if ($TargetHeight -eq 0) {
$TargetHeight = [int][Math]::Round($image.Height * $TargetWidth / $image.Width)
}
$bitmap = New-Object System.Drawing.Bitmap $TargetWidth, $TargetHeight
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
$graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
$graphics.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality
$graphics.Clear([System.Drawing.Color]::Transparent)
if ($Mode -eq "crop") {
$scale = [Math]::Max($TargetWidth / $image.Width, $TargetHeight / $image.Height)
$sourceWidth = [int][Math]::Round($TargetWidth / $scale)
$sourceHeight = [int][Math]::Round($TargetHeight / $scale)
$sourceX = [int][Math]::Max(0, [Math]::Round(($image.Width - $sourceWidth) / 2))
$sourceY = [int][Math]::Max(0, [Math]::Round(($image.Height - $sourceHeight) / 2))
$sourceRect = New-Object System.Drawing.Rectangle $sourceX, $sourceY, $sourceWidth, $sourceHeight
$destRect = New-Object System.Drawing.Rectangle 0, 0, $TargetWidth, $TargetHeight
$graphics.DrawImage($image, $destRect, $sourceRect, [System.Drawing.GraphicsUnit]::Pixel)
}
elseif ($Mode -eq "pad") {
$scale = [Math]::Min($TargetWidth / $image.Width, $TargetHeight / $image.Height)
$drawWidth = [int][Math]::Round($image.Width * $scale)
$drawHeight = [int][Math]::Round($image.Height * $scale)
$drawX = [int][Math]::Round(($TargetWidth - $drawWidth) / 2)
$drawY = [int][Math]::Round(($TargetHeight - $drawHeight) / 2)
$destRect = New-Object System.Drawing.Rectangle $drawX, $drawY, $drawWidth, $drawHeight
$graphics.DrawImage($image, $destRect)
}
else {
$graphics.DrawImage($image, 0, 0, $TargetWidth, $TargetHeight)
}
$bitmap.Save($DestinationPath, [System.Drawing.Imaging.ImageFormat]::Png)
$graphics.Dispose()
$bitmap.Dispose()
$image.Dispose()
}
$presetConfig = $presets[$Preset]
$files = Get-ImageFiles -Path $resolvedInput
foreach ($file in $files) {
$name = if ($OutputName -and $files.Count -eq 1) {
$OutputName
}
else {
"{0}-{1}.png" -f $file.BaseName, $presetConfig.Suffix
}
if ([System.IO.Path]::GetExtension($name) -eq "") {
$name = "$name.png"
}
$destination = Join-Path $resolvedOutputDir $name
Resize-Image `
-SourcePath $file.FullName `
-DestinationPath $destination `
-TargetWidth $presetConfig.Width `
-TargetHeight $presetConfig.Height `
-Mode $presetConfig.Mode
Write-Output ("{0} -> {1}" -f $file.Name, $destination)
}

11
tsconfig.json Normal file
View File

@@ -0,0 +1,11 @@
{
"extends": "astro/tsconfigs/strict",
"include": [
".astro/types.d.ts",
"**/*",
"./worker-configuration.d.ts"
],
"exclude": [
"dist"
]
}

14
wrangler.jsonc Normal file
View File

@@ -0,0 +1,14 @@
{
"$schema": "./node_modules/wrangler/config-schema.json",
"compatibility_date": "2026-06-27",
"compatibility_flags": ["global_fetch_strictly_public"],
"name": "tenwholeyears-web",
"main": "@astrojs/cloudflare/entrypoints/server",
"assets": {
"directory": "./dist",
"binding": "ASSETS"
},
"observability": {
"enabled": true
}
}