diff options
Diffstat (limited to 'src/routes')
| -rw-r--r-- | src/routes/+layout.svelte | 147 | ||||
| -rw-r--r-- | src/routes/+page.svelte | 115 | ||||
| -rw-r--r-- | src/routes/api/blog.json/+server.ts | 51 | ||||
| -rw-r--r-- | src/routes/api/cv.json/+server.ts | 75 | ||||
| -rw-r--r-- | src/routes/api/mastodon.json/+server.ts | 55 | ||||
| -rw-r--r-- | src/routes/api/remarks.json/+server.ts | 51 |
6 files changed, 494 insertions, 0 deletions
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..be635e8 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,147 @@ +<script lang="ts"> + //// import + import { browser } from "$app/environment"; + import { onMount } from "svelte"; + + //// util + import "./../../sass/global.scss"; + + //// var + const scrollControl = createScrollPreventer(); + + //// function + function horizontalScroll(event: WheelEvent): void { + const target = (event.target as HTMLElement).closest("section"); + + if (target && isVerticallyScrollable(target) && !isAtVerticalScrollLimit(target, event.deltaY) && isFullyInView(target)) + return; /// ignore vertically scrollable elements + + if (event.deltaY !== 0) { + event.preventDefault(); + window.scrollBy({ left: event.deltaY }); + } + } + + function isAtVerticalScrollLimit(element: HTMLElement, deltaY: number) { + if (!element || !deltaY) + return false; + + return (deltaY > 0 && Math.abs(element.scrollTop + element.clientHeight - element.scrollHeight) < 1) || + (deltaY < 0 && element.scrollTop <= 0); + } + + function isFullyInView(element: HTMLElement) { + if (!element) + return false; + + const rect = element.getBoundingClientRect(); + + return ( + rect.top >= 0 && + rect.left >= 24 && // 0 + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); + } + + function isVerticallyScrollable(element: HTMLElement) { + if (!element) + return false; + + return element.scrollHeight > element.clientHeight; + } + + function createScrollPreventer(): { prevent: () => void; allow: () => void } { + return { + prevent: () => { + window.removeEventListener("wheel", horizontalScroll); + }, + allow: () => { + window.addEventListener("wheel", horizontalScroll, { passive: false }); + } + }; + } + + /// ready + onMount(() => { + browser && scrollControl.allow(); + }); +</script> + +<style lang="scss"> + div { + width: fit-content; height: 100%; + + display: flex; + white-space: nowrap; + } + + aside { + width: 100%; height: calc(var(--line-height) * 2); + bottom: 0; left: 0; + + align-items: center; + display: flex; + font-size: 0.8rem; + margin: 0; + padding-left: calc(var(--padding) * 3); + position: fixed; + text-transform: lowercase; + + > * { + display: block; + margin-right: calc(var(--padding) * 2); + position: relative; + user-select: none; + + &:not(:last-child) { + &::after { + content: "/"; + font-weight: normal; + opacity: 0.15; + position: absolute; + right: calc(var(--padding) * -1.5); + } + } + } + + em { + white-space: nowrap; + } + + img { + image-rendering: pixelated; + } + } + + :global(a) { + transition: none; + + &:hover { + color: var(--uchu-yin-4); + text-decoration: underline var(--uchu-yin-2); + } + } +</style> + +<div> + <slot></slot> +</div> + +<aside> + <strong>Contact</strong> + <span>paul@webb.page</span> + <!-- <a href="https://netopwibby.socii.network" target="_blank">socii@netopwibby</a> --> + <a href="https://social.coop/@netopwibby" target="_blank">@netopwibby@social.coop</a> + <a href="https://blacksky.community/profile/webb.page" target="_blank">@webb.page</a> + <em>scroll vertically to scroll horizontally</em> + <img alt="" src="/88x31/a.gif"/> + <img alt="" src="/88x31/b.gif"/> + <img alt="" src="/88x31/c.gif"/> + <a class="banner-wrapper" href="https://www.webb.page"> + <img alt="" src="/88x31/d.png"/> + </a> + <a class="banner-wrapper" href="https://ellesho.me/page/?ref=https://webb.page"> + <img alt="" src="/88x31/elle.webp"/> + </a> +</aside> diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..a0f1a90 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,115 @@ +<script lang="ts"> + //// import + import { version } from "../../package.json"; + + //// component + import About from "$lib/component/About.svelte"; + import Blog from "$lib/component/Blog.svelte"; + import CV from "$lib/component/CV.svelte"; + import Music from "$lib/component/Music.svelte"; + import MyMusic from "$lib/component/MyMusic.svelte"; + import Remarks from "$lib/component/Remarks.svelte"; + import Project from "$lib/component/Project.svelte"; + import Promo from "$lib/component/Promo.svelte"; + import Ring from "$lib/component/Ring.svelte"; + import Social from "$lib/component/Social.svelte"; + import Support from "$lib/component/Support.svelte"; + import Uses from "$lib/component/Uses.svelte"; +</script> + +<style lang="scss"> + :root { + --color-border: var(--uchu-yin-2); + } + + section { + max-width: 700px; + overflow-y: auto; + + @media (min-width: 601px) { + min-width: 500px; + width: 45vw; + } + + @media (max-width: 600px) { + width: 75vw; + } + + &:not(:last-of-type) { + margin-right: 30px; + } + + &:last-of-type { + margin-right: var(--line-height); + } + } + + .full-height { + height: 100%; + } + + .with-background { + background-color: var(--uchu-gray-2); + overflow-x: hidden; + } +</style> + +<svelte:head> + <meta name="version" content={version}/> +</svelte:head> + +<section> + <aside class="with-background"> + <About/> + </aside> +</section> + +<section> + <aside class="with-background"> + <Support/> + </aside> + + <aside class="with-background"> + <CV/> + </aside> +</section> + +<section> + <aside class="with-background"> + <Remarks/> + </aside> + + <aside class="with-background"> + <Blog/> + </aside> +</section> + +<section> + <aside class="with-background"> + <Social/> + </aside> + + <aside class="with-background"> + <Promo/> + </aside> + + <aside class="with-background"> + <Uses/> + </aside> +</section> + +<section class="full-height with-background"> + <Project/> +</section> + +<section class="full-height with-background"> + <MyMusic/> +</section> + +<section class="full-height with-background"> + <Ring/> +</section> + +<section class="full-height with-background"> + <Music/> +</section> diff --git a/src/routes/api/blog.json/+server.ts b/src/routes/api/blog.json/+server.ts new file mode 100644 index 0000000..f416647 --- /dev/null +++ b/src/routes/api/blog.json/+server.ts @@ -0,0 +1,51 @@ + + + +/*** IMPORT ------------------------------------------- ***/ + +import { error, json } from "@sveltejs/kit"; + +/*** EXPORT ------------------------------------------- ***/ + +export const POST = async({ fetch, request }) => { + try { + const { filename } = await request.json(); + + const response = await fetch(`https://blog.webb.page/${String(filename)}`, { + headers: { "Content-Type": "text/plain" }, + method: "GET" + }); + + const memo = parseMemo(await response.text()); + memo.push(` <span class="special-char">[</span><a href="https://blog.webb.page/${String(filename).split(".txt")[0]}">READ</a><span class="special-char">]</span>`, "\n"); + + return json({ content: memo.join("\n") }); + } catch(welp) { + console.error(`Error fetching memo: ${String(welp)}`); + return error(500); + } +}; + +/*** HELPER ------------------------------------------- ***/ + +function parseMemo(text: string): string { + const intro = text.split(/^Body$/m)[0]; + const lines = intro.split("\n").filter(Boolean); + const format = [""]; + let firstLine = ""; + + lines.map((line, index) => { + if (index === 1) + firstLine = " " + line.split(/\s+\B/)[1].trim(); + + if (index === 2) { + firstLine += ` • ${line.trim()}`; + format.push(firstLine); + } + + if (index === 4) + format.push(" " + line.trim()); + }); + + return format; +} diff --git a/src/routes/api/cv.json/+server.ts b/src/routes/api/cv.json/+server.ts new file mode 100644 index 0000000..956525f --- /dev/null +++ b/src/routes/api/cv.json/+server.ts @@ -0,0 +1,75 @@ + + + +/*** IMPORT ------------------------------------------- ***/ + +import { error, json } from "@sveltejs/kit"; + +/*** EXPORT ------------------------------------------- ***/ + +export const POST = async({ fetch, request }) => { + try { + const response = await fetch("https://cv.webb.page", { + headers: { "Content-Type": "text/plain" }, + method: "GET" + }); + + const content = await response.text(); + return json({ content: processCV(content) }); + } catch(welp) { + console.error(`Error fetching CV: ${String(welp)}`); + return error(500); + } +}; + +/*** HELPER ------------------------------------------- ***/ + +function processCV(text) { + const sections = text.split(/(^░▒▓[\s\S]*?^---$)/m); + const intro = sections[1]; + const body = sections[2]; + const lines = body.split(/\n{2,}/); + const table = processTable(body); + + const processedLines = lines.map(line => { + if (line.includes("Proficiency") && line.includes("Notes")) + return table.join("").trim(); + + return line.replace(/\s+/g, " "); + }); + + return intro + processedLines.join("\n\n") + "\n\n\n"; +} + +function processTable(text) { + const lines = text.split(/\r?\n/); + const tableStartIndex = lines.findIndex(line => line.includes("|")); + + if (tableStartIndex === -1) + return null; + + const separatorLineIndex = lines.findIndex((line, index) => index > tableStartIndex && line.includes("|-") || line.includes("| -")); + + if (separatorLineIndex === -1) + return null; + + let tableEndIndex = separatorLineIndex + 1; + + while (tableEndIndex < lines.length && lines[tableEndIndex].includes("|")) { + tableEndIndex++; + } + + const contentRows = lines.slice(separatorLineIndex + 1, tableEndIndex); + + return contentRows.map(row => { + let cells = row.split("|").map(cell => cell.trim()); + + if (cells[0] === "") + cells.shift(); + + if (cells[cells.length - 1] === "") + cells.pop(); + + return cells.join(" / ") + "\n"; + }); +} diff --git a/src/routes/api/mastodon.json/+server.ts b/src/routes/api/mastodon.json/+server.ts new file mode 100644 index 0000000..72cbd38 --- /dev/null +++ b/src/routes/api/mastodon.json/+server.ts @@ -0,0 +1,55 @@ + + + +/*** IMPORT ------------------------------------------- ***/ + +import { error, json } from "@sveltejs/kit"; + +/*** EXPORT ------------------------------------------- ***/ + +export const POST = async({ fetch }) => { + try { + const response = await fetch("https://social.coop/users/netopwibby/outbox?page=true", { + headers: { "Content-Type": "application/json" }, + method: "GET" + }); + + const { orderedItems } = await response.json(); + const { object } = findCreateType(orderedItems); + + return json({ + created: object.published, + content: object.content, + link: object.url, + media: processAttachments(object.attachment) + }); + } catch(welp) { + console.error(`Error fetching latest Mastodon post: ${String(welp)}`); + return error(500); + } +}; + +/*** HELPER ------------------------------------------- ***/ + +function findCreateType(arr: Array<{ [key: string]: any }>): { [key: string]: any } | undefined { + return arr.find(obj => obj.type === "Create"); +} + +function processAttachments(attachments: Array<{ [key: string]: any }>): Array<string> { + const media = []; + + if (attachments) { + for (const attachment of attachments) { + // TODO + // : use blurhash given to us by mastodon + // : https://github.com/woltapp/blurhash/tree/master/TypeScript#example + // : `attachment` comes with blurhash, height, mediaType, width + // if (attachment && attachment.url) + + // @ts-ignore | Argument of type "any" is not assignable to parameter of type "never". + media.push(attachment.url); + } + } + + return media; +} diff --git a/src/routes/api/remarks.json/+server.ts b/src/routes/api/remarks.json/+server.ts new file mode 100644 index 0000000..c708207 --- /dev/null +++ b/src/routes/api/remarks.json/+server.ts @@ -0,0 +1,51 @@ + + + +/*** IMPORT ------------------------------------------- ***/ + +import { error, json } from "@sveltejs/kit"; + +/*** EXPORT ------------------------------------------- ***/ + +export const POST = async({ fetch, request }) => { + try { + const { filename } = await request.json(); + + const response = await fetch("https://blog.webb.page/remarks/" + String(filename), { + headers: { "Content-Type": "text/plain" }, + method: "GET" + }); + + const remark = parseRemark(await response.text()); + remark.push(` <span class="special-char">[</span><a href="https://blog.webb.page/remarks/${String(filename).split(".txt")[0]}">READ</a><span class="special-char">]</span>`, "\n"); + + return json({ content: remark.join("\n") }); + } catch(welp) { + console.error(`Error fetching remark: ${String(welp)}`); + return error(500); + } +}; + +/*** HELPER ------------------------------------------- ***/ + +function parseRemark(text: string): string { + const intro = text.split(/^Body$/m)[0]; + const lines = intro.split("\n").filter(Boolean); + const format = [""]; + let firstLine = ""; + + lines.map((line, index) => { + if (index === 1) + firstLine = " " + line.split(/\s+\B/)[1].trim(); + + if (index === 2) { + firstLine += ` • ${line.trim()}`; + format.push(firstLine); + } + + if (index === 4) + format.push(" " + line.trim()); + }); + + return format; +} |
