Add programming-resources; swi->generic Nintendo;

Remove custom html from markdown, clean-up UI (again)

Signed-off-by: Paul W. <lambdapaul@protonmail.com>
This commit is contained in:
Paul W. 2024-02-13 18:01:07 -05:00
parent dc86590e6a
commit e3c70632e2
No known key found for this signature in database
GPG Key ID: 0023B93C0FF1E1D4
28 changed files with 392 additions and 304 deletions

BIN
bun.lockb

Binary file not shown.

13
components/container.tsx Normal file
View File

@ -0,0 +1,13 @@
export type ChildrenType = JSX.Element | Array<ChildrenType>;
function Container(props: { children?: ChildrenType, ignore?: boolean }) {
if (props.ignore)
return <>{props.children}</>;
return (
<div className='container'>
{props.children}
</div>
);
}
export default Container;

View File

@ -1,25 +1,18 @@
import Title from './title'; import Title from './title';
import Container, { ChildrenType } from './container';
type ChildrenType = JSX.Element | Array<ChildrenType>;
type LayoutProps = { type LayoutProps = {
children?: ChildrenType, children?: ChildrenType,
removeContainer?: boolean, removeContainer?: boolean,
}; };
function Container(props: {children?: ChildrenType, ignore?: boolean}) { function Layout(props: LayoutProps) {
if (props.ignore)
return <>{props.children}</>;
return <div className='container'>
{props.children}
</div>;
}
function Layout(props : LayoutProps) {
return ( return (
<> <>
<Title /> <Title />
<Container ignore={props.removeContainer}>{props.children}</Container> <Container ignore={props.removeContainer}>
{props.children}
</Container>
</> </>
); );
} }

View File

@ -1,103 +0,0 @@
import style from '../styles/lists.module.css';
import React, { ReactElement } from 'react';
export interface listItem {
[x: string]: any;
children?: listItem[] | string[];
url?: string;
type?: string;
title: string;
description?: string;
};
export function toListItem(record: Record<string, any>): listItem | null {
if (!record.title)
return null;
let children: listItem[] | string[] = [];
if (Array.isArray(record.children) && record.children.length) {
let lchildren: listItem[] = [];
let schildren: string[] = [];
for (const child of record.children) {
if (typeof child === 'string') {
schildren.push(child);
continue;
}
const lChild = toListItem(child);
if (lChild)
lchildren.push(lChild);
}
if (!lchildren.length) {
children = schildren;
}
else {
children = [...lchildren, ...schildren.map((s: string): listItem => {
return { title: s };
})];
}
}
return Object.assign(record, {
title: record.title,
url: record.url,
children: children.length ? children : undefined,
type: record.type?.length ? record.type : undefined,
description: record.description,
});
}
export function mapChild(
obj: listItem | string,
level: number,
typeMap? : Record<string, (o: listItem) => JSX.Element>
) {
if (typeof obj === 'string') {
if (obj === '')
return <></>
return <span className={style.listItem}>{obj}</span>
}
if (obj.title === '')
return <></>
const desc = obj.description
? <span className={style.listItemDesc}>{obj.description}</span>
: <></>;
if (obj.url)
return (
<>
<span className={style.listItem}><a href={obj.url}>{obj.title}</a></span>
{desc}
</>);
if (!obj.children) {
let cb;
if (obj.type && typeMap) {
cb = typeMap[obj.type]
}
return cb
? cb(obj)
: (<><span className={style.listItem}>{obj.title}</span>{desc}</>);
}
let title: ReactElement;
if (level >= 0 && level <= 4)
title = React.createElement(`h${level + 2}`, {}, obj.title);
else
title = React.createElement('strong', {}, obj.title);
return (
<section className={level < 4 && `block ${style.block}` || ''}>
{title}
{obj.description ? <p className={style.desc}>{obj.description}</p> : <></>}
<div>
{obj.children.map(l => mapChild(l, level + 1, typeMap))}
</div>
</section>
);
}

View File

