1 Commits

Author SHA1 Message Date
6a44a847cd Header 2026-04-02 21:41:48 -05:00
43 changed files with 2150 additions and 1345 deletions

96
.gitignore vendored
View File

@@ -1,67 +1,41 @@
# Logs
logs
*.log
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# env files (can opt-in for committing if needed)
.env*
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# vercel
.vercel
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
node_modules_pc/
node_modules_surface/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
/package-lock.json
/nbproject/private/
.vscode
.serverless
.cache
yarn.lock
# typescript
*.tsbuildinfo
next-env.d.ts

36
README.md Normal file
View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

8
next.config.ts Normal file
View File

@@ -0,0 +1,8 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
reactCompiler: true,
};
export default nextConfig;

1025
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,23 @@
{
"name": "domsplace",
"version": "7.0.0",
"main": "index.js",
"license": "MIT",
"version": "0.1.0",
"private": true,
"scripts": {
"start:dev": "nodemon --exec ts-node ./src/index.ts"
},
"devDependencies": {
"@types/express": "^5.0.6",
"@types/node": "^25.5.0",
"nodemon": "^3.1.14",
"ts-node": "^10.9.2",
"typescript": "^5.9.3"
"start:dev": "next dev",
"build:prod": "next build",
"start:prod": "next start"
},
"dependencies": {
"express": "^5.2.1"
"next": "16.2.2",
"react": "19.2.4",
"react-dom": "19.2.4",
"sass": "^1.98.0"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"babel-plugin-react-compiler": "1.0.0",
"typescript": "^5"
}
}

1
public/file.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

1
public/globe.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
public/next.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/vercel.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

1
public/window.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

7
src/app/globals.scss Normal file
View File

@@ -0,0 +1,7 @@
@use '@/styles/elements/root';
@use '@/styles/elements/a';
@use '@/styles/elements/all';
@use '@/styles/elements/button';
@use '@/styles/elements/body';
@use '@/styles/elements/html';

23
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,23 @@
import type { Metadata } from "next";
import "./globals.scss";
import { Header } from "@/components/Header/Header";
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<Header />
{children}
</body>
</html>
);
}

11
src/app/page.tsx Normal file
View File

@@ -0,0 +1,11 @@
import React from 'react';
const Home:React.FC<{}> = () => {
return (
<main>
Homepage
</main>
);
}
export default Home;

View File

@@ -0,0 +1,59 @@
.header {
margin-bottom: 2rem;
display: flex;
align-items: center;
justify-content: space-between;
border-radius: var(--radius-large);
border: 1px solid var(--color-neumorph-light);
background: var(--color-header-bg);
padding: 1rem 1.5rem;
box-shadow:
var(--color-header-shadow1),
var(--color-header-shadow2),
var(--color-header-shadow3);
backdrop-filter: blur(12px);
}
.header__brand {}
.header__subtitle {
color: var(--color-header-subtitle);
}
.header__title {
color: var(--color-header-title);
}
.header__nav {
display: none;
align-items: center;
gap: 0.75rem;
}
@media (min-width: 768px) {
.header__nav {
display: flex;
}
}
.header__nav-btn {
border-radius: 9999px;
border: 1px solid var(--color-neumorph-light);
background: var(--color-header-btn-bg);
padding: 0.5rem 1rem;
font-size: 0.875rem;
color: var(--color-header-btn-text);
box-shadow:
var(--color-header-btn-shadow1),
var(--color-header-btn-shadow2);
transition: color 0.2s, box-shadow 0.2s;
border-width: 1px;
border-style: solid;
border-color: var(--color-neumorph-light);
}
.header__nav-btn:hover {
color: var(--color-header-btn-hover-text);
box-shadow:
var(--color-header-btn-hover-shadow1),
var(--color-header-btn-hover-shadow2);
}

View File

@@ -0,0 +1,29 @@
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
border-radius: var(--radius-large);
border: 1px solid var(--color-neumorph-light);
background: var( --color-surface);
box-shadow: var(--box-shadow-deep);
max-width: var(--max-width-large);
margin: auto auto 2rem auto;
&__brand {
}
&__nav {
display: none;
align-items: center;
gap: 0.75rem;
}
@media (min-width: 768px) {
&__nav {
display: flex;
}
}
}

