mirror of
https://github.com/mat-1/matdoesdev.git
synced 2025-08-02 14:46:04 +00:00
buttons
This commit is contained in:
parent
f6b9db6160
commit
6b08b2bbed
15 changed files with 769 additions and 26 deletions
Binary file not shown.
68
Caddyfile
68
Caddyfile
|
@ -1,25 +1,25 @@
|
|||
(https_redirect) {
|
||||
@do_https_redirect {
|
||||
not header_regexp veryoldbrowser User-Agent Navigator|MSIE|Mosaic
|
||||
protocol http
|
||||
}
|
||||
redir @do_https_redirect https://{host}{uri}
|
||||
@do_https_redirect {
|
||||
not header_regexp veryoldbrowser User-Agent Navigator|MSIE|Mosaic|Kindle
|
||||
protocol http
|
||||
}
|
||||
redir @do_https_redirect https://{host}{uri}
|
||||
}
|
||||
|
||||
(gif_redirect) {
|
||||
@do_not_gif_redirect {
|
||||
not header_regexp oldweb_today Origin http://localhost:10001
|
||||
@do_not_gif_redirect {
|
||||
not header_regexp oldweb_today Origin http://localhost:10001
|
||||
not header_regexp oldweb_today Origin oldweb.today
|
||||
not header_regexp veryveryoldbrowser User-Agent Navigator|MSIE|NCSA_Mosaic
|
||||
}
|
||||
vars not_gif_redirect false
|
||||
vars @do_not_gif_redirect not_gif_redirect true
|
||||
@do_gif_redirect {
|
||||
expression `{vars.not_gif_redirect} == false`
|
||||
path_regexp ^/retro/.*\.png$
|
||||
}
|
||||
not header_regexp veryveryoldbrowser User-Agent Navigator|MSIE|NCSA_Mosaic
|
||||
}
|
||||
vars not_gif_redirect false
|
||||
vars @do_not_gif_redirect not_gif_redirect true
|
||||
@do_gif_redirect {
|
||||
expression `{vars.not_gif_redirect} == false`
|
||||
path_regexp ^/retro/.*\.png$
|
||||
}
|
||||
|
||||
uri @do_gif_redirect path_regexp \.png$ .gif
|
||||
uri @do_gif_redirect path_regexp \.png$ .gif
|
||||
}
|
||||
|
||||
matdoes.dev:80 matdoes.dev:443 http://matctazmu565vivubva3p3bulaneangiff47xmnezzjx2nuinwjoxjyd.onion {
|
||||
|
@ -33,21 +33,21 @@ matdoes.dev:80 matdoes.dev:443 http://matctazmu565vivubva3p3bulaneangiff47xmnezz
|
|||
# block chrome but not chromium-based browsers
|
||||
@chrome {
|
||||
header_regexp chrome User-Agent Chrome\/[0-9./]+\s(Mobile\s)?Safari\/[0-9./]+$
|
||||
not header User-Agent *Googlebot/*
|
||||
not path /dot_git/*
|
||||
not header_regexp not_chrome User-Agent Googlebot/|eightyeightthirtyone
|
||||
not path /dot_git/*
|
||||
}
|
||||
respond @chrome "This site is best viewed with Firefox (or literally any browser that isn't Chrome).
|
||||
respond @chrome "This site is best viewed with Firefox (or any browser that isn't Chrome).
|
||||
|
||||
If you're unable to use Firefox, you can also access this website via SSH, Gemini, Gopher, Finger, Telnet, and some others." 403
|
||||
root * /www
|
||||
|
||||
# easter egg that makes old browsers show the retro page
|
||||
@retro_redirect {
|
||||
path /
|
||||
header_regexp oldbrowser User-Agent PaleMoon|Trident|MSIE|Netscape|Navigator
|
||||
}
|
||||
header_regexp oldbrowser User-Agent PaleMoon|Trident|MSIE|Netscape|Navigator
|
||||
}
|
||||
redir @retro_redirect /retro 302
|
||||
|
||||
root * /www
|
||||
file_server {
|
||||
precompressed br gzip
|
||||
}
|
||||
|
@ -93,6 +93,18 @@ If you're unable to use Firefox, you can also access this website via SSH, Gemin
|
|||
# .git easter egg
|
||||
uri /.git/* replace .git dot_git 1
|
||||
|
||||
route /buttons/88x31.* {
|
||||
uri strip_prefix /buttons
|
||||
file_server {
|
||||
root /opt/x227f
|
||||
}
|
||||
}
|
||||
handle_path /buttons/i/* {
|
||||
try_files {path} {path}.png {path}.gif {path}.jpg {path}.webp {path}.avif {path}.bmp
|
||||
root * /opt/x227f/buttons
|
||||
file_server
|
||||
}
|
||||
|
||||
handle_errors {
|
||||
@should_be_404 {
|
||||
expression {http.error.status_code} == 404
|
||||
|
@ -112,13 +124,12 @@ If you're unable to use Firefox, you can also access this website via SSH, Gemin
|
|||
file_server @is_451 {
|
||||
status 451
|
||||
}
|
||||
|
||||
|
||||
rewrite @should_be_404 /404.html
|
||||
file_server @is_not_451
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
(matrix_media_proxy) {
|
||||
handle /_matrix/media/*/download/matdoes.dev/discord_* {
|
||||
header Access-Control-Allow-Origin *
|
||||
|
@ -181,7 +192,10 @@ f.matdoes.dev {
|
|||
reverse_proxy 127.0.0.1:4000
|
||||
}
|
||||
git.matdoes.dev {
|
||||
reverse_proxy 127.0.0.1:3000
|
||||
reverse_proxy 127.0.0.1:3000
|
||||
}
|
||||
s.matdoes.dev {
|
||||
reverse_proxy 127.0.0.1:28019
|
||||
}
|
||||
|
||||
mail.matdoes.dev {
|
||||
|
@ -208,3 +222,7 @@ hetzner.matdoes.dev {
|
|||
}
|
||||
respond "uwu" 402
|
||||
}
|
||||
|
||||
xmpp.matdoes.dev {
|
||||
respond ""
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
"@sveltejs/adapter-static": "^2.0.3",
|
||||
"@sveltejs/kit": "1.27.7",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"cbor-x": "^1.5.6",
|
||||
"cookie": "^0.6.0",
|
||||
"html-minifier": "^4.0.0",
|
||||
"patch-package": "^8.0.0",
|
||||
|
|
2
src/routes/buttons/+layout.server.ts
Normal file
2
src/routes/buttons/+layout.server.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const ssr = false
|
||||
export const prerender = false
|
62
src/routes/buttons/+layout.svelte
Normal file
62
src/routes/buttons/+layout.svelte
Normal file
|
@ -0,0 +1,62 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import { writable } from 'svelte/store'
|
||||
import './app.css'
|
||||
import { buttonIndexFromHash, pageIndexFromName } from './88x31'
|
||||
|
||||
$: selectedPage = $page.url.pathname.split('/').pop()
|
||||
|
||||
let selectedButtonHash = writable<string | null>(null)
|
||||
let selectedPageName = writable<string | null>(null)
|
||||
|
||||
page.subscribe(async (page) => {
|
||||
await new Promise((r) => requestAnimationFrame(r))
|
||||
const hash = page.url.hash.slice(1)
|
||||
// if the hash has a . then it's a page name
|
||||
if (hash === '') {
|
||||
selectedButtonHash.set(null)
|
||||
selectedPageName.set(null)
|
||||
} else if (hash.includes('.')) {
|
||||
selectedButtonHash.set(null)
|
||||
selectedPageName.set(hash)
|
||||
} else {
|
||||
selectedButtonHash.set(hash)
|
||||
selectedPageName.set(null)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<nav>
|
||||
<a href="/buttons" class:selected={selectedPage === 'buttons'}>List</a>
|
||||
<a
|
||||
href="/buttons/degrees{$selectedPageName ? `#${$selectedPageName}` : ''}"
|
||||
class:selected={selectedPage === 'degrees'}>Degrees of separation</a
|
||||
>
|
||||
<!-- <a href="/buttons/stats" class:selected={selectedPage === 'stats'}>Stats</a> -->
|
||||
</nav>
|
||||
<nav class="source">
|
||||
<a href="https://github.com/mat-1/x227f">Source</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
nav {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
238
src/routes/buttons/+page.svelte
Normal file
238
src/routes/buttons/+page.svelte
Normal file
|
@ -0,0 +1,238 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { buttonIndexFromHash, buttonUrlFromIndex, data, pageIndexFromName } from './88x31'
|
||||
import { writable } from 'svelte/store'
|
||||
import { page } from '$app/stores'
|
||||
import ButtonLink from './ButtonLink.svelte'
|
||||
import ExternalLinkIcon from './ExternalLinkIcon.svelte'
|
||||
import ExternalLink from './ExternalLink.svelte'
|
||||
|
||||
let searchQuery = writable('')
|
||||
let sort = writable('relevance')
|
||||
|
||||
let visibleButtons = new Set<string>()
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
visibleButtons.add(entry.target.id)
|
||||
} else {
|
||||
visibleButtons.delete(entry.target.id)
|
||||
}
|
||||
})
|
||||
|
||||
visibleButtons = visibleButtons
|
||||
})
|
||||
|
||||
let buttonsEl: HTMLDivElement
|
||||
|
||||
// when a new button is added, observe it
|
||||
const buttonContainerObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node instanceof HTMLDivElement && node.classList.contains('button-container')) {
|
||||
observer.observe(node)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
let refs: HTMLDivElement[] = []
|
||||
onMount(() => {
|
||||
buttonContainerObserver.observe(buttonsEl, {
|
||||
childList: true,
|
||||
})
|
||||
// observe initial buttons
|
||||
refs.forEach((ref) => {
|
||||
observer.observe(ref)
|
||||
})
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
refs.forEach((ref) => {
|
||||
observer.unobserve(ref)
|
||||
})
|
||||
buttonContainerObserver.disconnect()
|
||||
})
|
||||
|
||||
let matchingTextIndexes = new Set<number>()
|
||||
|
||||
let buttonEntries: [number, string][] = [...data.buttons.entries()]
|
||||
|
||||
searchQuery.subscribe(updateSearch)
|
||||
sort.subscribe(updateSearch)
|
||||
|
||||
function updateSearch() {
|
||||
const value = $searchQuery
|
||||
const sortValue = $sort
|
||||
|
||||
const newMatchingTextIndexes = new Set<number>()
|
||||
if (value !== '') {
|
||||
for (let textIndex = 0; textIndex < data.texts.length; textIndex++) {
|
||||
if (data.texts[textIndex].toLowerCase().includes(value.toLowerCase())) {
|
||||
newMatchingTextIndexes.add(textIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
matchingTextIndexes = newMatchingTextIndexes
|
||||
|
||||
// filter buttonEntries
|
||||
const newButtonEntriesWithScore: [number, [number, string]][] = []
|
||||
|
||||
for (let buttonIndex = 0; buttonIndex < data.button_names.length; buttonIndex++) {
|
||||
const textIndexes = data.button_names[buttonIndex]
|
||||
if (value === '' || textIndexes.some((textIndex) => matchingTextIndexes.has(textIndex))) {
|
||||
// lower score is better
|
||||
let score: number
|
||||
if (sortValue === 'relevance') {
|
||||
if (value === '') {
|
||||
// relevance search doesn't make sense if there's no query
|
||||
score = 0
|
||||
} else {
|
||||
// shortest text index
|
||||
const textIndexLengths = textIndexes
|
||||
.map((textIndex) => data.texts[textIndex])
|
||||
.filter((text) => {
|
||||
return text.toLowerCase().includes(value.toLowerCase())
|
||||
})
|
||||
.map((text) => text.length)
|
||||
score = Math.min(...textIndexLengths)
|
||||
}
|
||||
} else if (sortValue === 'popularity') {
|
||||
score = 1 / data.button_backlinks[buttonIndex].length
|
||||
} else if (sortValue === 'random') {
|
||||
score = Math.random()
|
||||
} else {
|
||||
throw new Error(`Unknown sort value: ${sortValue}`)
|
||||
}
|
||||
newButtonEntriesWithScore.push([score, [buttonIndex, data.buttons[buttonIndex]]])
|
||||
}
|
||||
}
|
||||
const newButtonEntries = newButtonEntriesWithScore
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.map((entry) => entry[1])
|
||||
buttonEntries = newButtonEntries
|
||||
}
|
||||
|
||||
let selectedButtonHash = writable<string | null>(null)
|
||||
$: selectedButtonIndex =
|
||||
$selectedButtonHash === null ? null : buttonIndexFromHash($selectedButtonHash)
|
||||
let selectedPageName = writable<string | null>(null)
|
||||
$: selectedPageIndex = $selectedPageName === null ? null : pageIndexFromName($selectedPageName)
|
||||
|
||||
page.subscribe(async (page) => {
|
||||
// this is to work around a sveltekit bug that makes it click the hash twice, which clicks the wrong link the second time
|
||||
await new Promise((r) => requestAnimationFrame(r))
|
||||
|
||||
// hash
|
||||
const hash = page.url.hash.slice(1)
|
||||
// if the hash has a . then it's a page name
|
||||
if (hash === '') {
|
||||
selectedButtonHash.set(null)
|
||||
selectedPageName.set(null)
|
||||
} else if (hash.includes('.')) {
|
||||
selectedButtonHash.set(null)
|
||||
selectedPageName.set(hash)
|
||||
} else {
|
||||
selectedButtonHash.set(hash)
|
||||
selectedPageName.set(null)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if selectedButtonIndex !== null}
|
||||
<h1>
|
||||
<img src={buttonUrlFromIndex(selectedButtonIndex)} alt="Button" class="button" />
|
||||
</h1>
|
||||
|
||||
<p>Links to ({data.button_links[selectedButtonIndex].length}):</p>
|
||||
<ul>
|
||||
{#each data.button_links[selectedButtonIndex] as pageIndex, i}
|
||||
<li>
|
||||
<ExternalLink {pageIndex} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<p>Linked from ({data.button_backlinks[selectedButtonIndex].length}):</p>
|
||||
<ul>
|
||||
{#each data.button_backlinks[selectedButtonIndex] as pageIndex, i}
|
||||
<li>
|
||||
<ExternalLink {pageIndex} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else if selectedPageIndex !== null}
|
||||
<h1>
|
||||
{$selectedPageName}
|
||||
<a href="https://{data.pages[selectedPageIndex]}">
|
||||
<ExternalLinkIcon />
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
<p>Buttons ({data.links[selectedPageIndex].length}):</p>
|
||||
<div class="normal-button-grid">
|
||||
{#each data.links[selectedPageIndex] as linkedPageIndex, i}
|
||||
<ButtonLink
|
||||
pageIndex={linkedPageIndex}
|
||||
buttonIndex={data.link_buttons[selectedPageIndex][i]}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<p>Linked from ({data.backlinks[selectedPageIndex].length}):</p>
|
||||
<div class="normal-button-grid">
|
||||
{#each data.backlinks[selectedPageIndex] as backlinkedPageIndex, i}
|
||||
<ButtonLink
|
||||
pageIndex={backlinkedPageIndex}
|
||||
buttonIndex={data.backlink_buttons[selectedPageIndex][i]}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class:hidden={selectedButtonIndex !== null || selectedPageIndex !== null}>
|
||||
<input type="text" placeholder="Search" bind:value={$searchQuery} />
|
||||
<select bind:value={$sort}>
|
||||
<option value="relevance">Relevance</option>
|
||||
<option value="popularity">Popularity</option>
|
||||
<option value="random">Random</option>
|
||||
</select>
|
||||
|
||||
<p><b>{buttonEntries.length.toLocaleString()}</b> buttons</p>
|
||||
|
||||
<div class="compact-button-grid" bind:this={buttonsEl}>
|
||||
{#each buttonEntries as [index, buttonHash] (buttonHash)}
|
||||
<div class="button-container" id={buttonHash} bind:this={refs[index]}>
|
||||
{#if visibleButtons.has(buttonHash)}
|
||||
<a href="/buttons#{buttonHash}">
|
||||
<img src={buttonUrlFromIndex(index)} alt="Button" class="button" />
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.compact-button-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.normal-button-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: inline-block;
|
||||
width: 88px;
|
||||
height: 31px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
62
src/routes/buttons/88x31.ts
Normal file
62
src/routes/buttons/88x31.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { decode } from 'cbor-x/decode'
|
||||
|
||||
interface Data {
|
||||
pages: string[]
|
||||
buttons: string[]
|
||||
texts: string[]
|
||||
|
||||
button_file_exts: string[]
|
||||
button_names: number[][]
|
||||
button_links: number[][]
|
||||
button_backlinks: number[][]
|
||||
|
||||
links: number[][]
|
||||
link_buttons: number[][]
|
||||
link_button_alts: (number | null)[][]
|
||||
link_button_titles: (number | null)[][]
|
||||
|
||||
backlinks: number[][]
|
||||
backlink_buttons: number[][]
|
||||
}
|
||||
|
||||
const res = await fetch('https://matdoes.dev/buttons/88x31.cbor')
|
||||
const buffer = await res.arrayBuffer()
|
||||
export const data: Data = decode(new Uint8Array(buffer))
|
||||
|
||||
export function buttonUrlFromIndex(index: number) {
|
||||
const hash = buttonHashFromIndex(index)
|
||||
const ext = data.button_file_exts[index]
|
||||
return `https://matdoes.dev/buttons/i/${hash}.${ext}`
|
||||
}
|
||||
|
||||
export function buttonUrlFromHash(hash: string) {
|
||||
return `https://matdoes.dev/buttons/i/${hash}`
|
||||
}
|
||||
|
||||
export function buttonHashFromIndex(index: number) {
|
||||
return data.buttons[index]
|
||||
}
|
||||
|
||||
function binarySearch<T>(arr: T[], key: T): number | null {
|
||||
let low = 0
|
||||
let high = arr.length - 1
|
||||
|
||||
while (low <= high) {
|
||||
const mid = (low + high) >>> 1
|
||||
const midVal = arr[mid]
|
||||
|
||||
if (midVal < key) low = mid + 1
|
||||
else if (midVal > key) high = mid - 1
|
||||
else return mid
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function pageIndexFromName(name: string): number | null {
|
||||
return binarySearch(data.pages, name)
|
||||
}
|
||||
|
||||
export function buttonIndexFromHash(hash: string): number | null {
|
||||
return binarySearch(data.buttons, hash)
|
||||
}
|
25
src/routes/buttons/ButtonLink.svelte
Normal file
25
src/routes/buttons/ButtonLink.svelte
Normal file
|
@ -0,0 +1,25 @@
|
|||
<script lang="ts">
|
||||
import { buttonHashFromIndex, buttonUrlFromIndex, data } from './88x31'
|
||||
import ExternalLink from './ExternalLink.svelte'
|
||||
import ExternalLinkIcon from './ExternalLinkIcon.svelte'
|
||||
|
||||
export let pageIndex: number
|
||||
export let buttonIndex: number
|
||||
</script>
|
||||
|
||||
<div class="button-with-link-container">
|
||||
{#if pageIndex !== null}
|
||||
<ExternalLink {pageIndex} />
|
||||
{/if}
|
||||
<a href="/buttons#{buttonHashFromIndex(buttonIndex)}">
|
||||
<img src={buttonUrlFromIndex(buttonIndex)} alt="Button" class="button" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.button-with-link-container {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
max-width: 100px;
|
||||
}
|
||||
</style>
|
26
src/routes/buttons/ExternalLink.svelte
Normal file
26
src/routes/buttons/ExternalLink.svelte
Normal file
|
@ -0,0 +1,26 @@
|
|||
<script lang="ts">
|
||||
import { data } from './88x31'
|
||||
import ExternalLinkIcon from './ExternalLinkIcon.svelte'
|
||||
|
||||
export let pageIndex: number
|
||||
|
||||
function cutOff(str: string, length: number) {
|
||||
if (str.length <= length) return str
|
||||
return str.slice(0, length - 1) + '…'
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<a href="/buttons#{data.pages[pageIndex]}">{cutOff(data.pages[pageIndex], 32)}</a>
|
||||
<a href="https://{data.pages[pageIndex]}">
|
||||
<ExternalLinkIcon />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
a {
|
||||
word-break: break-all;
|
||||
overflow: hidden;
|
||||
width: fit-content;
|
||||
}
|
||||
</style>
|
12
src/routes/buttons/ExternalLinkIcon.svelte
Normal file
12
src/routes/buttons/ExternalLinkIcon.svelte
Normal file
|
@ -0,0 +1,12 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path
|
||||
d="M432 320H400a16 16 0 0 0 -16 16V448H64V128H208a16 16 0 0 0 16-16V80a16 16 0 0 0 -16-16H48A48 48 0 0 0 0 112V464a48 48 0 0 0 48 48H400a48 48 0 0 0 48-48V336A16 16 0 0 0 432 320zM488 0h-128c-21.4 0-32.1 25.9-17 41l35.7 35.7L135 320.4a24 24 0 0 0 0 34L157.7 377a24 24 0 0 0 34 0L435.3 133.3 471 169c15 15 41 4.5 41-17V24A24 24 0 0 0 488 0z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
svg {
|
||||
fill: currentColor;
|
||||
height: 0.8em;
|
||||
}
|
||||
</style>
|
After Width: | Height: | Size: 491 B |
31
src/routes/buttons/app.css
Normal file
31
src/routes/buttons/app.css
Normal file
|
@ -0,0 +1,31 @@
|
|||
body {
|
||||
background-color: #1e1e2e;
|
||||
color: #cdd6f4;
|
||||
/* serif is chosen intentionally as it makes the website look worse */
|
||||
font-family: serif;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select {
|
||||
background-color: #313244;
|
||||
color: #cdd6f4;
|
||||
border: 1px solid #9399b2;
|
||||
border-radius: 4px;
|
||||
padding: 0.25em;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #89b4fa;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 88px;
|
||||
height: 31px;
|
||||
image-rendering: pixelated;
|
||||
}
|
169
src/routes/buttons/degrees/+page.svelte
Normal file
169
src/routes/buttons/degrees/+page.svelte
Normal file
|
@ -0,0 +1,169 @@
|
|||
<script lang="ts">
|
||||
import { writable } from 'svelte/store'
|
||||
import { pageIndexFromName, data } from '../88x31'
|
||||
import ButtonLink from '../ButtonLink.svelte'
|
||||
import { onMount } from 'svelte'
|
||||
import { page } from '$app/stores'
|
||||
|
||||
let originPage = writable('')
|
||||
let targetPage = writable('')
|
||||
|
||||
let originPageId: number | null
|
||||
let targetPageId: number | null
|
||||
|
||||
let pageAndButtonIndexes: [number, number][] | null = []
|
||||
|
||||
function calculatePath() {
|
||||
originPageId = pageIndexFromName($originPage)
|
||||
targetPageId = pageIndexFromName($targetPage)
|
||||
|
||||
if (originPageId === null || targetPageId === null) return
|
||||
|
||||
// determine the button for site 1 (since we won't find it by following links here)
|
||||
const originButtonIndexes: number[] = data.backlink_buttons[originPageId]
|
||||
const originButtonIndex = originButtonIndexes.sort(
|
||||
(a, b) =>
|
||||
originButtonIndexes.filter((v) => v === a).length -
|
||||
originButtonIndexes.filter((v) => v === b).length
|
||||
)[0]
|
||||
|
||||
if (originPageId === targetPageId) {
|
||||
pageAndButtonIndexes = [[originPageId, originButtonIndex]]
|
||||
return
|
||||
}
|
||||
|
||||
// breadth first search
|
||||
let nextQueue = [originPageId]
|
||||
let visited = new Set<number>()
|
||||
let found = false
|
||||
let cameFrom = new Map<number, number>()
|
||||
|
||||
while (nextQueue.length > 0) {
|
||||
let queue = nextQueue
|
||||
nextQueue = []
|
||||
|
||||
for (let pageId of queue) {
|
||||
if (pageId === targetPageId) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
||||
for (let link of data.links[pageId]) {
|
||||
if (link === null) continue
|
||||
if (visited.has(link)) continue
|
||||
visited.add(link)
|
||||
nextQueue.push(link)
|
||||
cameFrom.set(link, pageId)
|
||||
}
|
||||
}
|
||||
|
||||
if (found) break
|
||||
}
|
||||
|
||||
console.log('found', found)
|
||||
|
||||
if (!found) {
|
||||
pageAndButtonIndexes = null
|
||||
return
|
||||
}
|
||||
|
||||
console.log('targetPageId', targetPageId)
|
||||
|
||||
let nextItem: number = targetPageId
|
||||
let current: number = cameFrom.get(nextItem)!
|
||||
|
||||
const targetBacklinkIndex = data.links[current]?.indexOf(nextItem)
|
||||
const targetButtonIndex =
|
||||
targetBacklinkIndex === undefined ? -1 : data.link_buttons[current][targetBacklinkIndex]
|
||||
|
||||
// reconstruct path
|
||||
let pageAndButtonIndexesReversed: [number, number][] = [[targetPageId, targetButtonIndex]]
|
||||
|
||||
while (current !== originPageId) {
|
||||
nextItem = cameFrom.get(current)!
|
||||
|
||||
const backlinkIndex = data.links[nextItem]?.indexOf(current)
|
||||
const buttonIndex =
|
||||
backlinkIndex === undefined ? -1 : data.link_buttons[nextItem][backlinkIndex]
|
||||
|
||||
pageAndButtonIndexesReversed.push([current, buttonIndex])
|
||||
|
||||
current = nextItem
|
||||
}
|
||||
|
||||
pageAndButtonIndexesReversed.push([originPageId, originButtonIndex])
|
||||
|
||||
pageAndButtonIndexes = pageAndButtonIndexesReversed.reverse()
|
||||
|
||||
localStorage.setItem('88x31-degrees-originPage', $originPage)
|
||||
localStorage.setItem('88x31-degrees-targetPage', $targetPage)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
return page.subscribe(async (page) => {
|
||||
const hash = decodeURIComponent(page.url.hash.slice(1))
|
||||
let [origin, target] = hash.split('→')
|
||||
|
||||
if (origin === undefined) origin = ''
|
||||
if (target === undefined) target = ''
|
||||
|
||||
if (origin === '') origin = localStorage.getItem('88x31-degrees-originPage') ?? ''
|
||||
if (target === '') target = localStorage.getItem('88x31-degrees-targetPage') ?? ''
|
||||
|
||||
if (origin !== '' && $originPage !== origin) originPage.set(origin)
|
||||
if (target !== '' && $targetPage !== target) targetPage.set(target)
|
||||
})
|
||||
})
|
||||
|
||||
originPage.subscribe(calculatePath)
|
||||
targetPage.subscribe(calculatePath)
|
||||
|
||||
originPage.subscribe(updateHash)
|
||||
targetPage.subscribe(updateHash)
|
||||
|
||||
function updateHash() {
|
||||
history.replaceState(null, '', `#${$originPage}→${$targetPage}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
bind:value={$originPage}
|
||||
placeholder="Origin page"
|
||||
class:invalid={originPageId === null}
|
||||
/>
|
||||
→
|
||||
<input
|
||||
type="text"
|
||||
bind:value={$targetPage}
|
||||
placeholder="Target page"
|
||||
class:invalid={targetPageId === null}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{#if originPageId !== null && targetPageId !== null}
|
||||
{#if pageAndButtonIndexes === null}
|
||||
<p><b>No path :(</b></p>
|
||||
{:else}
|
||||
<p>
|
||||
{#if pageAndButtonIndexes.length - 1 === 1}
|
||||
<b>1</b> degree of separation
|
||||
{:else}
|
||||
<b>{pageAndButtonIndexes.length - 1}</b> degrees of separation
|
||||
{/if}
|
||||
</p>
|
||||
{#each pageAndButtonIndexes as [pageIndex, buttonIndex], i}
|
||||
{#if i > 0}
|
||||
<span class="arrow">→</span>
|
||||
{/if}
|
||||
<ButtonLink {pageIndex} {buttonIndex} />
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.invalid {
|
||||
border-color: red;
|
||||
}
|
||||
</style>
|
0
src/routes/buttons/stats/+page.svelte
Normal file
0
src/routes/buttons/stats/+page.svelte
Normal file
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"module": "es2020",
|
||||
"module": "es2022",
|
||||
"lib": ["es2020", "DOM", "WebWorker"],
|
||||
"target": "es2020",
|
||||
/**
|
||||
|
|
97
yarn.lock
97
yarn.lock
|
@ -22,6 +22,48 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cbor-extract/cbor-extract-darwin-arm64@npm:2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "@cbor-extract/cbor-extract-darwin-arm64@npm:2.1.1"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cbor-extract/cbor-extract-darwin-x64@npm:2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "@cbor-extract/cbor-extract-darwin-x64@npm:2.1.1"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cbor-extract/cbor-extract-linux-arm64@npm:2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "@cbor-extract/cbor-extract-linux-arm64@npm:2.1.1"
|
||||
conditions: os=linux & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cbor-extract/cbor-extract-linux-arm@npm:2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "@cbor-extract/cbor-extract-linux-arm@npm:2.1.1"
|
||||
conditions: os=linux & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cbor-extract/cbor-extract-linux-x64@npm:2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "@cbor-extract/cbor-extract-linux-x64@npm:2.1.1"
|
||||
conditions: os=linux & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cbor-extract/cbor-extract-win32-x64@npm:2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "@cbor-extract/cbor-extract-win32-x64@npm:2.1.1"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@emnapi/runtime@npm:^0.44.0":
|
||||
version: 0.44.0
|
||||
resolution: "@emnapi/runtime@npm:0.44.0"
|
||||
|
@ -1275,6 +1317,49 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cbor-extract@npm:^2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "cbor-extract@npm:2.1.1"
|
||||
dependencies:
|
||||
"@cbor-extract/cbor-extract-darwin-arm64": "npm:2.1.1"
|
||||
"@cbor-extract/cbor-extract-darwin-x64": "npm:2.1.1"
|
||||
"@cbor-extract/cbor-extract-linux-arm": "npm:2.1.1"
|
||||
"@cbor-extract/cbor-extract-linux-arm64": "npm:2.1.1"
|
||||
"@cbor-extract/cbor-extract-linux-x64": "npm:2.1.1"
|
||||
"@cbor-extract/cbor-extract-win32-x64": "npm:2.1.1"
|
||||
node-gyp: "npm:latest"
|
||||
node-gyp-build-optional-packages: "npm:5.0.3"
|
||||
dependenciesMeta:
|
||||
"@cbor-extract/cbor-extract-darwin-arm64":
|
||||
optional: true
|
||||
"@cbor-extract/cbor-extract-darwin-x64":
|
||||
optional: true
|
||||
"@cbor-extract/cbor-extract-linux-arm":
|
||||
optional: true
|
||||
"@cbor-extract/cbor-extract-linux-arm64":
|
||||
optional: true
|
||||
"@cbor-extract/cbor-extract-linux-x64":
|
||||
optional: true
|
||||
"@cbor-extract/cbor-extract-win32-x64":
|
||||
optional: true
|
||||
bin:
|
||||
download-cbor-prebuilds: bin/download-prebuilds.js
|
||||
checksum: e7471f9ad421d352d60079faa63234ea7795d4ae64ce617a49a5f3b82a1a95e81c141f75bc1d7c0ae3d7dca924a78f9109aab5ee2a2113830bf67705c08839d0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cbor-x@npm:^1.5.6":
|
||||
version: 1.5.6
|
||||
resolution: "cbor-x@npm:1.5.6"
|
||||
dependencies:
|
||||
cbor-extract: "npm:^2.1.1"
|
||||
dependenciesMeta:
|
||||
cbor-extract:
|
||||
optional: true
|
||||
checksum: c9ac318e4a47bccae73f7e697a28a5738670edac87f9df5bd673cd9ca9a35eae9fe7897b25773a5d2577208f5fc0cdbb9d688a2de0b1db6d3352f503f6e3efcd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chalk@npm:^4.0.0, chalk@npm:^4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "chalk@npm:4.1.2"
|
||||
|
@ -2565,6 +2650,7 @@ __metadata:
|
|||
"@types/js-yaml": "npm:^4.0.9"
|
||||
"@typescript-eslint/eslint-plugin": "npm:^6.13.2"
|
||||
"@typescript-eslint/parser": "npm:^6.13.2"
|
||||
cbor-x: "npm:^1.5.6"
|
||||
cookie: "npm:^0.6.0"
|
||||
eslint: "npm:^8.55.0"
|
||||
eslint-config-prettier: "npm:^9.1.0"
|
||||
|
@ -2821,6 +2907,17 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-gyp-build-optional-packages@npm:5.0.3":
|
||||
version: 5.0.3
|
||||
resolution: "node-gyp-build-optional-packages@npm:5.0.3"
|
||||
bin:
|
||||
node-gyp-build-optional-packages: bin.js
|
||||
node-gyp-build-optional-packages-optional: optional.js
|
||||
node-gyp-build-optional-packages-test: build-test.js
|
||||
checksum: 334336bdefb398469a115a2c9d4c141d28e093fd703be7adc1448f9dd3e1b5525281a789be8a60a778c91212daaa310155b1908b1fb9a987cec61a9fe04d774a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-gyp@npm:latest":
|
||||
version: 10.0.0
|
||||
resolution: "node-gyp@npm:10.0.0"
|
||||
|
|
Loading…
Add table
Reference in a new issue