Compare commits

..

94 Commits

Author SHA1 Message Date
paul 5c9a871b25 Minor typo fix
Signed-off-by: Paul W. <lambdapaul@protonmail.com>
2023-11-01 18:27:47 -04:00
paul 1cc50c621e Fix button focus for active and rm unused import
Signed-off-by: Paul W. <lambdapaul@protonmail.com>
2023-10-31 18:26:52 -04:00
paul 8867754911 Minor UI changes
Signed-off-by: Paul W. <lambdapaul@protonmail.com>
2023-10-30 21:50:20 -04:00
paul d3591c4db6 Fix markdown library incompatibility; fix 404
Signed-off-by: Paul W. <lambdapaul@protonmail.com>
2023-10-30 00:55:45 -04:00
paul 35d56f5cde Bump script ver and refactor md metadata gen
Signed-off-by: Paul W. <lambdapaul@protonmail.com>
2023-10-30 00:18:38 -04:00
paul b5ca20b93d next 13, remove unused, update incorrect/outdated 2023-09-20 00:25:34 -04:00
Paul W 797d13fd0d Add nav key prop 2022-10-04 23:46:16 -04:00
Paul W d94de055d8 Refactor, update UI, and add notes 2022-10-04 23:41:59 -04:00
paul 3781c2c154 Create card component 2022-09-28 00:01:03 -04:00
paul 2c25fdc731 Update next and other deps 2022-09-27 22:04:44 -04:00
paul f326bc1894 Add music to playlist, add more github info and css 2022-09-27 20:52:24 -04:00
paul cf7e85fa6d Add basic GitHub acc. info on about page 2022-09-10 02:43:46 -04:00
paul bc2db8a657 Remove "logo.svg" 2022-09-10 02:08:05 -04:00
paul 0786698336 Remove bloat
Keep rec and res files to not remove the pages themselves
2022-09-10 02:03:52 -04:00
paul bd1e190e6b Create .gitkeep 2022-08-30 18:11:47 -04:00
paul e626a2d653 Delete thoughts-on-baba-is-you.md
Not sure why I had this here.
2022-08-30 17:19:07 -04:00
paul 1c05475a1e Remove links to sites that are barely used 2022-08-06 14:47:40 -04:00
paul 2528efebe3 Update 6502 note 2022-05-16 14:26:38 -04:00
paul 7d26cb0e52 Remove canvas 2022-05-16 14:24:17 -04:00
paul e75ee85077 Update z80 note 2022-05-16 14:23:33 -04:00
paul 8a6ab0d8ea Update steam note 2022-05-16 14:21:25 -04:00
paul e7dc8c7697 Fix svg color 2022-05-16 12:19:26 -04:00
paul 74b168682d Simplify logo svg 2022-05-16 12:15:47 -04:00
paul acce257c0e Add mono font; fix pre for mobile 2022-05-15 18:55:12 -04:00
paul 24c2bcfff0 Add license; make corrections; cleanup 2022-05-15 09:56:45 -04:00
paul 7d17d88b60 Delete monogatari-watch-order.md 2022-05-13 15:45:16 -04:00
paul eb7bb1caaf Add monogatari note 2022-05-04 03:15:25 -04:00
paul 1f3a91207d Merge pull request #9 from LambdaPaul/dev
UI enhancements
2022-04-30 10:01:35 -04:00
paul 9747cd809d UI enhancements 2022-04-30 09:56:18 -04:00
paul fb67086969 Merge pull request #8 from LambdaPaul/dev 2022-04-28 12:40:26 -04:00
paul 19affa2f1f New notes 2022-04-28 12:37:12 -04:00
paul dc761025fa Parameterize css colors 2022-04-28 10:13:27 -04:00
paul 789e7ce70a More UI changes 2022-04-28 10:05:26 -04:00
paul c1ec5ffc7b Give btns active colors 2022-04-28 09:24:17 -04:00
paul 6ba7d561fb More changes to structure and UI 2022-04-27 21:55:18 -04:00
paul 2d4a2809b4 Make prop requests to static 2022-04-27 06:39:10 -04:00
paul 6c999dbabf Change gen dir for deploy purposes 2022-04-27 06:28:15 -04:00
paul 9296ffd2c8 Remove file gen placeholder 2022-04-27 05:38:48 -04:00
paul 82d92ec6a7 Merge branch 'master' of github.com:LambdaPaul/www 2022-04-27 05:37:53 -04:00
paul 4d954bf648 Content update and minor UI changes 2022-04-27 05:36:54 -04:00
paul bbd553d32c UI updates 2022-04-27 05:10:49 -04:00
paul f63607b010 ignore dynamically generated import [bleh] 2022-04-27 04:07:40 -04:00
paul 415f950a42 Cleanup post caching 2022-04-27 04:03:21 -04:00
paul adae0b1fb2 Create posts.json 2022-04-24 00:43:03 -04:00
paul 24a76f8f7c type error hotfixes 2022-04-24 00:40:26 -04:00
paul d189096e96 Webpack hook for jsongen, [somewhat broken] UI changes 2022-04-24 00:27:51 -04:00
paul 103eec9551 Type error and other fixes 2022-04-23 20:35:53 -04:00
paul 8538512fa4 Accessibility changes 2022-04-23 19:26:23 -04:00
paul 088932a742 Yamlify page objects 2022-04-23 19:03:43 -04:00
paul 7ac30458d1 Add orient express 2022-04-20 16:12:27 -04:00
paul 2164775c6f Change 404 tag 2022-02-15 22:24:17 -05:00
paul 9ca3e18dd4 Remove grade-calc 2022-02-15 22:19:49 -05:00
paul 389d4611e5 Use higher res cover for Baba 2022-02-14 15:48:51 -05:00
paul a7a7429d4c Minor dep. updates 2022-02-14 15:45:27 -05:00
paul bfacb23f8a Add posts and old grade-calc
Fix small bugs
>bad commit
2022-02-14 15:32:58 -05:00
paul 391fff28b1 Update playlists.tsx 2022-01-24 19:58:01 -05:00
paul 54485484da Add Twitter 2021-12-17 20:27:27 -05:00
paul 85bdb97d04 Update about.tsx 2021-12-13 13:35:49 -05:00
paul cf23df94c9 Remove redundant sort 2021-12-12 22:19:49 -05:00
paul 52882a8b39 Update rec. list 2021-12-08 03:54:18 -05:00
paul 185b5a5e86 Add Cantarell font variations 2021-12-08 03:07:00 -05:00
paul dcb0c403b3 Add normalize.css 2021-12-08 02:53:04 -05:00
paul dc397934cf Add lang attr. for better accessibility 2021-12-08 02:20:30 -05:00
paul d3af524580 move misplaced file 2021-12-08 02:02:43 -05:00
paul 86a6063c65 Fix keybind on Win changing focus to addrbar 2021-12-08 02:00:34 -05:00
paul 2818cb708c Fix single quotes in js obj 2021-12-08 01:14:11 -05:00
paul e7705b2781 Update dependencies 2021-12-08 01:13:39 -05:00
paul f539f78820 Merge pull request #6 from LambdaPaul/debug
Add resources
2021-12-07 23:31:34 -05:00
paul b817641b4b Display resources page data 2021-12-07 23:30:34 -05:00
paul 13de0e15b0 Remove instructions that do not work 2021-12-07 23:23:56 -05:00
paul a64de137b3 Merge pull request #5 from LambdaPaul/debug
Switch to next.js from vanilla
2021-12-07 22:42:40 -05:00
paul 9f56e40d1c Init next.js port 2021-12-07 22:38:31 -05:00
paul ba6071d2a6 Remove vanilla code 2021-12-06 19:23:41 -05:00
paul c2ea836a9d Make site.json for better automation 2021-10-01 02:39:41 -04:00
paul 24dfab7da6 Reduce result links size 2021-10-01 02:05:15 -04:00
paul d61a535a95 Fix fuzzy init sort 2021-10-01 02:03:22 -04:00
paul b278e2fac0 Add playlists 2021-10-01 01:51:58 -04:00
paul 3509ba9974 Add backspace help 2021-10-01 01:23:35 -04:00
paul 834817401b Add netlify rel 2021-10-01 01:23:17 -04:00
paul fc82097486 Delete CNAME 2021-10-01 00:49:06 -04:00
paul 6cb7b4f1a5 Add about page 2021-10-01 00:28:24 -04:00
paul 79aad66352 Minify lambda SVG 2021-10-01 00:28:00 -04:00
Paul 91dc1c1296 Fix arrow key scrolling and make it work on keydown instead of up
There is an issue with the new key based nav system. When control elements lose their focus, there is no good way to bring it back.
2021-09-24 01:34:34 -04:00
Paul 7e15316ad6 Improve UX and add more page links 2021-09-24 00:08:37 -04:00
Paul e2cfd0f266 Add readme to home, fuzzy search instruction and make UI a tad bit better 2021-09-22 23:53:57 -04:00
paul 9d0d4a4953 Remove redundant messages from README.md 2021-09-22 01:49:16 -04:00
Paul f16083a42c Add README.md 2021-09-14 00:22:00 -04:00
Paul 21e8950146 Remove preset pages; force users to look pages up; fix bugs 2021-09-13 23:55:02 -04:00
Paul 4965af478a Add '.DS_Store' to ignore list 2021-09-13 22:52:58 -04:00
paul 7ece106d6a Deemphasize persia.css footer 2021-08-08 13:32:28 -04:00
paul 38656e9e27 Make the styling consistent for all pages 2021-08-07 21:43:23 -04:00
paul 50f8fc7412 Add favicon.ico 2021-08-07 19:11:35 -04:00
paul 377509b792 Fix broken README.md url 2021-08-07 18:52:30 -04:00
paul 53fb800236 Init www 2021-06-02 21:57:01 -04:00
65 changed files with 1135 additions and 1362 deletions
-3
View File
@@ -6,6 +6,3 @@ dist/
*.bun *.bun
**/.*.md **/.*.md
.env .env
public/posts.json
public/notes.json
public/sitemap.json
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2022-2024 Paul W. Copyright (c) 2022-2023 Paul W.
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+14 -14
View File
@@ -1,20 +1,20 @@
# PaulW.XYZ # PaulW.XYZ
A [Next.js](https://nextjs.com) website that mainly involves generating content out of a bunch of markdown files contained in `notes/` and `posts/` which contain rough, unorganized yet useful information and thought-out articles respectively. A [Next.js](https://nextjs.com) website that mainly involves generating content out of a bunch of markdown files contained in `notes/` and `posts/` which contain rough, unorganized yet useful information and thought-out articles respectively. It's still a work in progress as I have no clear direction I want to take this site toward. However, it is something that I will always keep in check; as I use this as a hub for whatever I work on, especially if it involves a lot of reading.
## License
## Third-Party Licenses
Any trademarks listed on this site are the property of their respective owners. This site neither endorses nor is affiliated with any of the owners. Any source code available on any of the pages is without warranty of any kind and the use of such is at your own risk. Any trademarks listed on this site are the property of their respective owners. This site neither endorses nor is affiliated with any of the owners. Any source code available on any of the pages is without warranty of any kind and the use of such is at your own risk.
### Fonts ### Third-party
#### Fonts
[Hack](https://github.com/source-foundry/Hack) - [Hack](https://github.com/source-foundry/Hack)
- &copy; 2018 Source Foundry Authors - &copy; 2018 Source Foundry Authors
- MIT License - MIT License
- [Cantarell](https://github.com/davelab6/cantarell)
[Cantarell](https://github.com/davelab6/cantarell) - &copy; 2009-2010, [Understanding Limited](mailto:dave@understandinglimited.com)
- &copy; 2009-2010, [Understanding Limited](mailto:dave@understandinglimited.com) - Open Font License, Version 1.1
- Open Font License, Version 1.1 - [EB Garamond](https://github.com/georgd/EB-Garamond)
- &copy; 2010-2013 [Georg Duffner](http://www.georgduffner.at)
[EB Garamond](https://github.com/georgd/EB-Garamond) - Open Font License, Version 1.1
- &copy; 2010-2013 [Georg Duffner](http://www.georgduffner.at)
- Open Font License, Version 1.1
BIN
View File
Binary file not shown.
+27
View File
@@ -0,0 +1,27 @@
import Title from './title';
type ChildrenType = JSX.Element | Array<ChildrenType>;
type LayoutProps = {
children?: ChildrenType,
removeContainer?: boolean,
};
function Container(props: {children?: ChildrenType, ignore?: boolean}) {
if (props.ignore)
return <>{props.children}</>;
return <div className='container'>
{props.children}
</div>;
}
function Layout(props : LayoutProps) {
return (
<>
<Title />
<Container ignore={props.removeContainer}>{props.children}</Container>
</>
);
}
export default Layout;
+103
View File
@@ -0,0 +1,103 @@
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>
);
}
+20
View File
@@ -0,0 +1,20 @@
import Link from 'next/link';
import Pages from '../public/external.json';
function QuickLinks() {
return (
<div className='block'>
<div className='h2'>Quick Links</div>
{
Object.entries(Pages).map(([title, link], i) => {
const extern = link.match(/^http/) && `blue extern` || '';
return (
<Link key={i} href={link} className={`${extern} link button`}>{title}</Link>
);
})
}
</div>
);
}
export default QuickLinks;
+24
View File
@@ -0,0 +1,24 @@
import Link from "next/link";
import NotesInfo from '../public/notes.json';
function RecentNotes() {
const notes = Object.entries(NotesInfo);
return (
<div className='block'>
<div className='h2'>Recent Notes</div>
{notes?.slice(0, 10)
.map(([slug, note]: any) => {
return <Link key={slug} href={`/notes/${slug}`} className={`button link`}>{note.title}</Link>
})
}
{
notes.length > 10 &&
<div>
<Link href='/notes' className='h5'>More...</Link>
</div>
}
</div>
);
}
export default RecentNotes;
+38
View File
@@ -0,0 +1,38 @@
import Link from "next/link";
import date from "../lib/date";
import style from '../styles/recent-posts.module.css';
import PostsInfo from '../public/posts.json';
function RecentPosts() {
const posts = Object.entries(PostsInfo);
if (!posts.length)
return <></>;
return (
<div className='block'>
<div className='h2'>Recent Posts</div>
<div className={style.container}>
{posts?.slice(0, 10)
.map(([slug, post]: any) => {
return <div className={style.block} key={post.slug}>
<span className={style.postDate}>
{date.toRelativeDate(new Date(post.otime))}
</span>
<div className={style.postTitle}>
<Link href={`/posts/${slug}`}>
{post.title}
</Link>
</div>
</div>
})}
</div>
{
posts.length > 10 &&
<div className={style.more}>
<Link href='/posts' className='h5'>More...</Link>
</div>
}
</div>
);
}
export default RecentPosts;
@@ -1,44 +1,40 @@
'use client'
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { useRouter } from 'next/router';
import { Fragment } from 'react';
import style from './title.module.css'; import style from '../styles/title.module.css';
import SiteMap from '../../../public/sitemap.json'; import SiteMap from '../public/sitemap.json';
import { Sites } from '../lib/site'; import Head from 'next/head';
import { SiteSubPages } from '../lib/site';
function createPathElements(ancestors: Array<{ name: string, path: string }>) { function createPathElements(ancestors: Array<{ name: string, path: string }>) {
let currentPath = ''; let currentPath = '';
return ancestors.map((ancestor, id) => { return ancestors.map((ancestor, id) => {
currentPath += `/${ancestor.path}` currentPath += `/${ancestor.path}`
return ( return (
<Fragment key={currentPath} > <>
<Link href={currentPath}>{ancestor.name}</Link> <Link key={id + 1} href={currentPath}>{ancestor.name}</Link>
<> / </> <> / </>
</Fragment> </>
); );
}); });
} };
export default function Title() { function Title() {
const pagePath = usePathname();
const router = useRouter();
const pagePath = router.asPath;
const splitPath: Array<{ name: string, path: string }> = []; const splitPath: Array<{ name: string, path: string }> = [];
// TODO(Paul): clean this up let currRoot: SiteSubPages = SiteMap.subpages;
let currRoot: Sites = SiteMap.pages;
let title: string | null = null; let title: string | null = null;
if (pagePath && pagePath !== '/') { if (pagePath !== '/') {
const subPaths = pagePath.split('?')[0].split('#')[0].split('/'); const subPaths = pagePath.split('/');
for (const p of subPaths.slice(1, subPaths.length)) { for (const p of subPaths.slice(1, subPaths.length)) {
if (!p || !currRoot[p])
continue;
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].pages === undefined) || currRoot[p].subpages !== undefined)
break; currRoot = currRoot[p].subpages!;
currRoot = currRoot[p].pages!;
} }
if (splitPath !== undefined && splitPath.length > 0) if (splitPath !== undefined && splitPath.length > 0)
title = splitPath.pop()!.name; title = splitPath.pop()!.name;
@@ -48,21 +44,21 @@ export default function Title() {
const pathElements = splitPath && createPathElements(splitPath) || <></>; const pathElements = splitPath && createPathElements(splitPath) || <></>;
return ( return (
<> <>
{/* <head> <Head>
<title>{title && `${title} | PaulW.XYZ` || 'PaulW.XYZ'}</title> <title>{title && `${title} | PaulW.XYZ` || 'PaulW.XYZ'}</title>
</head> */} </Head>
<div className={style.container}> <div className={style.container}>
<h1 className={style.title}> <h1 className={style.title}>
{title || 'PaulW.XYZ'} {title || 'PaulW.XYZ'}
</h1> </h1>
</div> </div>
<div className={style.nav}> <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>
</> </>
); );
} }
export default Title;
+80
View File
@@ -0,0 +1,80 @@
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'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;
const year = oDate.getFullYear();
const month = months[oDate.getMonth()];
const day = oDate.getDate();
let sfx;
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?.year ? `${out} ${year}` : out;
return out;
}
function toRelativeDate(date: Date | string): string {
const oDate = (typeof date === 'string')? new Date(date): date;
const now = new Date();
const diff = now.getTime() - oDate.getTime();
let tdiff = Math.floor(diff/1000);
if (tdiff < 0) {
return toHumanReadableDate(oDate);
}
if (tdiff < 60) {
return `${tdiff} seconds ago`;
}
tdiff = Math.floor(tdiff/60);
if (tdiff < 60) {
return `${tdiff} minute${tdiff === 1? '' : 's'} ago`;
}
tdiff = Math.floor(tdiff/60);
if (tdiff < 24) {
return `${tdiff} hour${tdiff === 1? '' : 's'} ago`;
}
if (tdiff < 48) {
return `Yesterday`;
}
if (oDate.getFullYear() != now.getFullYear())
return toHumanReadableDate(oDate);
return toHumanReadableDate(oDate, {year: true});
}
function isValid(date: any) {
return (new Date(date)).toString() === 'Invalid Date';
}
const DateTool = {
toRelativeDate,
isValid
};
export default DateTool;
+2 -2
View File
@@ -1,11 +1,11 @@
export interface Site { export interface Site {
title: string; title: string;
pages?: Sites; subpages?: SiteSubPages;
mtime?: string; mtime?: string;
otime?: string; otime?: string;
} }
export interface Sites { export interface SiteSubPages {
[slug: string]: Site; [slug: string]: Site;
} }
+1 -1
View File
@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/basic-features/typescript for more information.
+40
View File
@@ -0,0 +1,40 @@
module.exports = {
i18n: {
locales: ['en-US'],
defaultLocale: 'en-US'
},
webpack: (config, _options) => {
config.module.rules.push(
{
test: /\.ya?ml$/,
use: 'js-yaml-loader',
},
{
test: /\.svg$/,
use: [{ loader: '@svgr/webpack' }],
},
{
test: /\.md$/,
type: 'asset/source',
},
{
test: /\.otf$/,
type: 'asset/resource',
},
{
test: /\.txt$/,
type: 'asset/source',
},
{
resourceQuery: /raw/,
type: 'asset/source',
},
);
return config
},
images: {
domains: ['avatars.githubusercontent.com']
},
}
-49
View File
@@ -1,49 +0,0 @@
import type {NextConfig } from 'next';
import NextBundleAnalyzer from '@next/bundle-analyzer';
let config: NextConfig = {
reactStrictMode: true,
turbopack: {
rules: {
'*.txt': {
as: '*.js',
loaders: ['raw-loader'],
},
'*.md': {
as: '*.js',
loaders: ['raw-loader'],
}
},
resolveExtensions: ['.txt', '.md', '.tsx', '.ts', '.js']
},
webpack: (config, _options) => {
config.module.rules.push(
{
test: /\.svg$/,
use: [{ loader: '@svgr/webpack' }],
},
{
test: /\.md$/,
type: 'asset/source',
},
{
test: /\.otf$/,
type: 'asset/resource',
},
{
test: /\.txt$/,
type: 'asset/source',
},
);
return config;
},
};
if (process.env.ANALYZE) {
config = NextBundleAnalyzer({
enabled: true
})(config);
}
export default config;
-28
View File
@@ -1,28 +0,0 @@
# Web Browsers
Extensions/Plugins I Use on All Supported Browsers:
- uBlock Origin
- Decentraleyes
- FastForward (RIP)
- CanvasBlocker
- ClearURLs
- Greasemonkey/Tampermonkey
- Indie Wiki Buddy
- SingleFile
Product/Service-specific Extensions:
- SteamDB
- Return YouTube Dislike
- SponsorBlock
## Chromium
- [Chromium Source Docs](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/README.md)
## Firefox
- [Firefox Source Docs](https://firefox-source-docs.mozilla.org/)
### Reducing UI element padding
- Go to the browser's [about\:config](#) page
- Set `browser.uidensity` equal to `1`
## Safari
- [Webkit Docs](https://docs.webkit.org/index.html)
-64
View File
@@ -1,64 +0,0 @@
# Lua Programming Language
<!-- TODO ## Lua 5.4 C API-->
## Lua 5.4 Bytecode
> These are **unstable** and may differ in different versions of the language.
> They are not part of the language specification but an implementation detail, which in this case is the reference implementation.
> The reference implementation used to have a stack based but now uses a register based VM similar to how modern real computer architectures.
The instructions are 32 bits wide; every instruction has an opcode that takes up 7 bits, which leaves out 25 bits for the addresses and values.
The instructions work with three register referred to as: A, B, C; each are of length 8 bits.
<table>
<thead>
<tr>
<th></th>
<th>31</th><th>...</th><th>24</th><th>23</th><th>...</th><th>16</th><th>15</th><th>14</th><th>...</th><th>7</th><th>6</th><th>...</th><th>0</th>
</tr>
</thead>
<tr>
<td>iABC</td>
<td colspan='3' style='text-align:center'>C (8 bits)</td>
<td colspan='3' style='text-align:center'>B (8 bits)</td>
<td style='text-align:center'>k (1 bit)</td>
<td colspan='3' style='text-align:center'>A (8 bits)</td>
<td colspan='3' style='text-align:center'>OP (7 bits)</td>
</tr>
<tr>
<td>iABx</td>
<td colspan='7' style='text-align:center'>B (17 bits)</td>
<td colspan='3' style='text-align:center'>A (8 bits)</td>
<td colspan='3' style='text-align:center'>OP (7 bits)</td>
</tr>
<tr>
<td>iAsBx</td>
<td colspan='7' style='text-align:center'>signed B 17 bits)</td>
<td colspan='3' style='text-align:center'>A (8 bits)</td>
<td colspan='3' style='text-align:center'>OP (7 bits)</td>
</tr>
<tr>
<td>iAx</td>
<td colspan='10' style='text-align:center'>Ax (25 bits)</td>
<td colspan='3' style='text-align:center'>OP (7 bits)</td>
</tr>
<tr>
<td>sJ</td>
<td colspan='10' style='text-align:center'>signed jump address (25 bits)</td>
<td colspan='3' style='text-align:center'>OP (7 bits)</td>
</tr>
</table>
```lua
-- arithmetic to calculate the lengths used from https://www.lua.org/source/5.4/lopcodes.h.html
A = 8
B = 8
C = 8
Bx = A + B + 1 -- 17
Ax = A + Bx -- 25
sJ = A + Bx -- 25
```
This page contains excerpts from Lua's source code which is the copyright of Lua.org, PUC-Rio and is licensed under the MIT License.
[lua.org/license.html](https://www.lua.org/license.html)
+17
View File
@@ -0,0 +1,17 @@
# 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/)
-7
View File
@@ -1,7 +0,0 @@
# References
## [Intel® 64 and IA-32 Architectures Software Developers Manual](https://software.intel.com/en-us/download/intel-64-and-ia-32-architectures-sdm-combined-volumes-1-2a-2b-2c-2d-3a-3b-3c-3d-and-4)
## [CUDA C++ Programming Guide](https://docs.nvidia.com/cuda/pdf/CUDA_C_Programming_Guide.pdf)
## [Windows Internals Book](https://learn.microsoft.com/en-us/sysinternals/resources/windows-internals)
-95
View File
@@ -1,95 +0,0 @@
# Resources
A handy list of blog posts, articles, videos, and books that I would probably
refer to someone or within something.
## Programming
### What Every C Programmer Should Know About Undefined Behavior
- https://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
### Optimizing Software Occlusion Culling
- https://fgiesen.wordpress.com/2013/02/17/optimizing-sw-occlusion-culling-index/
### Mipmap selection in too much detail
- https://pema.dev/2025/05/09/mipmaps-too-much-detail/
### Memory Allocation Strategies
- https://www.gingerbill.org/series/memory-allocation-strategies/
### 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
### Easy Scalable Text Rendering on the GPU
- https://medium.com/@evanwallace/easy-scalable-text-rendering-on-the-gpu-c3f4d782c5ac
### C and C++ Prioritize Performance over Correctness
- https://research.swtch.com/ub [[PDF](https://research.swtch.com/ub.pdf)]
### The Aggregate Magic Algorithms
- http://aggregate.org/MAGIC/
### You Could Have Invented Monads! (And Maybe You Already Have.)
- http://blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html
### Fix Your Timestep!
- https://gafferongames.com/post/fix_your_timestep/
### UTF-8 Everywhere
- http://utf8everywhere.org
### Parsing Gigabytes of JSON per Second
- https://arxiv.org/abs/1902.08318 [[PDF](https://arxiv.org/pdf/1902.08318)]
### What are OKLCH colors?
- https://jakub.kr/components/oklch-colors
### Software Foundations series
- https://softwarefoundations.cis.upenn.edu/
### Software Rendering Alpha-Blending Tricks
- https://gist.github.com/mattiasgustavsson/c11e824e3d603d0c86e5e0dde4ecf839
### A Relational Model of Data for Large Shared Data Banks
- https://fermatslibrary.com/s/a-relational-model-of-data-for-large-shared-data-banks
### Powersort
- https://www.wild-inter.net/publications/munro-wild-2018.pdf
-33
View File
@@ -1,33 +0,0 @@
# Retro Gaming
The use of the term retro is debatable here as the term is used quite inconsistently. To be consistent, _retro_ here refers to anything older than the 7th generation of gaming consoles (x360, ps3) which came out in the mid-2000s.
## Recompilations
### Zelda64Recomp
- https://github.com/Zelda64Recomp/Zelda64Recomp
## Open-source Recreations
### OpenMW
- https://github.com/OpenMW/openmw
### OpenTTD
- https://github.com/OpenTTD/OpenTTD
### NFSIISE
- https://github.com/zaps166/NFSIISE
### RE3
- https://github.com/github/dmca/blob/master/2021/02/2021-02-19-take-two.md
- :(
## WidescreenFixesPack
- https://thirteenag.github.io/wfp
- https://github.com/ThirteenAG/WidescreenFixesPack
## Linux Arm Handhelds
- https://portmaster.games/
- native ports of the reimplementations of many old-skool games
-59
View File
@@ -1,59 +0,0 @@
# Software
## Cross-platform
### Media
- [mpv](https://mpv.io/)
## Windows
- [PowerToys](https://learn.microsoft.com/en-us/windows/powertoys/)
- it's kind of hard to use Windows once you get used to using this
### Archive Utility
- [NanaZip](https://github.com/M2Team/NanaZip)
- 7-zip fork for Windows w/ zstd (which I use a lot), brotli, etc.
### Package Managers
- Winget
- comes with Windows
- Chocolatey
- requires Administrator permissions
### Mounting ISO, CUE images
Windows versions 8 and above natively support mounting ISOs. However CUE images are not supported.
WinCDEmu is a lightweight, open-source disc emulator that supports mounting CUE, NRG, IMG, ISO, etc. images.
- [WinCDEmu Website](https://wincdemu.sysprogs.org/)
- [Source (GitHub)](https://github.com/sysprogs/WinCDEmu)
- [Portable Version](https://wincdemu.sysprogs.org/portable/)
### Master Control Panel / God Mode
(Misnomer; you probably won't use this either)
Shows a list of all the available settings on Windows in a single view.
Open it by exceuting the following command or saving it as a shortcut: `explorer.exe shell:::{ED7BA470-8E54-465E-825C-99712043E01C}`
## MacOS
### Clipboard Management
- [maccy](https://maccy.app/)
- not sure why macOs doesn't have a native clipboard manager like Windows and KDE
### Terminal Emulator
- [iTerm2](https://iterm2.com/)
- terminal with features you'd probably want
### Package Manager
- [HomeBrew](https://brew.sh)
- package manager everyone uses but it is noticeably slow
### Video Players
- [IINA](https://iina.io/)
- video player based on mpv with native macOS UI
+54
View File
@@ -0,0 +1,54 @@
# Steam Deck
<a href='https://www.steamdeck.com/' class='link button extern blue'>Official Website</a>
## Third-party Software
* [Decky Plugin Loader](https://decky.xyz/)
* Installer: [decky\_installer.desktop](https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop)
## Access 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
```cfg
#!/usr/bin/env xdg-open
[Desktop]
Version=1.0
Type=Application
Name=YouTube TV
GenericName=Online Video 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'
Terminal=false
MimeType=text/plain;
# $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
* The URL is https://www.youtube.com/tv
* Without the user agent change, the above URL is inaccessible
* Adblockers like uBlock Origin, AdBlock Plus (both tested) do not remove ads unlike on the desktop site
* Choosing the Xbox user agent is recommended as button prompts match the Steam Deck's `ABXY` button layout
* 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)
```javascript
const { app, BrowserWindow } = require('electron');
app.whenReady()
.then(() => {
const win = new BrowserWindow({
backgroundColor: '#2e2c29',
kiosk: true
});
win.maximize();
win.loadURL('https://youtube.com/tv');
const wc = win.webContents;
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'
})
.catch(()=>{}); // swallow errs
```
## 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
+33 -118
View File
@@ -1,6 +1,27 @@
# Steam # Steam Client
- [Steam Store](https://store.steampowered.com) <a href='https://store.steampowered.com' class='link button extern blue'>Steam Store</a>
<a href='https://developer.valvesoftware.com/wiki/SteamCMD' class='link button extern blue'>SteamCMD</a>
## Accessing the Console
- Use the following URIs on a browser or a file manager to open GUI client with the console:
- `steam://nav/console`
- `steam://open/console`
- will not work if the Steam client is running in the background
- The `-console` flag can be used with the client executable
- Alternatively, SteamCMD, a command-line only version of the Steam client, can be used
- [Windows 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
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.
## Resources ## Resources
@@ -11,124 +32,18 @@
## Third-party Resources ## Third-party Resources
- [SteamDB](https://steamdb.info/) - [SteamDB](https://steamdb.info/)
- tracks depot changes, price history, everything steam - tracks depot changes, price history, everything steam
- [gg.deals](https://gg.deals)
- tracks game deals for steam, steam key stores and other platforms
- [IsThereAnyDeal](https://isthereanydeal.com)
- similar to gg.deals except it does not support key seller tracking
- [SteamGifts](https://steamgifts.com/) - [SteamGifts](https://steamgifts.com/)
- giveaway Steam keys or take part in giveaways - giveaway Steam keys or take part in giveaways
- [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 - useful bot written in C# to farm trading cards for owned games that can be sold
sold - [IsThereAnyDeal](https://isthereanydeal.com)
- 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)
- [gg.deals](https://gg.deals)
- 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
- [ProtonDB](https://www.protondb.com/)
- community-sourced Linux and Steam Deck compatibility tracker
## Steam Client
- [Steam Client Valve Wiki Page](https://developer.valvesoftware.com/wiki/Steam)
- [SteamCMD Valve Wiki Page](https://developer.valvesoftware.com/wiki/SteamCMD)
### Accessing the Console
- Use the following URIs on a browser or a file manager to open GUI client with
the console:
- `steam://nav/console`
- `steam://open/console`
- will not work if the Steam client is running in the background
- The `-console` flag can be used with the client executable
- Alternatively, SteamCMD, a command-line only version of the Steam client, can
be used
- [Windows
Binary (.zip)](https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip)
- [Linux
Binary (.zip)](https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz)
- [macOS
Binary (.zip)](https://steamcdn-a.akamaihd.net/client/installer/steamcmd_osx.tar.gz)
### Downloading Older Depots
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.
## Steam Deck
- [Official Website](https://www.steamdeck.com/)
### Third-party Software
- [Decky Plugin Loader](https://decky.xyz/)
- Source: [GitHub / SteamDeckHomebrew](https://github.com/SteamDeckHomebrew)
- Installer:
[decky_installer.desktop](https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop)
### 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
```ini
#!/usr/bin/env xdg-open
[Desktop]
Version=1.0
Type=Application
Name=YouTube TV
GenericName=Online Video 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'
Terminal=false
MimeType=text/plain;
# $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
- The URL for the TV user interface is https://www.youtube.com/tv
- Without the user agent change, the above URL is inaccessible and will redirect
you to the desktop version of the website
- Adblockers like uBlock Origin, AdBlock Plus (both tested) do not remove ads
even if they work with the desktop version
- Choosing an Xbox user agent is recommended as button prompts match the Steam
Deck's `ABXY` button layout
- 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(() => {
const win = new BrowserWindow({
backgroundColor: '#2e2c29',
kiosk: true,
});
win.maximize();
win.loadURL('https://youtube.com/tv');
win.webContents.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';
})
.catch(() => { });
```
### 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; some TVs and some
monitors only support refresh rates that are multiples of 30Hz
+13 -23
View File
@@ -1,7 +1,5 @@
{ {
"private": true,
"scripts": { "scripts": {
"prebuild": "node ./scripts/generate-metadata.js",
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
@@ -9,33 +7,25 @@
}, },
"dependencies": { "dependencies": {
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"highlight.js": "^11.10.0", "js-yaml-loader": "^1.2.2",
"next": "^15.3.1", "next": "^13.5.1",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"raw-loader": "^4.0.2", "react": "^18.2.0",
"react": "^19.0.0", "react-dom": "^18.2.0",
"react-dom": "^19.0.0", "react-markdown": "^9.0.0",
"react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.5.0",
"rehype-autolink-headings": "^7.1.0",
"rehype-highlight": "^7.0.0",
"rehype-highlight-code-lines": "^1.0.4",
"rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"remark-loader": "^6.0.0",
"remark-math": "^6.0.0",
"uri-js": "^4.4.1" "uri-js": "^4.4.1"
}, },
"devDependencies": { "devDependencies": {
"@next/bundle-analyzer": "^15.0.4", "@svgr/webpack": "^6.5.1",
"@svgr/webpack": "^8.1.0", "@types/node": "^18.17.17",
"@types/node": "^22.7.4", "@types/react": "^18.2.22",
"@types/react": "^19.0.1", "@types/react-dom": "^18.2.7",
"@types/react-dom": "^19.0.1",
"@types/react-syntax-highlighter": "^15.5.7", "@types/react-syntax-highlighter": "^15.5.7",
"eslint": "^9.11.1", "eslint": "^8.49.0",
"eslint-config-next": "^15.0.4", "eslint-config-next": "^13.5.1",
"typescript": "^5.6.2" "typescript": "^4.9.5"
} }
} }
+37
View File
@@ -0,0 +1,37 @@
import Head from 'next/head';
import Link from 'next/link';
import style from '../styles/title.module.css';
function NotFoundPage() {
// clean this page up
return (
<>
<Head>
<title>404: Not Found | PaulW.XYZ</title>
</Head>
<div className={style.container}>
<h1 className={style.title}>
Page Not Found
</h1>
</div>
<div className={`${style.nav} h1`}>
<Link href='/'>PaulW.XYZ</Link> / ... ??? / 404: Not Found </div>
<div className='container'>
<section className='block text center'>
<h1>Error 404</h1>
<p>
<strong>Uh oh! The page you are looking for does not exist...</strong><br />
</p>
<Link href='/' className='button green back link'>Go Home</Link>
<a className='button blue link extern' href='https://en.wikipedia.org/wiki/List_of_HTTP_status_codes'>
More on HTTP status codes
</a>
</section>
</div>
</>
);
}
export default NotFoundPage;
+7
View File
@@ -0,0 +1,7 @@
import type { AppProps } from 'next/app'
import 'normalize.css';
import '../styles/global.css';
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
+37
View File
@@ -0,0 +1,37 @@
import ReactMarkdown from 'react-markdown';
import ReadmeMd from '../README.md';
import License from '../LICENSE.txt';
import Layout from '../components/layout';
function AboutPage() {
return (
<Layout >
<section className='block'>
<p>This is a personal website written by <a href='https://github.com/LambdaPaul'>@LambdaPaul</a>.</p>
<p>Why did I write this?
I do not really know, at least the content I put here.
I wanted a place on the web where I wanted to put everything I think is worth looking at some point in the future.
It seems wise to have things up here even though they may not be worthwhile, as many things ultimately are not.</p>
<p>Got any questions, concerns, or issues? Contact me via email: <code>lambdapaul [at] pm [dot] me</code>.</p>
</section>
<hr />
<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>Relevant information regarding the source is available on the repo and is also provided below.</p>
</section>
<section className='block'>
<h2>README</h2>
<ReactMarkdown>
{ReadmeMd.replace(/^#{1,5} /g, (s: string) => { return `#${s}` })}
</ReactMarkdown>
</section>
<section className='block'>
<h2>LICENSE</h2>
<pre className='license'>{License}</pre>
</section>
</Layout>
);
}
export default AboutPage;
+33
View File
@@ -0,0 +1,33 @@
import React from 'react';
import Link from 'next/link';
import Layout from '../components/layout';
import QuickLinks from '../components/quick-links';
import RecentNotes from '../components/recent-notes';
import RecentPosts from '../components/recent-posts';
import RootInfo from '../public/home.json';
function Nav() {
const nav = RootInfo;
return (
<div className='block' style={{ textAlign: 'center' }}>
{
Object.entries(nav).map(([slug, info], i) => {
return <Link key={i} href={slug} className='button green'>{info.title}</Link>
})
}
</div>
)
}
function HomePage() {
return (
<Layout>
<Nav />
<QuickLinks />
<RecentNotes />
<RecentPosts />
</Layout>
)
}
export default HomePage;
+88
View File
@@ -0,0 +1,88 @@
import Layout from '../../components/layout';
import ReactMarkdown from 'react-markdown';
import SyntaxHighlighter from 'react-syntax-highlighter';
import { monokaiSublime as hlTheme } from 'react-syntax-highlighter/dist/cjs/styles/hljs';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import readMarkdown from '../../lib/read-markdown';
import NotesInfo from '../../public/notes.json';
interface Note {
title: string,
mtime: string,
}
interface Notes {
[slug: string]: Note;
}
function Markdown({ content }: any) {
return <ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
components={{
code({ node, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '')
return match
? (
<SyntaxHighlighter
showLineNumbers={true}
language={match[1]}
//@ts-ignore
style={hlTheme}
PreTag='div'
codeTagProps={{ style: { display: 'block' } }}
customStyle={{ padding: '0', borderRadius: '1rem' }}
{...props}
>{String(children).replace(/\n$/, '')}</SyntaxHighlighter>
)
: <code className={className} {...props}>
{children}
</code>
}
}}
>{content}</ReactMarkdown>
}
function Note({ note }: any) {
return (<>
<Layout >
<section className='block'>
<Markdown content={note.content} />
</section>
</Layout>
</>
);
}
export async function getStaticProps({ params }: any) {
const note: string = params.note;
const notesInfo: Notes = NotesInfo;
const noteInfo: Note = notesInfo[note];
return {
props: {
note: {
...noteInfo,
content: await readMarkdown('notes', note, true)
}
}
}
}
export async function getStaticPaths() {
return {
paths: Object.keys(NotesInfo).map((note: string) => {
return {
params: {
note
}
}
}),
fallback: false
};
}
export default Note;
+40
View File
@@ -0,0 +1,40 @@
import Link from 'next/link';
import Layout from '../../components/layout';
import date from '../../lib/date';
import NotesInfo from '../../public/notes.json';
function NoteEntry(props: { path: string, note: { title: string, mtime: string } }) {
return (
<tr>
<td style={{ flex: '1 0 50%' }}>
<Link href={props.path}>
{props.note.title}
</Link>
</td>
<td style={{ fontStyle: 'italic' }}>
{props.note.mtime && date.toRelativeDate(new Date(props.note.mtime))}
</td>
</tr>
);
}
function NotesPage() {
const notes = Object.entries(NotesInfo);
return (
<Layout>
{!notes || notes.length === 0 && <>No notes found</> || <table>
<tbody>
{notes.map(([slug, note]: any, i: number) => {
return <NoteEntry path={`/notes/${slug}`} note={note} key={i} />
})}
</tbody>
</table>}
</Layout>
)
}
export default NotesPage;
+66
View File
@@ -0,0 +1,66 @@
import Layout from '../../components/layout';
import ReactMarkdown from 'react-markdown';
import style from '../../styles/post.module.css';
import PostsInfo from '../../public/posts.json';
import readMarkdown from '../../lib/read-markdown';
interface Post {
title: string;
mtime: string;
otime?: string;
}
interface Posts {
[slug: string]: Post
}
function Post({ post }: { post: Post & { content: string, cover?: string } }) {
return (<>
<Layout removeContainer={true} >
{<div className={style.imageBlock}
style={{
backgroundImage:
post.cover ?
`url(/assets/images/${post.cover})` :
'linear-gradient(to bottom right, #565a0f, #08432c 15%, rgb(5, 39, 10) 40%, rgb(0, 22, 46) 80%)'
}}></div>}
<div className={style.spacer}></div>
<section className={`${style.block} block`}>
<div className='container'>
<ReactMarkdown>{post.content}</ReactMarkdown>
</div>
</section>
</Layout>
</>
);
}
export async function getStaticProps({ params }: any) {
const postsInfo: Posts = PostsInfo;
const post: Post = postsInfo[params.post];
return {
props: {
post: {
...post,
content: await readMarkdown('posts', params.post, true)
}
}
}
}
export async function getStaticPaths() {
return {
paths: Object.keys(PostsInfo).map((post: string) => {
return {
params: {
post
}
}
}),
fallback: false
};
}
export default Post;
+53
View File
@@ -0,0 +1,53 @@
import Link from 'next/link';
import Layout from '../../components/layout';
import date from '../../lib/date';
import PostsInfo from '../../public/posts.json';
function PostsPage() {
return (
<Layout>
{Object.keys(PostsInfo).length && <Posts /> || <NoPosts />}
</Layout>
)
}
function NoPosts() {
return (<><div className='text center'>
<div>**crickets**</div>
<div>No posts found...</div>
<div><Link href='/' className='link button green back'>Go Home</Link></div>
</div></>);
}
function Posts() {
const posts = Object.entries(PostsInfo);
return (
<>
<table>
<tbody>
{
posts.map(([slug, post]: any, i: number) => {
return <tr key={i} style={{ alignItems: 'center' }}>
<td style={{ display: 'inline-block', textAlign: 'right', fontSize: '0.9rem' }}>
<div style={{ fontStyle: 'italics', fontSize: '.8rem' }}>{
post.mtime && `Updated ${date.toRelativeDate(new Date(post.mtime))}`
}</div>
<div>{date.toRelativeDate(new Date(post.otime))}</div>
</td>
<td style={{
flex: '1 1 60%',
alignItems: 'center',
fontFamily: `'EB Garamond', 'Garamond', 'Times New Roman', Times, serif`
}}>
<Link href={`/posts/${slug}`} style={{ textDecoration: 'none' }}>{post.title}</Link>
</td>
</tr>
})}
</tbody>
</table>
</>
)
}
export default PostsPage;
+31
View File
@@ -0,0 +1,31 @@
import Link from 'next/link';
import Layout from '../components/layout';
import { Site } from '../lib/site';
import SiteMap from '../public/sitemap.json';
function traverseMap(head: Site, cwd = '', depth = 0) {
if (head.subpages === undefined)
return [];
let elements = [];
for (const [slug, info] of Object.entries(head.subpages)) {
const path = `${cwd}/${slug}`;
const children = (<><ul> {traverseMap(info, path, depth + 1)}</ul></>);
elements.push(<>
<li>
<Link className='button' href={path}>{info.title}</Link> {children}
</li>
</>);
}
return elements;
}
function SiteMapPage() {
return <Layout>
<ul>{traverseMap(SiteMap)}</ul>
</Layout>;
}
export default SiteMapPage;
+2 -2
View File
@@ -1,4 +1,4 @@
{ {
"Git": "https://git.paulw.xyz/xyz", "GitHub": "https://github.com/lambdapaul",
"Twitter/X": "https://x.com/paulw_xyz" "Twitter/X": "https://x.com/lambda_paul"
} }
+12 -12
View File
@@ -1,14 +1,14 @@
{ {
"posts": { "posts": {
"title": "Posts" "title": "Posts"
}, },
"notes": { "notes": {
"title": "Notes" "title": "Notes"
}, },
"about": { "about": {
"title": "About" "title": "About"
}, },
"sitemap": { "sitemap": {
"title": "Site Map" "title": "Site Map"
} }
} }
+1
View File
@@ -0,0 +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-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"}}
+1
View File
@@ -0,0 +1 @@
{}
+1
View File
@@ -0,0 +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"}}}
+80 -75
View File
@@ -1,131 +1,136 @@
const path = require('path') const fs = require('fs/promises');
const fs = require('fs/promises') const { createReadStream } = require('fs');
const path = require('path');
const gitRef = process.env.WWW_GIT_REF ?? 'master' const readline = require('readline/promises');
const giteaApiRepo = `https://git.paulw.xyz/api/v1/repos/xyz/www/` const { info } = require('console');
async function readFirstLines(filePath, lineCount = 1) { async function readFirstLines(filePath, lineCount = 1) {
const gitFileFetch = await fetch(`${giteaApiRepo}raw/${filePath}?ref=${gitRef}`) return new Promise((resolve, reject) => {
if (!gitFileFetch.ok) return null try {
const file = await gitFileFetch.text() const stream = createReadStream(filePath, 'utf-8');
const lines = file.split('\n') const rl = readline.createInterface({ input: stream });
const out = [] let counter = 0;
for (let i = 0; i < lineCount && i < lines.length; i++) { const lines = [];
out.push(lines[i]) rl.on('line', (line) => {
} counter++;
return out lines.push(line);
if (counter >= lineCount) {
rl.close();
rl.removeAllListeners();
}
});
rl.on('close', () => {
resolve(lines)
});
} catch (e) {
reject(e)
}
});
} }
async function getTitle(filePath) { async function getTitle(filePath) {
const firstLines = await readFirstLines(filePath) const firstLines = await readFirstLines(filePath);
if (firstLines === null || firstLines === undefined || firstLines.length === 0) return null if (firstLines === undefined || firstLines.length === 0)
let title = firstLines[0] return null;
let title = firstLines[0];
if (title.substring(0, 2) !== '# ') return null if (title.substring(0, 2) !== '# ')
return null;
title = title title = title
.substring(1, firstLines[0].length) .substring(1, firstLines[0].length)
.trim() .trim();
if (title.length < 3) if (title.length < 3)
return null return null;
return title return title;
} }
async function getMarkdownMetadata(dir) { async function getMarkdownMetadata(dir) {
const dirGitInfoFetch = await fetch(`${giteaApiRepo}contents/${dir}/?ref=${gitRef}`) const dirPath = path.join(process.cwd(), dir);
if (!dirGitInfoFetch.ok) return {} const files = (await fs.readdir(dirPath, 'utf-8'))
.filter((file) => {
const commits = {} return /^[^.].*.md$/.test(file);
const out = {} })
const dirGitInfo = await dirGitInfoFetch.json()
for (const file of dirGitInfo) {
if (file.name.startsWith('.') || !file.name.endsWith('.md')) continue
const title = await getTitle(file.path)
if (title === null) continue
const slug = file.name.replace(/\.md$/, '')
let mtime = new Date(); // better to have an incorrect recent date than the more incorrect unix time 0 (assuming the host doesn't have messed up clock)
if (!(file.last_commit_sha in commits)) { const out = {};
const lastCommitSha = await fetch(`${giteaApiRepo}/git/commits/${file.last_commit_sha}`) for (const file of files) {
if (lastCommitSha.ok) { const filePath = path.join(dirPath, file);
const commitJson = await lastCommitSha.json() const title = await getTitle(filePath);
commits[commitJson.sha] = (new Date(commitJson.created)) if (title === null)
} continue;
}
mtime = commits[file.last_commit_sha]
const slug = file.replace(/\.md$/, '');
// const pagePath = path.join('/', dir, slug);
out[slug] = { out[slug] = {
title: title, title: title,
mtime: mtime.toISOString(), // path: pagePath,
} mtime: (await fs.stat(filePath)).mtime,
};
} }
return out return out;
} }
async function readFilesMetadata(dir) { async function readFilesMetadata(dir) {
const filePath = jsonFilePath(dir) const filePath = jsonFilePath(dir);
try { try {
const fileContent = await fs.readFile(filePath, 'utf-8') const fileContent = await fs.readFile(filePath, 'utf-8');
const metadata = JSON.parse(fileContent) const metadata = JSON.parse(fileContent);
return metadata return metadata;
} catch { } catch {
return [] return [];
} }
} }
async function writeFilesMetadata(filePath, metadata) { async function writeFilesMetadata(filePath, metadata) {
try { try {
await fs.writeFile(filePath, JSON.stringify(metadata, null, 4), 'utf-8') await fs.writeFile(filePath, JSON.stringify(metadata), 'utf-8');
} catch (error) { } catch (error) {
console.error(error) console.error(error);
} }
} }
function jsonFilePath(dir) { function jsonFilePath(dir) {
return path.join(process.cwd(), 'public', `${dir}.json`); // ehh return path.join(process.cwd(), 'public', `${dir}.json`);
} }
async function generateNotesMetadata() { async function generateNotesMetadata() {
const dir = 'notes' const dir = 'notes';
await writeFilesMetadata(jsonFilePath(dir), await getMarkdownMetadata(dir)) await writeFilesMetadata(jsonFilePath(dir), await getMarkdownMetadata(dir));
} }
async function generatePostsMetadata() { async function generatePostsMetadata() {
const dir = 'posts' const dir = 'posts';
const currMetadata = await readFilesMetadata(dir) const currMetadata = await readFilesMetadata(dir);
const generatedMetadata = await getMarkdownMetadata(dir) const generatedMetadata = await getMarkdownMetadata(dir);
const newMetadata = {} const newMetadata = {};
for (const [name, data] of Object.entries(generatedMetadata)) { for (const [name, data] of Object.entries(generatedMetadata)) {
let otime = new Date() let otime;
if (currMetadata[name]?.otime !== undefined && currMetadata[name]?.otime !== null) if (currMetadata[name] !== undefined && currMetadata[name].otime !== undefined)
otime = currMetadata[name].otime ?? otime otime = currMetadata[name].otime
else else
otime = data.mtime ?? otime otime = data.mtime;
newMetadata[name] = { ...data, otime } newMetadata[name] = { ...data, otime }
} }
await writeFilesMetadata(jsonFilePath(dir), newMetadata) await writeFilesMetadata(jsonFilePath(dir), newMetadata);
} }
async function generateSiteMap() { async function generateSiteMap() {
await generateNotesMetadata() await generateNotesMetadata();
await generatePostsMetadata() await generatePostsMetadata();
const sitemap = { const sitemap = {
title: 'PaulW.XYZ', title: 'PaulW.XYZ',
pages: await readFilesMetadata('home') subpages: await readFilesMetadata('home')
} };
const pages = ['posts', 'notes'] const pages = ['posts', 'notes'];
for (const page of pages) { for (const page of pages) {
sitemap.pages[page].pages = await readFilesMetadata(page) sitemap.subpages[page].subpages = await readFilesMetadata(page);
} }
await writeFilesMetadata(jsonFilePath('sitemap'), sitemap) await writeFilesMetadata(jsonFilePath('sitemap'), sitemap);
} }
generateSiteMap() generateSiteMap();
Vendored
+5
View File
@@ -1,3 +1,8 @@
declare module '*.yaml' {
const record: Record<string, any>;
export default record;
}
declare module '*.md' { declare module '*.md' {
const rawmd: string; const rawmd: string;
export default rawmd; export default rawmd;
-43
View File
@@ -1,43 +0,0 @@
import ReactMarkdown from 'react-markdown';
import ReadmeMd from '../../../README.md';
import License from '../../../LICENSE.txt';
function AboutPage() {
return (
<>
<section className='block'>
<p>Paul&apos;s Personal Website.</p>
<p> You can find me on the following platforms:</p>
<ul>
<li>X/Twitter: <a href='https://x.com/paulw_xyz'>paulw_xyz</a></li>
<li>GitHub: <a href='https://github.com/paulwxyz'>paulwxyz</a></li>
{/* <li>BlueSky (unused): <a href='https://bsky.app/profile/@paulw.xyz'>@paulw.xyz</a></li> */}
<li><a href='https://git.paulw.xyz/xyz'>git.paulw.xyz</a></li>
</ul>
<p>
The original motivation was to just play with Next.js as it pretty much did the things I wanted web pages to do. But it came at the cost of needless complexity. As I use the JavaScript/ECMAScript/Whatever-you-want-to-call-it-script more and more, I am convinced that it is not a platform worth pursuing because the more complex it gets, the less control I have over what it does and this platform and its users seems to be okay with that sort of loss. I have been instead pivoting toward things that impressed and got me interested in working with computers.</p>
<p>Most services/products are keen on going against the <a href='https://stephango.com/file-over-app'>file over app</a> philosophy which entails prioritizing data over software and anticipate and embrace the eventual death of software. People instead want subscription services that barely support open formats and sometimes do not support exporting data to commonly used formats. The goal here is to avoid storing artifacts under locations that are easily not accessible, not under my control, and does not lock me out of using it with other software. The only reason I have not completely abandoned this is thanks to my decision to rely on Markdown files alone. Had it been reliant on any cloud software, I would have started over.</p>
<p>Got any questions, concerns, or issues? Contact me via email: <code>contact [at] paulw [dot] xyz</code>.</p>
</section>
<hr />
<section className='block'>
<p>Source for this site is available on GitHub: <a href='https://github.com/paulwxyz/www'>paulwxyz/www</a> and <a href='https://git.paulw.xyz/xyz/www'>git.paulw.xyz/xyz/www</a></p>
<p>Relevant information regarding the source is available on the repo and is also provided below.</p>
</section>
<section className='block'>
<h2>README</h2>
<ReactMarkdown>
{ReadmeMd.replace(/^#{6}\s+(.*)\s+$/gm, (s: string, a) => `**${a}**\n`).replace(/^#{1,5} /gm, (s: string) => { return `##${s}` })}
</ReactMarkdown>
</section>
<section className='block'>
<h2>LICENSE</h2>
<pre className='license'>{License}</pre>
</section>
</>
);
}
export default AboutPage;
-9
View File
@@ -1,9 +0,0 @@
export default function Container(props: { children?: React.ReactNode, ignore?: boolean }) {
if (props.ignore)
return <>{props.children}</>;
return (
<div className='container'>
{props.children}
</div>
);
}
-17
View File
@@ -1,17 +0,0 @@
'use client'
import Link from 'next/link';
import { toRelativeDate } from '../lib/date';
export function NoteEntry({ note }: { note: { title: string, mtime: string, slug: string } }) {
return (
<tr>
<td style={{ flex: '1 0 50%' }}>
<Link href={`/notes/${note.slug}`}>
{note.title}
</Link>
</td>
<td style={{ fontStyle: 'italic' }}>
{note.mtime && toRelativeDate(note.mtime)}
</td>
</tr>
);
}
-24
View File
@@ -1,24 +0,0 @@
import Link from 'next/link';
import Pages from '../../../public/external.json';
function QuickLinks() {
return (
<div className='block'>
{
Object.entries(Pages).map(([title, link]) => {
const extern = link.match(/^http/) && `blue extern` || '';
return (
<Link
key={link}
href={link}
className={`${extern} link button`}>
{title}
</Link>
);
})
}
</div>
);
}
export default QuickLinks;
-40
View File
@@ -1,40 +0,0 @@
import Link from "next/link";
import NotesInfo from '../../../public/notes.json';
function RecentNotes() {
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 (
<div className='block'>
<h2>Recent Notes</h2>
<ul>
{notes?.slice(0, 5)
.map(({slug, title, mtime}) => {
return (
<li key={slug} >
<Link href={`/notes/${slug}`}>{title}</Link>
</li>
);
})
}
{
notes.length > 5 &&
<Link href='/notes'>More...</Link>
}
</ul>
</div>
);
}
export default RecentNotes;
-50
View File
@@ -1,50 +0,0 @@
import Link from "next/link";
import { toRelativeDate } from "../lib/date";
import style from './recent-posts.module.css';
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() {
const posts = Object.entries(PostsInfo).reverse();
if (!posts.length)
return <></>;
return (
<div className='block'>
<h2>Recent Posts</h2>
<div className={style.container}>
{posts?.slice(0, 10)
.map(([slug, post]: any, i: number) => {
return (
<PostBlock
key={slug}
slug={slug}
title={post.title}
otime={post.otime} />
);
})}
</div>
{
posts.length > 10 &&
<div className={style.more}>
<Link href='/posts' >More...</Link>
</div>
}
</div>
);
}
export default RecentPosts;
-18
View File
@@ -1,18 +0,0 @@
import type {Metadata} from 'next'
import 'normalize.css'
import './global.css'
import Container from './components/container'
import Title from './components/title'
export default function RootLayout({children,}: Readonly<{children: React.ReactNode}>) {
return (
<html lang='en'>
<body>
<Title />
<Container>
{children}
</Container>
</body>
</html>
)
}
-132
View File
@@ -1,132 +0,0 @@
// getMonth() method ranges from 0-11 so no reason to account for it
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
function get12HourTime(pdate: Date | string): string {
const date = (typeof pdate === 'string') ? new Date(pdate) : pdate;
let hours = date.getHours();
const minutes = date.getMinutes();
let meridiem = 'A.M.';
let strhours = ''
if (hours > 12) {
hours -= 12;
meridiem = 'P.M.';
}
if (hours === 0)
hours = 12;
return `${hours}:${minutes < 10 ? '0' : ''}${minutes} ${meridiem}`;
}
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 month = months[oDate.getMonth()];
const day = oDate.getDate();
const suffix = getOrdinalDaySuffix(day)
let out = '';
out = !disable?.month ? `${month}` : '';
out = !disable?.day ? `${out} ${day}${suffix}` : out;
out = !disable?.year ? `${out}, ${year}` : out;
return out;
}
export function getOrdinalDaySuffix(day: number): string {
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 toLocaleString(pdate: Date | string): string {
const date = (typeof pdate === 'string') ? new Date(pdate) : pdate;
return `${toHumanReadableDate(date)} at ${get12HourTime(date)}`;
}
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 diff = now.getTime() - oDate.getTime();
let tdiff = Math.floor(diff / 1000);
if (tdiff < 0) {
return toHumanReadableDate(oDate);
}
if (tdiff < 60) {
return `${tdiff} seconds ago`;
}
tdiff = Math.floor(tdiff / 60);
if (tdiff < 60) {
return `${tdiff} minute${tdiff === 1 ? '' : 's'} ago`;
}
tdiff = Math.floor(tdiff / 60);
if (tdiff < 24) {
return `${tdiff} hour${tdiff === 1 ? '' : 's'} ago`;
}
if (tdiff < 48) {
return `Yesterday`;
}
if (oDate.getFullYear() != now.getFullYear())
return toHumanReadableDate(oDate);
return toHumanReadableDate(oDate, { year: true });
}
export function getFullMonth(month: number) {
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 = {
toRelativeDate,
getFullMonth,
isValid,
getOrdinalDaySuffix,
toLocaleString,
};
export default DateTool;
-35
View File
@@ -1,35 +0,0 @@
import Link from 'next/link';
import style from '../components/title.module.css';
function NotFoundPage() {
// TODO: figure out a way to somehow get next to ignore layout in special cases. tried /not-found/page.tsx but it doesn't work :X
return (
<>
{/* <head>
<title>404: Not Found | PaulW.XYZ</title>
</head>
<div className={style.container}>
<h1 className={style.title}>
Page Not Found
</h1>
</div>
<div className={`${style.nav} h1`}><Link href='/'>PaulW.XYZ</Link> / ... ??? / 404: Not Found</div>
<div className='container'>*/}
<section className='block text center'>
<h1>Error 404</h1>
<p>
<strong>Uh oh! The page you are looking for does not exist...</strong><br />
</p>
<Link href='/' className='button green back link'>Go Home</Link>
<a className='button blue link extern' href='https://en.wikipedia.org/wiki/List_of_HTTP_status_codes'>
More on HTTP status codes
</a>
</section>
{/*</div>*/}
</>
);
}
export default NotFoundPage;
-7
View File
@@ -1,7 +0,0 @@
.last-updated {
text-align: right;
display: block;
font-style: italic;
font-size: 1rem;
margin: 0.5rem 0.75rem;
}
-66
View File
@@ -1,66 +0,0 @@
import ReactMarkdown from 'react-markdown';
import { PluggableList } from 'unified';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import rehypeRaw from 'rehype-raw';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeHighlight from 'rehype-highlight';
import rehypeHighlightCodeLines, { type HighlightLinesOptions } from 'rehype-highlight-code-lines';
import readMarkdown from '../../lib/read-markdown';
import { toLocaleString } from '../../lib/date';
import NotesInfo from '../../../../public/notes.json';
import style from './note.module.css';
import 'highlight.js/styles/monokai-sublime.css';
import 'katex/dist/katex.min.css';
interface Note {
title: string,
mtime: string,
content?: string,
}
interface Notes {
[slug: string]: Note;
}
function Markdown({ content }: any) {
const remarkPlugins: PluggableList = [
remarkGfm,
remarkMath,
];
const rehypePlugins: PluggableList = [
rehypeSlug,
rehypeAutolinkHeadings,
rehypeRaw,
rehypeHighlight,
rehypeKatex,
];
return <ReactMarkdown remarkPlugins={remarkPlugins} rehypePlugins={rehypePlugins}>
{content}
</ReactMarkdown>
}
export default async function Note({params}: {params: Promise<{note: string}>}) {
const note = (await params).note
const n = await getNotes(note)
return (<>
<span className={style['last-updated']}>
Last updated: {toLocaleString(n.mtime)}
</span>
<section className='block'>
<Markdown content={n.content} />
</section>
</>
);
}
async function getNotes(name: string) {
const notesInfo: Notes = NotesInfo;
return {...notesInfo[name], content: await readMarkdown('notes', name, true)}
}
-39
View File
@@ -1,39 +0,0 @@
import NotesInfo from '../../../public/notes.json';
import { NoteEntry } from '../components/note-entry';
function NotesPage() {
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 (
<>
{
!notes || notes.length === 0
&& <>No notes found</>
|| <table>
<tbody>
{notes.map(
(note: any) => {
return (<NoteEntry note={note} key={note.slug} />);
}
)}
</tbody>
</table>
}
</>
)
}
export default NotesPage;
-33
View File
@@ -1,33 +0,0 @@
import React from 'react';
import Link from 'next/link';
import QuickLinks from './components/quick-links';
import RecentNotes from './components/recent-notes';
import RecentPosts from './components/recent-posts';
import RootInfo from '../../public/home.json';
function Nav() {
const nav = Object.entries(RootInfo);
return (
<div className='block'>
<h2>Navigation</h2>
{
nav.map(([slug, info]) => {
return <Link key={slug} href={slug} className='button green'>{info.title}</Link>
})
}
</div>
)
}
function HomePage() {
return (
<>
<QuickLinks />
<RecentPosts />
<RecentNotes />
<Nav />
</>
)
}
export default HomePage;
-85
View File
@@ -1,85 +0,0 @@
import ReactMarkdown from 'react-markdown';
import style from '../../styles/post.module.css';
import PostsInfo from '../../../../public/posts.json';
import readMarkdown from '../../lib/read-markdown';
import DateTool, { toLocaleString } from '../../lib/date';
interface IPost {
title: string;
mtime: string;
otime?: 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>
);
}
// post: IPost & { content: string, cover?: string, otime: string, mtime?: string }
export default async function Post({ params }: {params: Promise<{post: string}>}) {
const post = await getPost((await params).post);
if (!post)
return <></>;
return (<>
<div className='container'>
{ post.otime !== post.mtime && post.mtime &&
<span className={style.time}>
Last updated: {toLocaleString(post.mtime)}
</span>
}
<span className={style.time}>
{toLocaleString(post.otime)}
</span>
</div>
{<div className={style.imageBlock}
style={{
backgroundImage:
post.cover ?
`url(/assets/images/${post.cover})` :
'linear-gradient(to bottom right, rgb(5, 51, 11), rgb(5, 45, 13) 15%, rgb(5, 39,15) 40%, rgb(0, 30, 16) 80%)'
}}></div>}
<div className={`${style.spacer} ${post.cover ? style.background : ''}`}></div>
<section className={`${style.block} block`}>
<div className='container'>
<ReactMarkdown>{post.content}</ReactMarkdown>
</div>
</section>
<div className={style.spacer}></div>
</>
);
}
async function getPost(n: string) {
const postsInfo: Record<string, (IPost & { cover?: string, otime: string, mtime?: string })> = PostsInfo;
return {...postsInfo[n], content: await readMarkdown('posts', n, true)};
}
-49
View File
@@ -1,49 +0,0 @@
import Link from 'next/link';
import date from '../lib/date';
import PostsInfo from '../../../public/posts.json';
function PostsPage() {
return (<>
{Object.keys(PostsInfo).length && <Posts /> || <NoPosts />}
</>
)
}
function NoPosts() {
return (<div className='text center'>
<div>**crickets**</div>
<div>No posts found...</div>
<div><Link href='/' className='link button green back'>Go Home</Link></div>
</div>);
}
function Posts() {
const posts = Object.entries(PostsInfo);
return (
<table>
<tbody>
{
posts.map(([slug, post]: [string, any]) => {
return (<tr key={slug} style={{ alignItems: 'center' }}>
<td style={{ display: 'inline-block', textAlign: 'right', fontSize: '0.9rem' }}>
<div style={{ fontStyle: 'italics', fontSize: '.8rem' }}>{
post.mtime && (post.mtime != post.otime) && `Updated ${date.toRelativeDate(new Date(post.mtime))}`
}</div>
<div>{date.toRelativeDate(new Date(post.otime))}</div>
</td>
<td style={{
fontFamily: `'EB Garamond', 'Garamond', 'Times New Roman', Times, serif`
, fontSize: '1.25rem'
}}>
<Link href={`/posts/${slug}`} style={{ textDecoration: 'none' }}>{post.title}</Link>
</td>
</tr>)
})
}
</tbody>
</table>
)
}
export default PostsPage;
-41
View File
@@ -1,41 +0,0 @@
import Link from 'next/link';
import { Sites } from '../lib/site';
import SiteMap from '../../../public/sitemap.json';
function Desc(props: any) {
return (
<dl style={props.style}>
<dt>{props.term}</dt>
<dd>{props.details}</dd>
{props.children}
</dl>
);
}
function traverseMap(head?: Sites, cwd = '', depth = 0) {
if (!head) return [];
let elements = [];
for (const [slug, site] of Object.entries(head)) {
if (slug === 'sitemap')
continue;
let details;
let list;
const path = `${cwd}/${slug}`;
details = <Link href={path}>paulw.xyz{path}</Link>;
list = traverseMap(site.pages, path, depth + 1);
elements.push(<Desc style={{marginLeft: '3rem'}} key={site.title} term={site.title} details={details}>{list}</Desc>)
}
return elements;
}
function SiteMapPage() {
return <>
{traverseMap(SiteMap.pages)}
</>;
}
export default SiteMapPage;
+55
View File
@@ -0,0 +1,55 @@
.avatarContainer {
padding: 0.5rem;
text-align: center;
overflow: hidden;
}
.avatarContainer img {
border-radius: 1rem;
}
.cardLabel {
display: block;
font-weight: 700;
font-size: 1rem;
padding: .5rem;
}
.cardLabel:after {
content: ':';
}
.cardValue {
background-color: #303436;
font-size: 1rem;
padding: 0.25rem 1rem;
margin: 0.25rem 0;
border: none;
display: block;
border-radius: 0.5rem;
}
@media screen and (min-width: 800px) {
.card {
display: flex;
}
.cardTable {
flex: 1 1;
padding: 0.5rem;
align-items: stretch;
}
.cardRow {
display: flex;
}
.cardLabel {
flex: .2 1;
}
.cardValue {
flex: 1 1;
}
}
+21 -25
View File
@@ -17,7 +17,6 @@
src: url('/assets/fonts/Cantarell/Cantarell-Regular.otf') format('opentype'); src: url('/assets/fonts/Cantarell/Cantarell-Regular.otf') format('opentype');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: swap;
} }
@font-face { @font-face {
@@ -25,7 +24,6 @@
src: url('/assets/fonts/Cantarell/Cantarell-Thin.otf') format('opentype'); src: url('/assets/fonts/Cantarell/Cantarell-Thin.otf') format('opentype');
font-weight: 100; font-weight: 100;
font-style: normal; font-style: normal;
font-display: swap;
} }
@font-face { @font-face {
@@ -33,7 +31,6 @@
src: url('/assets/fonts/Cantarell/Cantarell-Light.otf') format('opentype'); src: url('/assets/fonts/Cantarell/Cantarell-Light.otf') format('opentype');
font-weight: 300; font-weight: 300;
font-style: normal; font-style: normal;
font-display: swap;
} }
@font-face { @font-face {
@@ -41,7 +38,6 @@
src: url('/assets/fonts/Cantarell/Cantarell-Bold.otf') format('opentype'); src: url('/assets/fonts/Cantarell/Cantarell-Bold.otf') format('opentype');
font-weight: bold; font-weight: bold;
font-style: normal; font-style: normal;
font-display: swap;
} }
@font-face { @font-face {
@@ -49,7 +45,6 @@
src: url('/assets/fonts/Cantarell/Cantarell-ExtraBold.otf') format('opentype'); src: url('/assets/fonts/Cantarell/Cantarell-ExtraBold.otf') format('opentype');
font-weight: 800; font-weight: 800;
font-style: normal; font-style: normal;
font-display: swap;
} }
@font-face { @font-face {
@@ -57,7 +52,6 @@
src: url('/assets/fonts/EB_Garamond/static/EBGaramond-Regular.ttf') format('truetype'); src: url('/assets/fonts/EB_Garamond/static/EBGaramond-Regular.ttf') format('truetype');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: swap;
} }
@font-face { @font-face {
@@ -65,7 +59,6 @@
src: url('/assets/fonts/EB_Garamond/static/EBGaramond-Bold.ttf') format('truetype'); src: url('/assets/fonts/EB_Garamond/static/EBGaramond-Bold.ttf') format('truetype');
font-weight: bold; font-weight: bold;
font-style: normal; font-style: normal;
font-display: swap;
} }
@font-face { @font-face {
@@ -73,39 +66,34 @@
src: url('/assets/fonts/EB_Garamond/static/EBGaramond-ExtraBold.ttf') format('truetype'); src: url('/assets/fonts/EB_Garamond/static/EBGaramond-ExtraBold.ttf') format('truetype');
font-weight: 800; font-weight: 800;
font-style: normal; font-style: normal;
font-display: swap;
} }
@font-face { @font-face {
font-family: 'Hack'; font-family: 'Hack';
src: url('/assets/fonts/Hack/hack-regular-subset.woff2') format('woff2'), url('/assets/fonts/Hack/hack-regular-subset.woff') format('woff'); src: url('/assets/fonts/Hack/hack-regular-subset.woff2?sha=3114f1256') format('woff2'), url('/assets/fonts/Hack/hack-regular-subset.woff?sha=3114f1256') format('woff');
font-weight: normal; font-weight: 400;
font-style: normal; font-style: normal;
font-display: swap;
} }
@font-face { @font-face {
font-family: 'Hack'; font-family: 'Hack';
src: url('/assets/fonts/Hack/hack-bold-subset.woff2') format('woff2'), url('/assets/fonts/Hack/hack-bold-subset.woff') format('woff'); src: url('/assets/fonts/Hack/hack-bold-subset.woff2?sha=3114f1256') format('woff2'), url('/assets/fonts/Hack/hack-bold-subset.woff?sha=3114f1256') format('woff');
font-weight: bold; font-weight: 700;
font-style: normal; font-style: normal;
font-display: swap;
} }
@font-face { @font-face {
font-family: 'Hack'; font-family: 'Hack';
src: url('/assets/fonts/Hack/hack-italic-subset.woff2') format('woff2'), url('/assets/fonts/Hack/hack-italic-webfont.woff') format('woff'); src: url('/assets/fonts/Hack/hack-italic-subset.woff2?sha=3114f1256') format('woff2'), url('/assets/fonts/Hack/hack-italic-webfont.woff?sha=3114f1256') format('woff');
font-weight: normal; font-weight: 400;
font-style: italic; font-style: italic;
font-display: swap;
} }
@font-face { @font-face {
font-family: 'Hack'; font-family: 'Hack';
src: url('/assets/fonts/Hack/hack-bolditalic-subset.woff2') format('woff2'), url('/assets/fonts/Hack/hack-bolditalic-subset.woff') format('woff'); src: url('/assets/fonts/Hack/hack-bolditalic-subset.woff2?sha=3114f1256') format('woff2'), url('/assets/fonts/Hack/hack-bolditalic-subset.woff?sha=3114f1256') format('woff');
font-weight: bold; font-weight: 700;
font-style: italic; font-style: italic;
font-display: swap;
} }
* { * {
@@ -235,8 +223,9 @@ code {
} }
table { table {
margin: 1rem auto; display: flex;
width:100%; flex-direction: column;
margin: 1rem 0;
overflow: hidden; overflow: hidden;
border-radius: 0.5rem; border-radius: 0.5rem;
} }
@@ -257,6 +246,11 @@ table tr:last-of-type
border-bottom-right-radius: 0.5rem; border-bottom-right-radius: 0.5rem;
} }
table thead tr,
table tbody tr {
display: flex;
}
table tbody tr:nth-of-type(2n) { table tbody tr:nth-of-type(2n) {
background-color: var(--table-even-color); background-color: var(--table-even-color);
} }
@@ -267,10 +261,12 @@ table tbody tr:nth-of-type(2n+1) {
table thead tr th, table thead tr th,
table tbody tr td { table tbody tr td {
display: flex;
flex: 1;
padding: .25rem 0.75rem; padding: .25rem 0.75rem;
} }
ul li { li {
list-style-type: square; list-style-type: square;
} }
@@ -288,9 +284,9 @@ ul li {
position: relative; position: relative;
} }
@media screen and (min-width: 818px) { @media screen and (min-width: 1018px) {
.container { .container {
max-width: 818px; max-width: 1018px;
margin: 0 auto; margin: 0 auto;
position: relative; position: relative;
} }
+39
View File
@@ -0,0 +1,39 @@
.desc {
font-size: 0.95rem;
}
.listItem {
padding: 0.25rem;
display: block;
}
.listItem::before {
content: '■';
padding: 0 0.5rem;
}
.listItemDesc {
display: inline-block;
padding: 0.25rem;
margin-left: 2.5rem;
font-size: 0.95rem;
}
.block .block {
margin: 0;
margin-left: 0.5rem;
border-radius: 0;
border-right: none;
border-bottom: none;
border-top: 1px dashed var(--main-border-color);
border-left: 1px dashed var(--main-border-color);
}
.block .block:first-of-type {
border-top: none;
}
.block .block:last-of-type {
border-bottom-left-radius: 1rem;
border-bottom: 1px dashed var(--main-border-color);
}
@@ -16,26 +16,14 @@
.block { .block {
font-family: 'EB Garamond', 'Garamond', 'Times New Roman', Times, serif; font-family: 'EB Garamond', 'Garamond', 'Times New Roman', Times, serif;
background-color: rgba(13, 17, 23, 0.97); background-color: rgba(13, 17, 23, 0.99);
margin: 0 auto; margin: 0 auto;
border-radius: 0; border-radius: 0;
font-size: 1.4rem; font-size: 1.25rem;
line-height: 2.5rem; line-height: 2rem;
padding: 2rem 1rem; padding: 2rem 1rem;
} }
.spacer { .spacer {
height: 6.25rem;
}
.background.spacer {
height: 25rem; height: 25rem;
} }
.time {
text-align: center;
display: block;
font-style: italic;
font-size: 1rem;
margin: 0.5rem 0.75rem;
}
@@ -30,16 +30,33 @@
} }
.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: 1rem; font-size: 0.9rem;
padding: .25rem 0.50rem; padding: .25rem 0.50rem;
} }
+9 -14
View File
@@ -1,7 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2023", "target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@@ -9,27 +13,18 @@
"noEmit": true, "noEmit": true,
"esModuleInterop": true, "esModuleInterop": true,
"module": "esnext", "module": "esnext",
"moduleResolution": "bundler", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "preserve",
"importHelpers": true, "importHelpers": true,
"incremental": true, "incremental": true
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
}, },
"include": [ "include": [
"next-env.d.ts", "next-env.d.ts",
"**/*.ts", "**/*.ts",
"**/*.tsx", "**/*.tsx",
"lib/slug.js", "lib/slug.js"
".next/types/**/*.ts"
], ],
"exclude": [ "exclude": [
"node_modules" "node_modules"