@ -4,12 +4,17 @@ import Pages from '../public/external.json';
function QuickLinks() { function QuickLinks() {
return ( return (
<div className='block'> <div className='block'>
<div className='h2'>Quick Links</div> <h2>Quick Links</h2>
{ {
Object.entries(Pages).map(([title, link], i) => { Object.entries(Pages).map(([title, link], i) => {
const extern = link.match(/^http/) && `blue extern` || ''; const extern = link.match(/^http/) && `blue extern` || '';
return ( return (
<Link key={i} href={link} className={`${extern} link button`}>{title}</Link> <Link
key={i}
href={link}
className={`${extern} link button`}>
{title}
</Link>
); );
}) })
} }

View File

@ -2,22 +2,29 @@ import Link from "next/link";
import NotesInfo from '../public/notes.json'; import NotesInfo from '../public/notes.json';
function RecentNotes() { function RecentNotes() {
const notes = Object.entries(NotesInfo); const notes = Object.entries(NotesInfo).reverse();
return ( return (
<div className='block'> <ul className='block'>
<div className='h2'>Recent Notes</div> <h2>Recent Notes</h2>
{notes?.slice(0, 10) {notes?.slice(0, 5)
.map(([slug, note]: any) => { .map(([slug, note]: any, i: number) => {
return <Link key={slug} href={`/notes/${slug}`} className={`button link`}>{note.title}</Link> return (
<li
key={i}
>
<Link
href={`/notes/${slug}`}>
{note.title}
</Link>
</li>
);
}) })
} }
{ {
notes.length > 10 && notes.length > 5 &&
<div> <Link href='/notes'>More...</Link>
<Link href='/notes' className='h5'>More...</Link>
</div>
} }
</div> </ul>
); );
} }

View File

@ -1,34 +1,46 @@
import Link from "next/link"; import Link from "next/link";
import date from "../lib/date"; import { toRelativeDate } from "../lib/date";
import style from '../styles/recent-posts.module.css'; import style from '../styles/recent-posts.module.css';
import PostsInfo from '../public/posts.json'; import PostsInfo from '../public/posts.json';
function PostBlock({ slug, otime, title }: { slug: string, otime: string, title: string }) {
return (
<div className={style.block}>
<span className={style.postDate}>
{toRelativeDate(new Date(otime))}
</span>
<div className={style.postTitle}>
<Link href={`/posts/${slug}`}>
{title}
</Link>
</div>
</div>
);
}
function RecentPosts() { function RecentPosts() {
const posts = Object.entries(PostsInfo); const posts = Object.entries(PostsInfo).reverse();
if (!posts.length) if (!posts.length)
return <></>; return <></>;
return ( return (
<div className='block'> <div className='block'>
<div className='h2'>Recent Posts</div> <h2>Recent Posts</h2>
<div className={style.container}> <div className={style.container}>
{posts?.slice(0, 10) {posts?.slice(0, 10)
.map(([slug, post]: any) => { .map(([slug, post]: any, i: number) => {
return <div className={style.block} key={post.slug}> return (
<span className={style.postDate}> <PostBlock
{date.toRelativeDate(new Date(post.otime))} key={i}
</span> slug={slug}
<div className={style.postTitle}> title={post.title}
<Link href={`/posts/${slug}`}> otime={post.otime} />
{post.title} );
</Link>
</div>
</div>
})} })}
</div> </div>
{ {
posts.length > 10 && posts.length > 10 &&
<div className={style.more}> <div className={style.more}>
<Link href='/posts' className='h5'>More...</Link> <Link href='/posts' >More...</Link>
</div> </div>
} }
</div> </div>

View File

@ -17,7 +17,7 @@ function createPathElements(ancestors: Array<{ name: string, path: string }>) {
</> </>
); );
}); });
}; }
function Title() { function Title() {
@ -31,9 +31,11 @@ function Title() {
const subPaths = pagePath.split('/'); const subPaths = pagePath.split('/');
for (const p of subPaths.slice(1, subPaths.length)) { for (const p of subPaths.slice(1, subPaths.length)) {
splitPath.push({ name: currRoot[p].title, path: p }); splitPath.push({ name: currRoot[p].title, path: p });
if (currRoot === undefined if (currRoot === undefined
|| currRoot[p] === undefined || currRoot[p] === undefined
|| currRoot[p].subpages !== undefined) || currRoot[p].subpages === undefined)
break;
currRoot = currRoot[p].subpages!; currRoot = currRoot[p].subpages!;
} }
if (splitPath !== undefined && splitPath.length > 0) if (splitPath !== undefined && splitPath.length > 0)
@ -53,9 +55,11 @@ function Title() {
</h1> </h1>
</div> </div>
<div className={`${style.nav} h1`}> <div className={`${style.nav} h1`}>
{title {
title
? <><Link href='/'>PaulW.XYZ</Link> / {pathElements}{title}</> ? <><Link href='/'>PaulW.XYZ</Link> / {pathElements}{title}</>
: <>PaulW.XYZ /</>} : <>PaulW.XYZ /</>
}
</div> </div>
</> </>
); );

View File

@ -1,3 +1,4 @@
// getMonth() method ranges from 0-11 so no reason to account for it
const months = [ const months = [
'January', 'January',
'February', 'February',
@ -13,35 +14,50 @@ const months = [
'December' 'December'
]; ];
const ordSfx = ['','st','nd','rd','th']; function toHumanReadableDate(date: Date | string, disable?: { year?: boolean, month?: boolean, day?: boolean }) {
const oDate = (typeof date === 'string') ? new Date(date) : date;
function toHumanReadableDate(date: Date | string, disable?: {year?: boolean, month?: boolean, day?: boolean}) {
const oDate = (typeof date === 'string')? new Date(date): date;
const year = oDate.getFullYear(); const year = oDate.getFullYear();
const month = months[oDate.getMonth()]; const month = months[oDate.getMonth()];
const day = oDate.getDate(); const day = oDate.getDate();
let sfx; let out = !disable?.day ? `${day}${getOrdinalDaySuffix(day)}` : '';
if (day >= 1 && day <= 3)
sfx = ordSfx[day];
else
sfx = ordSfx[4];
let out = !disable?.day ? `${day}${sfx}` : '';
out = !disable?.month ? `${out} ${month}` : out; out = !disable?.month ? `${out} ${month}` : out;
out = !disable?.year ? `${out} ${year}` : out; out = !disable?.year ? `${out} ${year}` : out;
return out; return out;
} }
function toRelativeDate(date: Date | string): string {
const oDate = (typeof date === 'string')? new Date(date): date; export function getOrdinalDaySuffix(day: number) {
switch (day) {
case 1:
case 21:
case 31:
return 'st';
case 2:
case 22:
return 'nd';
case 3:
case 23:
return 'rd';
default:
return 'th';
}
}
export function toRelativeDate(date: Date | string): string {
const oDate = (typeof date === 'string') ? new Date(date) : date;
if (!isValid(oDate)) {
return 'Invalid Date';
}
const now = new Date(); const now = new Date();
const diff = now.getTime() - oDate.getTime(); const diff = now.getTime() - oDate.getTime();
let tdiff = Math.floor(diff/1000); let tdiff = Math.floor(diff / 1000);
if (tdiff < 0) { if (tdiff < 0) {
return toHumanReadableDate(oDate); return toHumanReadableDate(oDate);
@ -51,14 +67,14 @@ function toRelativeDate(date: Date | string): string {
return `${tdiff} seconds ago`; return `${tdiff} seconds ago`;
} }
tdiff = Math.floor(tdiff/60); tdiff = Math.floor(tdiff / 60);
if (tdiff < 60) { if (tdiff < 60) {
return `${tdiff} minute${tdiff === 1? '' : 's'} ago`; return `${tdiff} minute${tdiff === 1 ? '' : 's'} ago`;
} }
tdiff = Math.floor(tdiff/60); tdiff = Math.floor(tdiff / 60);
if (tdiff < 24) { if (tdiff < 24) {
return `${tdiff} hour${tdiff === 1? '' : 's'} ago`; return `${tdiff} hour${tdiff === 1 ? '' : 's'} ago`;
} }
if (tdiff < 48) { if (tdiff < 48) {
return `Yesterday`; return `Yesterday`;
@ -66,15 +82,24 @@ function toRelativeDate(date: Date | string): string {
if (oDate.getFullYear() != now.getFullYear()) if (oDate.getFullYear() != now.getFullYear())
return toHumanReadableDate(oDate); return toHumanReadableDate(oDate);
return toHumanReadableDate(oDate, {year: true}); return toHumanReadableDate(oDate, { year: true });
} }
function isValid(date: any) { export function getFullMonth(month: number) {
return (new Date(date)).toString() === 'Invalid Date'; if (month >= 1 && month <= 12)
return months[month];
return 'Invalid Month';
} }
export function isValid(date: any) {
return (new Date(date)).toString() !== 'Invalid Date';
}
const DateTool = { const DateTool = {
toRelativeDate, toRelativeDate,
isValid getFullMonth,
isValid,
getOrdinalDaySuffix,
}; };
export default DateTool; export default DateTool;

View File

@ -1,4 +1,4 @@
module.exports = { const config = {
i18n: { i18n: {
locales: ['en-US'], locales: ['en-US'],
defaultLocale: 'en-US' defaultLocale: 'en-US'
@ -32,9 +32,15 @@ module.exports = {
}, },
); );
return config return config;
},
images: {
domains: ['avatars.githubusercontent.com']
}, },
};
if (process.env.ANALYZE) {
const bundleAnalyzer = require('@next/bundle-analyzer')({
enabled: true
});
module.exports = bundleAnalyzer(config);
} else {
module.exports = config;
} }

View File

@ -1,17 +0,0 @@
# Nintendo Switch
<a href='https://www.nintendo.com/switch' class='link button extern blue'>Official Website</a>
<a href='https://developer.nintendo.com/' class='link button extern blue'>Developer Portal</a>
## Third-party Software
- [Atmosphère](https://github.com/Atmosphere-NX/Atmosphere)
- custom firmware
## Third-party Resources
- [DekuDeals](https://www.dekudeals.com/)
- price tracker for games with support for all major US retailers
## High-level Emulators
- [yuzu](https://yuzu-emu.org/)
- [Ryujinx](https://ryujinx.org/)

28
notes/nintendo.md Normal file
View File

@ -0,0 +1,28 @@
# Nintendo Consoles
## Nintendo Switch
- [Official Website](https://www.nintendo.com/switch)
- [Developer Portal](https://developer.nintendo.com/)
### Third-party Software
- [Atmosphère (Custom Firmware)](https://github.com/Atmosphere-NX/Atmosphere)
### Third-party Resources
- [DekuDeals](https://www.dekudeals.com/)
- price tracker for games with support for all major US retailers
### High-level Emulators
- [yuzu](https://yuzu-emu.org/)
- [Ryujinx](https://ryujinx.org/)
## Wii U
- [Wii U Brew Wiki](https://wiiubrew.org/wiki/Main_Page)
- [Cemu emulator](https://cemu.info/)
- GitHub: [cemu-project / Cemu](https://github.com/cemu-project/Cemu)
## Nintendo 3DS
- [Citra emulator](https://citra-emu.org/)
- [Custom Firmware Guide (3ds.hacks.guide)](https://3ds.hacks.guide/)

View File

@ -0,0 +1,48 @@
# Programming Resources
A handy list of blog posts, articles, videos, and books that I would probably
refer to someone or within something.
- Untangling Lifetimes: The Arena Allocator
Making performant dynamic manual memory management in C feel almost like
garbage collection.
https://www.rfleury.com/p/untangling-lifetimes-the-arena-allocator
- Ryan Fleury's UI Series
Semi-paywalled
https://www.rfleury.com/p/ui-series-table-of-contents
- Immediate-Mode Graphical User Interfaces (2005)
https://caseymuratori.com/blog_0001
https://www.youtube.com/watch?v=Z1qyvQsjK5Y
- What Color is Your Function?
https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/
- Real-time audio programming 101: time waits for nothing
http://www.rossbencina.com/code/real-time-audio-programming-101-time-waits-for-nothing
- Triangulation
https://www.humus.name/index.php?ID=228
- Quantifying the Performance of Garbage Collection vs. Explicit Memory
Management
https://people.cs.umass.edu/~emery/pubs/gcvsmalloc.pdf
- Typing is Hard
https://3fx.ch/typing-is-hard.html
- The Aggregate Magic Algorithms
http://aggregate.org/MAGIC/

View File

@ -1,15 +1,21 @@
# Steam Deck # Steam Deck
<a href='https://www.steamdeck.com/' class='link button extern blue'>Official Website</a> - [Official Website](https://www.steamdeck.com/)
## Third-party Software ## Third-party Software
* [Decky Plugin Loader](https://decky.xyz/) - [Decky Plugin Loader](https://decky.xyz/)
* Installer: [decky\_installer.desktop](https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop) - Source: [GitHub / SteamDeckHomebrew](https://github.com/SteamDeckHomebrew)
- Installer:
[decky_installer.desktop](https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop)
## Access Console-like Youtube in Gaming Mode ## Console-like Youtube in Gaming Mode
* Using Chromium's undocumented command-line options, the user agent can be changed to PlayStation's, Xbox's or Tizen's (Samsung's TV OS) and the application can be launched in full screen by using the `--kiosk` flag. The following XDG Desktop Configuration, for example, can be used and added as a non-Steam game while in Desktop mode for access in gaming mode - Using Chromium's undocumented command-line options, the user agent can be
changed to PlayStation's, Xbox's or Tizen's (Samsung's TV OS) and the
application can be launched in full screen by using the `--kiosk` flag. The
following XDG Desktop Configuration, for example, can be used and added as a
non-Steam game while in Desktop mode for access in gaming mode
```cfg ```cfg
#!/usr/bin/env xdg-open #!/usr/bin/env xdg-open
@ -21,34 +27,45 @@ GenericName=Online Video Platform
Comment=An online video-sharing, social media platform Comment=An online video-sharing, social media platform
Exec=/usr/bin/flatpak run --branch=master --arch=x86_64 --file-forwarding org.chromium.Chrome @@ %F @@ --user-agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64; Xbox; Xbox Series X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.82 Safari/537.36 Edge/20.02' --kiosk 'https://www.youtube.com/tv' Exec=/usr/bin/flatpak run --branch=master --arch=x86_64 --file-forwarding org.chromium.Chrome @@ %F @@ --user-agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64; Xbox; Xbox Series X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.82 Safari/537.36 Edge/20.02' --kiosk 'https://www.youtube.com/tv'
Terminal=false Terminal=false
MimeType=text/plain; MimeType=text/plain; # $XDG_PATH contains the paths used to fetch icons, extensions for supported formats are optional Icon=com.youtube.tv
# $XDG_PATH contains the paths used to fetch icons, extensions for supported formats are optional
Icon=com.youtube.tv
``` ```
* Firefox can also be used however the supported command-line options are limited - Firefox can also be used however the supported command-line options are
* The URL is https://www.youtube.com/tv limited
* Without the user agent change, the above URL is inaccessible - The URL for the TV user interface is https://www.youtube.com/tv
* Adblockers like uBlock Origin, AdBlock Plus (both tested) do not remove ads unlike on the desktop site - Without the user agent change, the above URL is inaccessible and will redirect
* Choosing the Xbox user agent is recommended as button prompts match the Steam Deck's `ABXY` button layout you to the desktop version of the website
* The Electron framework can be used to build a wrapper for the URL. This is the preferable method as it supports exiting from within the application, while browsers only support manual termination from the Steam menu. E.g. (assuming you can build native linux binaries on a device) - Adblockers like uBlock Origin, AdBlock Plus (both tested) do not remove ads
even if they work with the desktop version
```javascript - Choosing an Xbox user agent is recommended as button prompts match the Steam
const { app, BrowserWindow } = require('electron'); Deck's `ABXY` button layout
app.whenReady() - The Electron framework can be used to build a wrapper for the URL
- This is the preferable method as it supports exiting from within the
application, while browsers only support manual termination from the Steam
menu.
- Sample code for the electron app (assuming you can build linux binaries
for the target platform):
```javascript
// sample code to get started
const { app, BrowserWindow } = require('electron');
app
.whenReady()
.then(() => { .then(() => {
const win = new BrowserWindow({ const win = new BrowserWindow({
backgroundColor: '#2e2c29', backgroundColor: '#2e2c29',
kiosk: true kiosk: true,
}); });
win.maximize(); win.maximize();
win.loadURL('https://youtube.com/tv'); win.loadURL('https://youtube.com/tv');
const wc = win.webContents; win.webContents.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; Xbox; Xbox Series X) '
wc.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; Xbox; Xbox Series X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.82 Safari/537.36 Edge/20.02' + 'AppleWebKit/537.36 (KHTML, like Gecko) '
+ 'Chrome/48.0.2564.82 Safari/537.36 Edge/20.02';
}) })
.catch(()=>{}); // swallow errs .catch(() => { });
``` ```
## Miscellaneous ## Miscellaneous
* When using a dock or a hub to connect to an external display, ensure the display supports the refresh rate set on the device as some TVs and other displays only support refresh rates that are multiples of 30Hz - When using a dock or a hub to connect to an external display, ensure the
display supports the refresh rate set on the device; some TVs and some
monitors only support refresh rates that are multiples of 30Hz

View File

@ -1,27 +1,33 @@
# Steam Client # Steam Client
<a href='https://store.steampowered.com' class='link button extern blue'>Steam Store</a> - [Steam Store](https://store.steampowered.com)
<a href='https://developer.valvesoftware.com/wiki/SteamCMD' class='link button extern blue'>SteamCMD</a> - [SteamCMD](https://developer.valvesoftware.com/wiki/SteamCMD)
## Accessing the Console ## Accessing the Console
- Use the following URIs on a browser or a file manager to open GUI client with the console:
- Use the following URIs on a browser or a file manager to open GUI client with
the console:
- `steam://nav/console` - `steam://nav/console`
- `steam://open/console` - `steam://open/console`
- will not work if the Steam client is running in the background - will not work if the Steam client is running in the background
- The `-console` flag can be used with the client executable - The `-console` flag can be used with the client executable
- Alternatively, SteamCMD, a command-line only version of the Steam client, can be used - Alternatively, SteamCMD, a command-line only version of the Steam client, can
- [Windows Binary](https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip) be used
- [Linux Binary](https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz) - [Windows
- [macOS Binary](https://steamcdn-a.akamaihd.net/client/installer/steamcmd_osx.tar.gz) Binary](https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip)
- [Linux
Binary](https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz)
- [macOS
Binary](https://steamcdn-a.akamaihd.net/client/installer/steamcmd_osx.tar.gz)
## Downloading Older Depots ## Downloading Older Depots
Download a single depot (used to download older versions of applications/games): Download a single depot (used to download older versions of applications/games):
```
download_depot <appid> <depotid> [<target manifestid>] [<delta manifestid>] [<depot flags filter>]
```
[SteamDB](https://steamdb.info/) can be used to find the required argument values. `download_depot <appid> <depotid> [<target manifestid>] [<delta manifestid>][<depot flags filter>]`
[SteamDB](https://steamdb.info/) can be used to find the required argument
values.
## Resources ## Resources
@ -38,12 +44,13 @@ download_depot <appid> <depotid> [<target manifestid>] [<delta manifestid>] [<de
- [SteamTradeMatcher](https://steamtradematcher.com/) - [SteamTradeMatcher](https://steamtradematcher.com/)
- one-to-one trading of items on Steam - one-to-one trading of items on Steam
- [ArchiSteamFarm](https://asf.justarchi.net) - [ArchiSteamFarm](https://asf.justarchi.net)
- useful bot written in C# to farm trading cards for owned games that can be sold - useful bot written in C# to farm trading cards for owned games that can be
sold
- [IsThereAnyDeal](https://isthereanydeal.com) - [IsThereAnyDeal](https://isthereanydeal.com)
- tracks game deals for steam, steam key stores and other platforms - tracks game deals for steam, steam key stores and other platforms
- somewhat broken although it is being migrated and modernized, see [New ITAD](https://new.isthereanydeal.com) - somewhat broken although it is being migrated and modernized, see [New
ITAD](https://new.isthereanydeal.com)
- [gg.deals](https://gg.deals) - [gg.deals](https://gg.deals)
- newer than and similar to IsThereAnyDeal with modern UI - newer than and similar to IsThereAnyDeal with modern UI
- [SteamGridDB](https://steamgriddb.com/) - [SteamGridDB](https://steamgriddb.com/)
- custom video game assets for games available and not available on steam - custom video game assets for games available and not available on steam

View File

@ -19,6 +19,7 @@
"uri-js": "^4.4.1" "uri-js": "^4.4.1"
}, },
"devDependencies": { "devDependencies": {
"@next/bundle-analyzer": "^14.0.1",
"@svgr/webpack": "^6.5.1", "@svgr/webpack": "^6.5.1",
"@types/node": "^18.17.17", "@types/node": "^18.17.17",
"@types/react": "^18.2.22", "@types/react": "^18.2.22",

View File

@ -17,7 +17,7 @@ function AboutPage() {
</section> </section>
<hr /> <hr />
<section className='block'> <section className='block'>
<p>Source for this site is available at <a className='button link extern blue' href='https://github.com/LambdaPaul/www'>GitHub / LambdaPaul / www</a></p> <p>Source for this site is available at <a className='link' href='https://github.com/LambdaPaul/www'>GitHub / LambdaPaul / www</a></p>
<p>Relevant information regarding the source is available on the repo and is also provided below.</p> <p>Relevant information regarding the source is available on the repo and is also provided below.</p>
</section> </section>
<section className='block'> <section className='block'>

View File

@ -9,7 +9,8 @@ import RootInfo from '../public/home.json';
function Nav() { function Nav() {
const nav = RootInfo; const nav = RootInfo;
return ( return (
<div className='block' style={{ textAlign: 'center' }}> <div className='block'>
<h2>Navigation</h2>
{ {
Object.entries(nav).map(([slug, info], i) => { Object.entries(nav).map(([slug, info], i) => {
return <Link key={i} href={slug} className='button green'>{info.title}</Link> return <Link key={i} href={slug} className='button green'>{info.title}</Link>
@ -22,10 +23,10 @@ function Nav() {
function HomePage() { function HomePage() {
return ( return (
<Layout> <Layout>
<Nav />
<QuickLinks /> <QuickLinks />
<RecentNotes />
<RecentPosts /> <RecentPosts />
<RecentNotes />
<Nav />
</Layout> </Layout>
) )
} }

View File

@ -1,37 +1,55 @@
import Link from 'next/link'; import Link from 'next/link';
import Layout from '../../components/layout';
import date from '../../lib/date'; import Layout from '../../components/layout';
import { toRelativeDate } from '../../lib/date';
import NotesInfo from '../../public/notes.json'; import NotesInfo from '../../public/notes.json';
function NoteEntry(props: { path: string, note: { title: string, mtime: string } }) { function NoteEntry({ note }: { note: { title: string, mtime: string, slug: string } }) {
return ( return (
<tr> <tr>
<td style={{ flex: '1 0 50%' }}> <td style={{ flex: '1 0 50%' }}>
<Link href={props.path}> <Link href={`/notes/${note.slug}`}>
{props.note.title} {note.title}
</Link> </Link>
</td> </td>
<td style={{ fontStyle: 'italic' }}> <td style={{ fontStyle: 'italic' }}>
{props.note.mtime && date.toRelativeDate(new Date(props.note.mtime))} {note.mtime && toRelativeDate(note.mtime)}
</td> </td>
</tr> </tr>
); );
} }
function NotesPage() { function NotesPage() {
const notes = Object.entries(NotesInfo); const notes = Object.entries(NotesInfo)
.map(([slug, note]) => {
return {
slug,
title: note.title,
mtime: new Date(note.mtime)
}
})
.sort(
(a, b) => {
return b.mtime.getTime() - a.mtime.getTime();
}
);
return ( return (
<Layout> <Layout>
{!notes || notes.length === 0 && <>No notes found</> || <table> {
!notes || notes.length === 0
&& <>No notes found</>
|| <table>
<tbody> <tbody>
{notes.map(([slug, note]: any, i: number) => { {notes.map(
return <NoteEntry path={`/notes/${slug}`} note={note} key={i} /> (note: any, i: number) => {
})} return (<NoteEntry note={note} key={i} />);
}
)}
</tbody> </tbody>
</table>} </table>
}
</Layout> </Layout>
) )
} }

View File

@ -3,6 +3,7 @@ import ReactMarkdown from 'react-markdown';
import style from '../../styles/post.module.css'; import style from '../../styles/post.module.css';
import PostsInfo from '../../public/posts.json'; import PostsInfo from '../../public/posts.json';
import readMarkdown from '../../lib/read-markdown'; import readMarkdown from '../../lib/read-markdown';
import DateTool from '../../lib/date';
interface Post { interface Post {
title: string; title: string;
@ -14,7 +15,43 @@ interface Posts {
[slug: string]: Post [slug: string]: Post
} }
function Post({ post }: { post: Post & { content: string, cover?: string } }) {
function TimeBlock({ mtime, otime }: { mtime: string, otime: string }) {
const ampm = (h: number) => { if (h >= 12) return 'p.m.'; return 'a.m.'; };
const mdate = new Date(mtime);
const odate = new Date(otime);
const format = (date: Date) => {
const day = date.getDay();
const ord = <sup>{DateTool.getOrdinalDaySuffix(date.getDay())}</sup>;
const month = DateTool.getFullMonth(date.getMonth());
const year = date.getFullYear();
const hours = date.getHours() > 12 ? date.getHours() - 12 : date.getHours();
const minPrefix = date.getMinutes() < 10 ? '0' : '';
const minutes = date.getMinutes();
const twelveSfx = ampm(date.getHours());
return <>{day}{ord} {month} {year} at {hours}:{minPrefix}{minutes} {twelveSfx}</>
};
return (
<div style={{ textAlign: 'right', fontSize: '16px', fontFamily: 'Cantarell', fontStyle: 'italic' }}>
{
mtime ?
<div className='mtime' data-text={mdate.toISOString()}>
Last updated: {format(mdate)}
</div>
:
<></>
}
<div className='otime'>
{format(odate)}
</div>
</div>
);
}
function Post({ post }: { post: Post & { content: string, cover?: string, otime: string, mtime?: string } }) {
return (<> return (<>
<Layout removeContainer={true} > <Layout removeContainer={true} >
{<div className={style.imageBlock} {<div className={style.imageBlock}
@ -27,6 +64,7 @@ function Post({ post }: { post: Post & { content: string, cover?: string } }) {
<div className={`${style.spacer} ${post.cover ? style.background : ''}`}></div> <div className={`${style.spacer} ${post.cover ? style.background : ''}`}></div>
<section className={`${style.block} block`}> <section className={`${style.block} block`}>
<div className='container'> <div className='container'>
<TimeBlock mtime={post.mtime} otime={post.otime} />
<ReactMarkdown>{post.content}</ReactMarkdown> <ReactMarkdown>{post.content}</ReactMarkdown>
</div> </div>
</section> </section>

View File

@ -30,7 +30,7 @@ function Posts() {
return <tr key={i} style={{ alignItems: 'center' }}> return <tr key={i} style={{ alignItems: 'center' }}>
<td style={{ display: 'inline-block', textAlign: 'right', fontSize: '0.9rem' }}> <td style={{ display: 'inline-block', textAlign: 'right', fontSize: '0.9rem' }}>
<div style={{ fontStyle: 'italics', fontSize: '.8rem' }}>{ <div style={{ fontStyle: 'italics', fontSize: '.8rem' }}>{
post.mtime && `Updated ${date.toRelativeDate(new Date(post.mtime))}` post.mtime && (post.mtime != post.otime) && `Updated ${date.toRelativeDate(new Date(post.mtime))}`
}</div> }</div>
<div>{date.toRelativeDate(new Date(post.otime))}</div> <div>{date.toRelativeDate(new Date(post.otime))}</div>
</td> </td>

View File

@ -9,11 +9,13 @@ function traverseMap(head: Site, cwd = '', depth = 0) {
let elements = []; let elements = [];
for (const [slug, info] of Object.entries(head.subpages)) { for (const [slug, info] of Object.entries(head.subpages)) {
const path = `${cwd}/${slug}`; const path = `${cwd}/${slug}`;
const children = (<><ul> {traverseMap(info, path, depth + 1)}</ul></>); const children = (<><dl style={{marginLeft: '3rem'}}> {traverseMap(info, path, depth + 1)}</dl></>);
elements.push(<> elements.push(<>
<li> <>
<Link className='button' href={path}>{info.title}</Link> {children} <dt>{info.title}</dt>
</li> <dd><Link href={path}>{path}</Link></dd>
{children}
</>
</>); </>);
} }
return elements; return elements;
@ -23,7 +25,7 @@ function SiteMapPage() {
return <Layout> return <Layout>
<ul>{traverseMap(SiteMap)}</ul> <dl>{traverseMap(SiteMap)}</dl>
</Layout>; </Layout>;
} }

View File

@ -1 +1 @@
{"mos-6502":{"title":"MOS 6502 Microprocessor","mtime":"2023-10-29T18:05:52.440Z"},"nintendo-switch":{"title":"Nintendo Switch","mtime":"2023-10-29T18:06:23.793Z"},"steam-deck":{"title":"Steam Deck","mtime":"2023-11-01T22:27:06.480Z"},"steam":{"title":"Steam Client","mtime":"2023-10-29T18:06:53.897Z"},"zilog-z80":{"title":"Zilog Z80 Microprocessor","mtime":"2023-10-29T18:07:08.580Z"}} {"mos-6502":{"title":"MOS 6502 Microprocessor","mtime":"2023-10-29T18:05:52.439Z"},"zilog-z80":{"title":"Zilog Z80 Microprocessor","mtime":"2023-10-29T18:07:08.579Z"},"programming-resources":{"title":"Programming Resources","mtime":"2024-02-13T21:59:46.795Z"},"steam":{"title":"Steam Client","mtime":"2024-02-13T22:34:17.609Z"},"nintendo":{"title":"Nintendo Consoles","mtime":"2024-02-13T22:45:34.306Z"},"steam-deck":{"title":"Steam Deck","mtime":"2024-02-13T22:53:51.919Z"}}

View File

@ -1 +1 @@
{"title":"PaulW.XYZ","subpages":{"posts":{"title":"Posts","subpages":{}},"notes":{"title":"Notes","subpages":{"mos-6502":{"title":"MOS 6502 Microprocessor","mtime":"2023-10-29T18:05:52.440Z"},"nintendo-switch":{"title":"Nintendo Switch","mtime":"2023-10-29T18:06:23.793Z"},"steam-deck":{"title":"Steam Deck","mtime":"2023-10-29T18:06:45.197Z"},"steam":{"title":"Steam Client","mtime":"2023-10-29T18:06:53.897Z"},"zilog-z80":{"title":"Zilog Z80 Microprocessor","mtime":"2023-10-29T18:07:08.580Z"}}},"about":{"title":"About"},"sitemap":{"title":"Site Map"}}} {"title":"PaulW.XYZ","subpages":{"posts":{"title":"Posts","subpages":{}},"notes":{"title":"Notes","subpages":{"mos-6502":{"title":"MOS 6502 Microprocessor","mtime":"2023-10-29T18:05:52.439Z"},"zilog-z80":{"title":"Zilog Z80 Microprocessor","mtime":"2023-10-29T18:07:08.579Z"},"programming-resources":{"title":"Programming Resources","mtime":"2024-02-13T21:59:46.795Z"},"steam":{"title":"Steam Client","mtime":"2024-02-13T22:34:17.609Z"},"nintendo":{"title":"Nintendo Consoles","mtime":"2024-02-13T22:45:34.306Z"},"steam-deck":{"title":"Steam Deck","mtime":"2024-02-13T22:53:51.919Z"}}},"about":{"title":"About"},"sitemap":{"title":"Site Map"}}}

View File

@ -2,12 +2,12 @@ const fs = require('fs/promises');
const { createReadStream } = require('fs'); const { createReadStream } = require('fs');
const path = require('path'); const path = require('path');
const readline = require('readline/promises'); const readline = require('readline/promises');
const { info } = require('console'); // const { info } = require('console');
async function readFirstLines(filePath, lineCount = 1) { async function readFirstLines(filePath, lineCount = 1) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
const stream = createReadStream(filePath, 'utf-8'); const stream = createReadStream(filePath, { encoding: 'utf-8' });
const rl = readline.createInterface({ input: stream }); const rl = readline.createInterface({ input: stream });
let counter = 0; let counter = 0;
const lines = []; const lines = [];

View File

@ -296,9 +296,9 @@ li {
position: relative; position: relative;
} }
@media screen and (min-width: 1018px) { @media screen and (min-width: 818px) {
.container { .container {
max-width: 1018px; max-width: 818px;
margin: 0 auto; margin: 0 auto;
position: relative; position: relative;
} }

View File

@ -19,8 +19,8 @@
background-color: rgba(13, 17, 23, 0.97); background-color: rgba(13, 17, 23, 0.97);
margin: 0 auto; margin: 0 auto;
border-radius: 0; border-radius: 0;
font-size: 1.25rem; font-size: 1.4rem;
line-height: 2rem; line-height: 2.5rem;
padding: 2rem 1rem; padding: 2rem 1rem;
} }

View File

@ -30,33 +30,16 @@
} }
.postTitle { .postTitle {
font-family: 'EB Garamond', 'Garamond', 'Times New Roman', Times, serif;
flex: 1 1 60%; flex: 1 1 60%;
padding: .25rem 0.75rem; padding: .25rem 0.75rem;
} }
.postTitle a {
text-decoration: none;
border: 1px dotted transparent;
transition: border-width 100ms ease-in-out;
}
.postTitle a:hover {
text-decoration: none;
border-bottom: 1px dotted var(--link-color);
}
.postTitle a:focus {
text-decoration: none;
border: 1px dotted var(--link-color);
}
.postDate { .postDate {
flex: 1 1; flex: 1 1;
display: inline-block; display: inline-block;
text-align: right; text-align: right;
font-style: italic; font-style: italic;
font-size: 0.9rem; font-size: 1rem;
padding: .25rem 0.50rem; padding: .25rem 0.50rem;
} }