View File

@@ -0,0 +1,21 @@
import HeaderButton from './HeaderButton';
import styles from './Header.module.scss';
export const Header: React.FC<{}> = () => {
return (
<header className={styles['header']}>
<div className={styles['header__brand']}>
</div>
<nav className={styles['header__nav']}>
{['Home', 'Features', 'Work', 'Contact'].map((item) => (
<HeaderButton
key={item}
>
{item}
</HeaderButton>
))}
</nav>
</header>
);
}

View File

@@ -0,0 +1,14 @@
.HeaderButton {
padding: 0.5rem 1rem;
font-size: 0.875rem;
border-radius: 999em;
background: var(--color-surface);
box-shadow: var(--box-shadow-raised-low);
border: 1px solid var(--color-neumorph-light);
transition: all 1s ease;
cursor: pointer;
&:hover {
box-shadow: var(--box-shadow-lowered-low);
}
}

View File

@@ -0,0 +1,14 @@
import React from 'react';
import styles from './HeaderButton.module.scss';
const HeaderButton:React.FC<{
children:React.ReactNode;
}> = ({ children }) => {
return (
<button className={styles.HeaderButton}>
{children}
</button>
);
}
export default HeaderButton;

View File

@@ -1,11 +0,0 @@
import express from 'express';
import router from './routes';
const app = express();
const port = process.env.PORT || 3000;
app.use(router);
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});

View File

@@ -1,9 +0,0 @@
export const LANGUAGES = <const>{
'en': 'English'
};
export type LocaleLanguage = keyof typeof LANGUAGES;
export type LocaleString = {
[K in LocaleLanguage]:string;
};

View File

@@ -1,28 +0,0 @@
import { Request, Response } from 'express';
import { SectionData, sectionRender } from "./section";
import TemplateDefault from "./templates/default";
import { templateRender } from './template';
import { LocaleString } from './locale';
export type Page = {
title?:LocaleString;
sections:SectionData<any>[];
}
export const pageRoute = (page:Page) => {
return async (req:Request, res:Response) => {
console.log(`Got Request. Header Agent: ${req.headers['user-agent']}`);
try {
const content = await templateRender({
page,
template: TemplateDefault,
request: req,
language: 'en'
});
res.status(200).send(content);
} catch (error) {
res.status(500).send(`Internal Server Error`);
}
}
}

View File

@@ -1,16 +0,0 @@
import { Router } from "express";
import { Page, pageRoute } from "../../page";
import SnesDigitalAudioMod from "./snes-digital-audio-mod";
const ThisPage:Page = {
sections: [
]
};
const router = Router();
router.get('/', pageRoute(ThisPage));
router.get('/snes-digital-audio-mod', SnesDigitalAudioMod);
export default router;

View File

