summaryrefslogtreecommitdiff
path: root/src/routes
diff options
context:
space:
mode:
Diffstat (limited to 'src/routes')
-rw-r--r--src/routes/+layout.svelte147
-rw-r--r--src/routes/+page.svelte115
-rw-r--r--src/routes/api/blog.json/+server.ts51
-rw-r--r--src/routes/api/cv.json/+server.ts75
-rw-r--r--src/routes/api/mastodon.json/+server.ts55
-rw-r--r--src/routes/api/remarks.json/+server.ts51
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;
+}