@@ -1,147 +0,0 @@
import { Router } from "express";
import { Page, pageRoute } from "../../../page";
/*
# Super Famicom / SNES Digital Audio Mod
Recently I have been revisiting some of my favorite retro game consoles, mostly
due to reorganising my loungeroom. Probably one I wish I spent more time playing
was the Super Nintendo, and I wanted to address some of the problems with my Japanese Super Famicom (SFC).
Primarily the problems stemmed from less than ideal quality, this is due to the
SNES's well known [terrible image softening](https://www.chrismcovell.com/gotRGB/snesblur.html)
and that my SFC was pretty yellowed.
Second issue was my audio. I have some somewhat decent SCART cables I use but
the static caused by interference from the analogue audio was definitely not
ideal. I wanted to bypass the SNES's analogue audio, and hear the digital audio
from the console. To achieve this I needed to perform a digital audio mod.
Finally, I wanted to challenge my soldering skills a bit more, and so I decided
to tackle these issues all at the same time.
## The plan
To address the image quality issue, I wanted to get a [1chip SFC](https://consolemods.org/wiki/SNES:SNES_Model_Differences#Comparisons).
These 1chip systems are named after their motherboard, which were the names
used on later revision motherboards that had noticeably improved image quality.
These later revisions integrate the video circuitry into a single chip, reducing
signal noise and resulting in a noticeably sharper image while retaining RGB output
Second, I had been aware of Digital Audio Mods for the SNES for a while, but
they all typically involved cutting the case of the system to accommodate a full
TOSLINK connector, which I wanted to avoid doing where possible, and keep the
original case intact.
Finally, I had been made aware that [The Retro Channel](https://www.chrismcovell.com/gotRGB/snesblur.html)
had a no-cut SNES digital audio mod, that replaced the RF module of the SNES, and
did not require cutting.
## New Super Famicom
There are really only three ways to get a 1chip SNES;
1) Run the motherboard lottery, where you purchase a SNES, then have to open it and check if it is a
1chip variant or not.
2) Purchase a SNES/SFC Jr. and mod it for RGB support.
3) Purchase a 1chip SNES/SFC from a reseller.
I decided to go with option 3, really wanted to keep the original SFC and didn't
want to spend a fortune trying to find a 1chip myself. I ended up purchasing a
1chip SFC from an eBay reseller for around $100 USD, not too bad considering they
can go for significantly more.
Originally I had planned to also do a full recap of the system, to extend its
life. This is definitely a moment where I realised that buying a 1chip from a
reseller was maybe not the best idea.
Upon opening the system I found it had been recapped already, but the quality of
the work left a lot to be desired. The recap solder points were very messy with
way too much solder remaining on the board. The legs of the caps were also left
rather long and get close to interfering with the RF Shield. I have not yet
recapped the system but I definitely plan to do so in the near future.
Other than the iffy recap job however, the system worked fine and the image
quality compared to my previous SFC was significantly improved.
## Digital Audio Mod
The SNES typically outputs line level audio through the AV port in stereo. This
is fine but the Digital Signal Processor (DSP) chip in the SNES is capable of
producing much higher quality audio, and several games use the full [32 KHz sample rate](https://www.alpha-ii.com/Info/snes-spdif.html)
that the system is capable of, but the audio the analogue output provides is
[significantly more limited](https://www.youtube.com/watch?v=6J7Sea0KniU&t=98s).
By the time I decide to purchase my 1chip, the no cut mods had sold out
unfortunately, which delayed me initially. After a few weeks however The Retro
Channel had created a new version, the [No-Cut Digital Audio Mod v2](https://lectronz.com/products/super-nintendo-digital-audio-no-cut-mod-v2)
The v1 version of the mod took over the RF connector and turned it into a
digital coaxial output, meaning that from the outside the console looked
completely stock. The new v2 version removed the entire RF module and replaced
it with a small PCB that doubled as a 3.5mm coaxial and mini-TOSLINK output,
meaning that the console looked slightly different but still required no cutting
of the case, which is nice.
I do wish I could have purchased one of the original v1 mods, but the v2 was
available and functionally is the same, so I purchased it.
## Installation
Installation was pretty straight forward, mostly following [The Retro Channel's video](https://www.youtube.com/watch?v=OXpKuyHBA48)
I was able to tackle it in an afternoon. The kit comes with all the parts you
need and it took me around an hour to install, taking my time and testing after
each solder to ensure no shorts or bad connections.
The only difficult part was soldering the three wires to the DSP chip, as they
are very close and keeping the legs apart was a bit tricky, but with patience I
got the soldering done without any issues.
## Results
From the outside it is clear the Super Famicom has been modded, but the mod is
otherwise clean and there's no damage to the case, so it looks good. I attached
a mini-TOSLINK to full size TOSLINK adapter, which hides the smaller 3.5mm size
of the connector and keeps it looking cleaner.
As for the audio, it's fantastic. This is by far the best sounding SNES audio I
have ever heard. I was worried my Sony STR-DN1040 would not like the SNES digital audio signal,
since pauses in the audio count as the digital audio stopping, but it handles it
fine and I've heard no stutters or pauses.
Finally I would be remiss if I did not mention the downsides. Really there is
only two. The obvious is the cost; the 1chip itself is expensive for a SNES and
the mod was also not cheap, then the time it took for me to install the mod was
not insignificant.
The second drawback is that the mod only provides digital audio on the audio
generated by the SNES's internal DSP chip. This is rare but the SNES could allow
games to perform their own audio processing, bypassing the SNES DSP chip entirely and
therefore not outputting through the mod. The only notable instances of this are
the Super Gameboy, which used a custom chip to emulate the Gameboy's audio on
the Super Gameboy Cartridge itself, bypassing the SNES DSP, and any games that
make use of the custom MSU-1 chip.
## Surround Sound
I want to do a full post on this in the future, but the SNES supported Dolby Pro Logic
surround sound in some games. I have yet to find a comprehensive list but definitely
Star Ocean supports it, and uses it very effectively. Over the digital audio the
surround is very clear and has a wide soundstage, it's extremely impressive for a
16-bit console.
## Conclusion
This was an expensive and time consuming mod, but it is about as close to the
perfect SNES as one can get. The only other mods I am aware of that could improve it are;
a better RGB bypass mod, similar to what the N64 RGB mods use, or a pure digital
video mod, similar to the [RetroGEM](https://www.pixelfx.co/product-page/n64-hdmi)
mods, but I am not aware of any for the SNES currently.
*/
const BlogPage:Page = {
sections: [
]
};
const router = Router();
router.get('/', pageRoute(BlogPage));
export default router;

View File

@@ -1,31 +0,0 @@
import { Router } from 'express';
import { Page, pageRoute } from '../page';
import BlogRoute from './blog';
const HomePage:Page = {
sections:[
{
type: 'hero',
properties: {
title: 'Dominic Masters\nSoftware Developer and Tinkerer.',
subtitle: `I develop all manner of things, and tinker with tech new and old.`,
buttonLeft: {
text: `View the blog`,
url: `/blog`
},
buttonRight: {
text: `About me`,
url: `/about`
}
}
}
]
};
const router = Router();
router.get('/', pageRoute(HomePage));
router.get('/blog', BlogRoute);
export default router;

View File

@@ -1,57 +0,0 @@
import { Request } from 'express';
import HeroSection from './sections/hero';
import { Template } from './template';
import { LocaleLanguage } from './locale';
import { Page } from './page';
const SECTION_TYPES = <const>{
'hero': HeroSection
}
export type Section<P> = {
properties:P;
validate:(properties:P) => P;
};
export type SectionType = keyof typeof SECTION_TYPES;
export type SectionTypeFor<T extends SectionType> = (
typeof SECTION_TYPES[T]
);
export type SectionProperties<T extends SectionType> = (
SectionTypeFor<T>['properties']
);
export type SectionData<T extends SectionType> = {
type:T;
properties:SectionProperties<T>;
}
export type SectionRenderer<T extends SectionType> = (p:{
properties:SectionProperties<T>;
template:Template;
language:LocaleLanguage;
request:Request;
page:Page;
}) => Promise<string>;
export const sectionRender = async <T extends SectionType>(p:{
request:Request,
section:SectionData<T>;
template:Template;
language:LocaleLanguage;
page:Page;
}):Promise<string> => {
if(!p.template.sections[p.section.type]) {
console.warn(`No section renderer found for section type "${p.section.type}" in template "${p.template.name}".`);
return '';
}
const renderer = p.template.sections[p.section.type] as SectionRenderer<T>;
const properties = p.section.properties;
return await renderer({
...p,
properties: p.section.properties
});
}

View File

@@ -1,29 +0,0 @@
import { Section } from "../section";
type HeroProperties = {
title:string|null;
subtitle:string|null;
buttonLeft?:{
text:string;
url:string;
};
buttonRight?:{
text:string;
url:string;
};
};
const HERO:Section<HeroProperties> = {
properties: {
title: '',
subtitle: '',
},
validate: props => {
if(!props.title) throw new Error('Hero section must have a title.');
return props;
}
};
export default HERO;

View File

@@ -0,0 +1,4 @@
a {
color: inherit;
text-decoration: none;
}

View File

@@ -0,0 +1,3 @@
* {
box-sizing: border-box;
}

View File

@@ -0,0 +1,16 @@
body {
margin: 0;
padding: 1rem;
max-width: 100vw;
overflow-x: hidden;
min-height: 100%;
font-family: Arial, Helvetica, sans-serif;
background-color: var(--color-background);
background:
radial-gradient(circle at top left, rgba(255, 0, 170, 0.18), transparent 28%),
radial-gradient(circle at bottom right, rgba(130, 70, 255, 0.22), transparent 30%),
linear-gradient(180deg, var(--color-background) 0%, var(--color-background-secondary) 100%)
;
color: white;
}

View File

@@ -0,0 +1,9 @@
button {
padding: 0;
border: none;
background: none;
font-family: inherit;
cursor: inherit;
color: inherit;
font-size: inherit;
}

View File

@@ -0,0 +1,5 @@
html {
height: 100%;
max-width: 100vw;
overflow-x: hidden;
}

View File

@@ -0,0 +1,31 @@
:root {
--color-background: #12091f;
--color-background-secondary: #0e0718;
--color-surface: rgba(26, 15, 44, 1);
--color-neumorph-light: rgba(255, 255, 255, 0.1);
--color-neumorph-dark: rgba(0, 0, 0, 0.25);
--radius-large: 1.75rem;
--box-shadow-raised-high:
10px 10px 24px rgba(5,2,10,0.85),
-8px -8px 20px rgba(91,53,141,0.08),
inset 1px 1px 0 rgba(255,255,255,0.03)
;
--box-shadow-raised-low:
6px 6px 14px rgba(8,3,15,0.9),
-5px -5px 14px rgba(103,64,156,0.1)
;
--box-shadow-lowered-low:
inset 3px 3px 8px rgba(6,2,12,0.95),
inset -2px -2px 8px rgba(118,76,176,0.08)
;
--backdrop-raised-high: blur(12px);
--backdrop-raised-low: blur(6px);
--max-width-large: 1600px;
}

View File

@@ -1,27 +0,0 @@
import { Request } from "express";
import { Page } from "./page";
import { LocaleLanguage } from "./locale";
import { Section, SectionRenderer, SectionType, SectionTypeFor } from "./section";
export type TemplateSections = {
[key in SectionType]?:SectionRenderer<key>;
};
export type Template = {
name:string;
sections:TemplateSections;
render:(p:{
page:Page;
request:Request;
language:LocaleLanguage;
}) => Promise<string>;
}
export const templateRender = (p:{
page:Page,
template:Template,
request:Request,
language:LocaleLanguage
}):Promise<string> => {
return p.template.render(p);
}

View File

@@ -1,16 +0,0 @@
import { SectionRenderer } from "../../section";
const DefaultTemplateHeroSection:SectionRenderer<'hero'> = async ({
properties,
template,
language,
request
}) => {
return [
'<div>',
`<h1>${properties.title}</h1>`,
'</div>'
].join('\n');
}
export default DefaultTemplateHeroSection;

View File

@@ -1,42 +0,0 @@
import { sectionRender } from "../../section";
import { Template } from "../../template";
import DefaultTemplateHeroSection from "./hero";
const TEMPLATE_DEFAULT:Template = {
name: 'default',
sections: {
'hero': DefaultTemplateHeroSection
},
render: async ({ language, page, request }) => {
return [
`<!DOCTYPE html>`,
`<html>`,
`<head>`,
`<meta charset="UTF-8" />`,
`<title>${page.title ? page.title[language] : 'Untitled Page'}</title>`,
`<style type="text/css">`,
`body { font-family: Arial, sans-serif; margin: 0; padding: 0; }`,
`h1 { color: #333; }`,
`</style>`,
`</head>`,
`<body>`,
`header`,
...(await Promise.all(page.sections.map(async section => {
return await sectionRender({
language,
page,
request,
template: TEMPLATE_DEFAULT,
section
});
}))),
`footer`,
`</body>`,
`</html>`
].join('\n');
}
};
export default TEMPLATE_DEFAULT;

View File

@@ -1,16 +0,0 @@
import { SectionRenderer } from "../../section";
const PSPTemplateHeroSection:SectionRenderer<'hero'> = async ({
properties,
template,
language,
request
}) => {
return [
'<div>',
`<h1>${properties.title}</h1>`,
'</div>'
].join('\n');
}
export default PSPTemplateHeroSection;

View File

@@ -1,42 +0,0 @@
import { sectionRender } from "../../section";
import { Template } from "../../template";
import PSPTemplateHeroSection from "./hero";
const TEMPLATE_PSP:Template = {
name: 'psp',
sections: {
'hero': PSPTemplateHeroSection
},
render: async ({ language, page, request }) => {
return [
`<!DOCTYPE html>`,
`<html>`,
`<head>`,
`<meta charset="UTF-8" />`,
`<title>${page.title ? page.title[language] : 'Untitled Page'}</title>`,
`<style type="text/css">`,
`body { font-family: Arial, sans-serif; margin: 0; padding: 0; }`,
`h1 { color: #333; }`,
`</style>`,
`</head>`,
`<body>`,
`header`,
...(await Promise.all(page.sections.map(async section => {
return await sectionRender({
language,
page,
request,
template: TEMPLATE_PSP,
section
});
}))),
`footer`,
`</body>`,
`</html>`
].join('\n');
}
};
export default TEMPLATE_PSP;

View File

@@ -1,14 +1,34 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "dist",
"rootDir": "src",
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}

145
types/cache-life.d.ts vendored Normal file
View File

@@ -0,0 +1,145 @@
// Type definitions for Next.js cacheLife configs
declare module 'next/cache' {
export { unstable_cache } from 'next/dist/server/web/spec-extension/unstable-cache'
export {
updateTag,
revalidateTag,
revalidatePath,
refresh,
} from 'next/dist/server/web/spec-extension/revalidate'
export { unstable_noStore } from 'next/dist/server/web/spec-extension/unstable-no-store'
/**
* Cache this `"use cache"` for a timespan defined by the `"default"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 900 seconds (15 minutes)
* expire: never
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 15 minutes, start revalidating new values in the background.
* It lives for the maximum age of the server cache. If this entry has no traffic for a while, it may serve an old value the next request.
*/
export function cacheLife(profile: "default"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"seconds"` profile.
* ```
* stale: 30 seconds
* revalidate: 1 seconds
* expire: 60 seconds (1 minute)
* ```
*
* This cache may be stale on clients for 30 seconds before checking with the server.
* If the server receives a new request after 1 seconds, start revalidating new values in the background.
* If this entry has no traffic for 1 minute it will expire. The next request will recompute it.
*/
export function cacheLife(profile: "seconds"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"minutes"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 60 seconds (1 minute)
* expire: 3600 seconds (1 hour)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 minute, start revalidating new values in the background.
* If this entry has no traffic for 1 hour it will expire. The next request will recompute it.
*/
export function cacheLife(profile: "minutes"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"hours"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 3600 seconds (1 hour)
* expire: 86400 seconds (1 day)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 hour, start revalidating new values in the background.
* If this entry has no traffic for 1 day it will expire. The next request will recompute it.
*/
export function cacheLife(profile: "hours"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"days"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 86400 seconds (1 day)
* expire: 604800 seconds (1 week)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 day, start revalidating new values in the background.
* If this entry has no traffic for 1 week it will expire. The next request will recompute it.
*/
export function cacheLife(profile: "days"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"weeks"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 604800 seconds (1 week)
* expire: 2592000 seconds (1 month)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 week, start revalidating new values in the background.
* If this entry has no traffic for 1 month it will expire. The next request will recompute it.
*/
export function cacheLife(profile: "weeks"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"max"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 2592000 seconds (1 month)
* expire: 31536000 seconds (365 days)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 month, start revalidating new values in the background.
* If this entry has no traffic for 365 days it will expire. The next request will recompute it.
*/
export function cacheLife(profile: "max"): void
/**
* Cache this `"use cache"` using a custom timespan.
* ```
* stale: ... // seconds
* revalidate: ... // seconds
* expire: ... // seconds
* ```
*
* This is similar to Cache-Control: max-age=`stale`,s-max-age=`revalidate`,stale-while-revalidate=`expire-revalidate`
*
* If a value is left out, the lowest of other cacheLife() calls or the default, is used instead.
*/
export function cacheLife(profile: {
/**
* This cache may be stale on clients for ... seconds before checking with the server.
*/
stale?: number,
/**
* If the server receives a new request after ... seconds, start revalidating new values in the background.
*/
revalidate?: number,
/**
* If this entry has no traffic for ... seconds it will expire. The next request will recompute it.
*/
expire?: number
}): void
import { cacheTag } from 'next/dist/server/use-cache/cache-tag'
export { cacheTag }
export const unstable_cacheTag: typeof cacheTag
export const unstable_cacheLife: typeof cacheLife
}

57
types/routes.d.ts vendored Normal file
View File

@@ -0,0 +1,57 @@
// This file is generated automatically by Next.js
// Do not edit this file manually
type AppRoutes = "/"
type PageRoutes = never
type LayoutRoutes = "/"
type RedirectRoutes = never
type RewriteRoutes = never
type Routes = AppRoutes | PageRoutes | LayoutRoutes | RedirectRoutes | RewriteRoutes
interface ParamMap {
"/": {}
}
export type ParamsOf<Route extends Routes> = ParamMap[Route]
interface LayoutSlotMap {
"/": never
}
export type { AppRoutes, PageRoutes, LayoutRoutes, RedirectRoutes, RewriteRoutes, ParamMap }
declare global {
/**
* Props for Next.js App Router page components
* @example
* ```tsx
* export default function Page(props: PageProps<'/blog/[slug]'>) {
* const { slug } = await props.params
* return <div>Blog post: {slug}</div>
* }
* ```
*/
interface PageProps<AppRoute extends AppRoutes> {
params: Promise<ParamMap[AppRoute]>
searchParams: Promise<Record<string, string | string[] | undefined>>
}
/**
* Props for Next.js App Router layout components
* @example
* ```tsx
* export default function Layout(props: LayoutProps<'/dashboard'>) {
* return <div>{props.children}</div>
* }
* ```
*/
type LayoutProps<LayoutRoute extends LayoutRoutes> = {
params: Promise<ParamMap[LayoutRoute]>
children: React.ReactNode
} & {
[K in LayoutSlotMap[LayoutRoute]]: React.ReactNode
}
}

61
types/validator.ts Normal file
View File

@@ -0,0 +1,61 @@
// This file is generated automatically by Next.js
// Do not edit this file manually
// This file validates that all pages and layouts export the correct types
import type { AppRoutes, LayoutRoutes, ParamMap } from "./routes.js"
import type { ResolvingMetadata, ResolvingViewport } from "next/types.js"
type AppPageConfig<Route extends AppRoutes = AppRoutes> = {
default: React.ComponentType<{ params: Promise<ParamMap[Route]> } & any> | ((props: { params: Promise<ParamMap[Route]> } & any) => React.ReactNode | Promise<React.ReactNode> | never | void | Promise<void>)
generateStaticParams?: (props: { params: ParamMap[Route] }) => Promise<any[]> | any[]
generateMetadata?: (
props: { params: Promise<ParamMap[Route]> } & any,
parent: ResolvingMetadata
) => Promise<any> | any
generateViewport?: (
props: { params: Promise<ParamMap[Route]> } & any,
parent: ResolvingViewport
) => Promise<any> | any
metadata?: any
viewport?: any
}
type LayoutConfig<Route extends LayoutRoutes = LayoutRoutes> = {
default: React.ComponentType<LayoutProps<Route>> | ((props: LayoutProps<Route>) => React.ReactNode | Promise<React.ReactNode> | never | void | Promise<void>)
generateStaticParams?: (props: { params: ParamMap[Route] }) => Promise<any[]> | any[]
generateMetadata?: (
props: { params: Promise<ParamMap[Route]> } & any,
parent: ResolvingMetadata
) => Promise<any> | any
generateViewport?: (
props: { params: Promise<ParamMap[Route]> } & any,
parent: ResolvingViewport
) => Promise<any> | any
metadata?: any
viewport?: any
}
// Validate ../../src/app/page.tsx
{
type __IsExpected<Specific extends AppPageConfig<"/">> = Specific
const handler = {} as typeof import("../../src/app/page.js")
type __Check = __IsExpected<typeof handler>
// @ts-ignore
type __Unused = __Check
}
// Validate ../../src/app/layout.tsx
{
type __IsExpected<Specific extends LayoutConfig<"/">> = Specific
const handler = {} as typeof import("../../src/app/layout.js")
type __Check = __IsExpected<typeof handler>
// @ts-ignore
type __Unused = __Check
}

1281
yarn.lock

File diff suppressed because it is too large Load Diff