ok i should actually save this
47
LICENSE
Normal file
|
@ -0,0 +1,47 @@
|
|||
THE KARKAT PUBLIC LICENSE (KKPL)
|
||||
Version 2.2, January 2024
|
||||
|
||||
Copyright (C) 2023-2024 MeowcaTheoRange
|
||||
<me@abtmtr.link>
|
||||
|
||||
THIS LICENSE GRANTS PERMISSION TO ANY
|
||||
TROLL, HUMAN, GROUP, OR ANY OTHER
|
||||
LEGALLY RECOGNIZED ENTITY ("YOU") TO
|
||||
MODIFY, USE, AND DISTRIBUTE
|
||||
("SCHMEEVE", "SCHMEEVING") THIS WORK
|
||||
WITHIN THE DESIRED SCOPE DEFINED BELOW,
|
||||
AS LONG AS THE KARKAT PUBLIC LICENSE
|
||||
PERSISTS WITHIN.
|
||||
|
||||
BY SCHMEEVING THIS WORK, YOU AGREE TO
|
||||
BE BOUND BY THIS LICENSE'S TERMS AND
|
||||
CONDITIONS:
|
||||
|
||||
0. YOU WON'T BE A WUSS ABOUT THIS
|
||||
LICENSE.
|
||||
1. YOU WILL ONLY SCHMEEVE WITHIN THE
|
||||
WORK'S DESIRED SCOPE FOR THIS
|
||||
LICENSE.
|
||||
2. YOU ACKNOWLEDGE THAT THE DEFINED
|
||||
SCOPE OF THIS WORK COMES WITH NO
|
||||
WARRANTY.
|
||||
3. YOU FROND PROMISE THAT YOU WON'T
|
||||
SUE IF YOUR COMPUTER EXPLODES
|
||||
BECAUSE OF THIS PROGRAM.
|
||||
4. YOU AGREE THAT KARKAT IS THE BEST
|
||||
HACKER ON ALTERNIA.
|
||||
5. YOU AGREE THAT BY WRITING IN THE
|
||||
FORM BELOW, YOU ARE THE RIGHTFUL
|
||||
OWNER OF THIS WORK.
|
||||
|
||||
WHAT PARTS OF THE WORK DOES THIS DAMNED
|
||||
LICENSE APPLY TO:
|
||||
All of it, except /public.
|
||||
WRITE THE WORK'S NAME HERE:
|
||||
abtmtr.link
|
||||
(Version 9, April 2024)
|
||||
WRITE THE NAME(S) OF THE WORK'S
|
||||
AUTHOR(S) HERE:
|
||||
MeowcaTheoRange <me@abtmtr.link>
|
||||
|
||||
FUCK FUCK FUCK FUCK FUCK FUCK FUCK.
|
|
@ -1,4 +1,6 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
const nextConfig = {
|
||||
trailingSlash: true
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
1570
package-lock.json
generated
|
@ -9,14 +9,17 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "14.1.4",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"next": "14.1.4"
|
||||
"react-markdown": "^9.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18"
|
||||
"@types/react-dom": "^18",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
|
BIN
public/88x31/acidicalchemist_neocities_org.gif
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
public/88x31/arimelody_me.gif
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
public/88x31/esoteric/gnu-linux.gif
Normal file
After Width: | Height: | Size: 550 B |
BIN
public/88x31/esoteric/html.gif
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
public/88x31/esoteric/ublock.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
public/88x31/esoteric/vivaldi.gif
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
public/88x31/esoteric/xkcd.gif
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
public/88x31/freeplay_floof_company.png
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
public/88x31/invoxiplaygames_uk.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
public/88x31/ioletsgo_gay.gif
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
public/88x31/jaiden_sh.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
public/88x31/mae_wtf.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
public/88x31/micro_pages_gay.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
public/88x31/moth_monster.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
public/88x31/owenzimmerman_com.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
public/88x31/sneexy_pages_gay.gif
Normal file
After Width: | Height: | Size: 690 B |
BIN
public/88x31/translunar_academy.png
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
public/88x31/whois_slipfox_xyz.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
public/fonts/Lexend Deca/Variable.ttf
Normal file
BIN
public/fonts/Material Symbols/Variable.ttf
Normal file
BIN
public/fonts/OpenDyslexic/Bold.otf
Normal file
BIN
public/fonts/OpenDyslexic/BoldItalic.otf
Normal file
BIN
public/fonts/OpenDyslexic/Italic.otf
Normal file
BIN
public/fonts/OpenDyslexic/Regular.otf
Normal file
BIN
public/fonts/Renogare/Regular.otf
Normal file
BIN
public/headers/about.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
public/headers/blog.png
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
public/headers/characters.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
public/headers/gallery.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
public/headers/links.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
public/headers/oao.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
public/headers/projects.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
public/headers/stories.png
Normal file
After Width: | Height: | Size: 28 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
Before Width: | Height: | Size: 1.3 KiB |
96
public/robots.txt
Normal file
|
@ -0,0 +1,96 @@
|
|||
# https://seirdy.one/robots.txt
|
||||
|
||||
User-agent: *
|
||||
Disallow: /noindex/
|
||||
Disallow: /misc/
|
||||
|
||||
# I opt out of online advertising so malware that injects ads on my site won't get paid.
|
||||
# You should do the same. my ads.txt file contains a standard placeholder to forbid any
|
||||
# compliant ad networks from paying for ad placement on my domain.
|
||||
User-Agent: Adsbot
|
||||
Disallow: /
|
||||
Allow: /ads.txt
|
||||
Allow: /app-ads.txt
|
||||
|
||||
## IP-violation scanners ##
|
||||
|
||||
# The next three are borrowed from https://www.videolan.org/robots.txt
|
||||
|
||||
# > This robot collects content from the Internet for the sole purpose of # helping educational institutions prevent plagiarism. [...] we compare student papers against the content we find on the Internet to see if we # can find similarities. (http://www.turnitin.com/robot/crawlerinfo.html)
|
||||
# --> fuck off.
|
||||
User-Agent: TurnitinBot
|
||||
Disallow: /
|
||||
|
||||
# > NameProtect engages in crawling activity in search of a wide range of brand and other intellectual property violations that may be of interest to our clients. (http://www.nameprotect.com/botinfo.html)
|
||||
# --> fuck off.
|
||||
User-Agent: NPBot
|
||||
Disallow: /
|
||||
|
||||
# iThenticate is a new service we have developed to combat the piracy of intellectual property and ensure the originality of written work for# publishers, non-profit agencies, corporations, and newspapers. (http://www.slysearch.com/)
|
||||
# --> fuck off.
|
||||
User-Agent: SlySearch
|
||||
Disallow: /
|
||||
|
||||
# BLEXBot assists internet marketers to get information on the link structure of sites and their interlinking on the web, to avoid any technical and possible legal issues and improve overall online experience. (http://webmeup-crawler.com/)
|
||||
# --> fuck off.
|
||||
User-Agent: BLEXBot
|
||||
Disallow: /
|
||||
|
||||
# Providing Intellectual Property professionals with superior brand protection services by artfully merging the latest technology with expert analysis. (https://www.checkmarknetwork.com/spider.html/)
|
||||
# "The Internet is just way to big to effectively police alone." (ACTUAL quote)
|
||||
# --> fuck off.
|
||||
User-agent: CheckMarkNetwork/1.0 (+https://www.checkmarknetwork.com/spider.html)
|
||||
Disallow: /
|
||||
|
||||
# Stop trademark violations and affiliate non-compliance in paid search. Automatically monitor your partner and affiliates’ online marketing to protect yourself from harmful brand violations and regulatory risks. We regularly crawl websites on behalf of our clients to ensure content compliance with brand and regulatory guidelines. (https://www.brandverity.com/why-is-brandverity-visiting-me)
|
||||
# --> fuck off.
|
||||
User-agent: BrandVerity/1.0
|
||||
Disallow: /
|
||||
|
||||
## Misc. icky stuff ##
|
||||
|
||||
# Pipl assembles online identity information from multiple independent sources to create the most complete picture of a digital identity and connect it to real people and their offline identity records. When all the fragments of online identity data are collected, connected, and corroborated, the result is a more trustworthy identity.
|
||||
# --> fuck off.
|
||||
User-agent: PiplBot
|
||||
Disallow: /
|
||||
|
||||
## Gen-AI data scrapers ##
|
||||
|
||||
# Eat shit, OpenAI.
|
||||
User-agent: ChatGPT-User
|
||||
Disallow: /
|
||||
User-agent: GPTBot
|
||||
Disallow: /
|
||||
|
||||
# Official way to opt-out of Google's generative AI training:
|
||||
# <https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers>
|
||||
User-agent: Google-Extended
|
||||
Disallow: /
|
||||
|
||||
# There isn't any public documentation for this AFAICT.
|
||||
# Reuters thinks this works so I might as well give it a shot.
|
||||
User-agent: anthropic-ai
|
||||
Disallow: /
|
||||
User-agent: Claude-Web
|
||||
Disallow: /
|
||||
|
||||
# FacebookBot crawls public web pages to improve language models for our speech recognition technology.
|
||||
# <https://developers.facebook.com/docs/sharing/bot/?_fb_noscript=1>
|
||||
User-Agent: FacebookBot
|
||||
Disallow: /
|
||||
|
||||
# I'm not blocking CCBot for now. It publishes a free index for anyone to use.
|
||||
# Googe used this to train the initial version of Bard (now called Gemini).
|
||||
# I allow CCBot since its index is also used for upstart/hobbyist search engines
|
||||
# like Alexandria and for genuinely useful academic work I personally like.
|
||||
# I allow Owler for similar reasons:
|
||||
# <https://openwebsearch.eu/owler/#owler-opt-out>
|
||||
# <https://openwebsearch.eu/common-goals-with-common-crawl/>.
|
||||
# Omgilibot/Omgili is similar to CCBot, except it sells the scrape results.
|
||||
# I'm not familiar enough with Omgili to make a call here.
|
||||
# In the long run, my embedded robots meta-tags and headers could cover gen-AI
|
||||
|
||||
# I don't block cohere-ai or Perplexitybot: they don't appear to actually scrape data for LLM training purposes. The crawling powers search engines with integrated pre-trained LLMs.
|
||||
# TODO: investigate whether YouBot scrapes to train its own in-house LLM.
|
||||
|
||||
Sitemap: https://seirdy.one/sitemap.xml
|
BIN
public/sfx/s_alternative.mp3
Normal file
BIN
public/sfx/s_busy.mp3
Normal file
BIN
public/sfx/s_happy.mp3
Normal file
BIN
public/sfx/s_hover.mp3
Normal file
BIN
public/sfx/s_impossible.mp3
Normal file
BIN
public/sfx/s_notice.mp3
Normal file
BIN
public/sfx/s_open.mp3
Normal file
BIN
public/sfx/s_possible_alt.mp3
Normal file
BIN
public/sfx/s_sad.mp3
Normal file
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
Before Width: | Height: | Size: 629 B |
BIN
public/welcome.png
Normal file
After Width: | Height: | Size: 54 KiB |
17
src/app/about/page.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { MainLayout } from "@/layout/MainLayout/MainLayout";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
<MainLayout currentPage="/about/" title="About" subtitle="abtmtr.link v9">
|
||||
<img style={{
|
||||
marginTop: 16
|
||||
}} src="/headers/about.png" alt="ABOUT"></img>
|
||||
<h1>abtmtr.link</h1>
|
||||
<p>Created by <b>MeowcaTheoRange</b></p>
|
||||
<p>Licensed under the <Link href="https://git.abtmtr.link/MeowcaTheoRange/KarkatPublicLicense/src/branch/master/LICENSE-v2.2" target="_blank">Karkat Public License (KKPL) v2.2</Link>.</p>
|
||||
<p><Link href="https://www.creativefabrica.com/product/renogare/" target="_blank">Renogare</Link> font by Graphite</p>
|
||||
<p><Link href="https://fonts.google.com/specimen/Lexend+Deca" target="_blank">Lexend Deca</Link> font from Google Fonts</p>
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
27
src/app/blog/[slug]/page.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { MainLayout } from "@/layout/MainLayout/MainLayout";
|
||||
import { notFound } from "next/navigation";
|
||||
import Markdown from 'react-markdown';
|
||||
import rehypeRaw from "rehype-raw";
|
||||
|
||||
export default async function Home({
|
||||
params
|
||||
}:{
|
||||
params: {
|
||||
slug: string
|
||||
}
|
||||
}) {
|
||||
const blog = await fetch(`https://blog.abtmtr.link/api/collections/mtr/posts/${params.slug}`).then(x=>x.json());
|
||||
if (blog.data == null)
|
||||
return notFound();
|
||||
return (
|
||||
<MainLayout currentPage={`/blog/${params.slug}`} title="Blog" subtitle="via blog.abtmtr.link" backButton>
|
||||
<h1>{blog.data.title}</h1>
|
||||
<p><small>Posted {new Date(blog.data.created).toLocaleString()} - {blog.data.views} views</small></p>
|
||||
<div style={{
|
||||
margin: "16px 0"
|
||||
}}>
|
||||
<Markdown rehypePlugins={[rehypeRaw]}>{blog.data.body}</Markdown>
|
||||
</div>
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
57
src/app/blog/page.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { ConditionalNull } from "@/components/utility/Conditional";
|
||||
import { MainLayout } from "@/layout/MainLayout/MainLayout";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import Markdown from "react-markdown";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
|
||||
export default async function Home({
|
||||
searchParams
|
||||
}: {
|
||||
searchParams: {
|
||||
page: string
|
||||
}
|
||||
}) {
|
||||
const curPage = parseInt(searchParams?.page) || 1;
|
||||
|
||||
const blogs = await fetch(`https://blog.abtmtr.link/api/collections/mtr/posts?page=${curPage}`).then(x=>x.json());
|
||||
return (
|
||||
<MainLayout currentPage="/blog/" title="Blog" subtitle="via blog.abtmtr.link">
|
||||
<img style={{
|
||||
marginTop: 16
|
||||
}} src="/headers/blog.png" alt="BLOG"></img>
|
||||
{blogs.data.posts.map((post:{
|
||||
title: string,
|
||||
body: string,
|
||||
created: string,
|
||||
updated: string,
|
||||
tags: string[],
|
||||
views: number,
|
||||
slug: string
|
||||
}) => (
|
||||
<div style={{
|
||||
margin: "16px 0"
|
||||
}}>
|
||||
<h2><Link href={`/blog/${post.slug}`}>{post.title}</Link></h2>
|
||||
<p><small>Posted {new Date(post.created).toLocaleString()} - {post.views} views</small></p>
|
||||
<Markdown
|
||||
rehypePlugins={[rehypeRaw]}
|
||||
allowedElements={[]}
|
||||
unwrapDisallowed={true}
|
||||
>
|
||||
{post.body.split("\n").slice(0, 2).join("\n")}
|
||||
</Markdown>
|
||||
</div>
|
||||
))}
|
||||
<p>
|
||||
<ConditionalNull condition={curPage > 1}>
|
||||
<Link href={`?page=${curPage - 1}`}>Previous</Link>
|
||||
</ConditionalNull>
|
||||
{" "}
|
||||
<ConditionalNull condition={curPage * 10 < blogs.data.total_posts}>
|
||||
<Link href={`?page=${curPage + 1}`}>Next</Link>
|
||||
</ConditionalNull>
|
||||
</p>
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
94
src/app/characters/[slug]/page.tsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
import { ConditionalNull } from "@/components/utility/Conditional";
|
||||
import { MainLayout } from "@/layout/MainLayout/MainLayout";
|
||||
import { HeightConverter } from "@/lib/utility/language";
|
||||
import Link from "next/link";
|
||||
import Markdown from "react-markdown";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
import styles from "../style.module.css";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
export default async function Home({
|
||||
params
|
||||
}: {
|
||||
params: {
|
||||
slug: string
|
||||
}
|
||||
}) {
|
||||
const charblogs = await fetch(`https://blog.abtmtr.link/api/collections/characters/posts/${params.slug}`).then(x=>x.json());
|
||||
|
||||
if (charblogs.data == null) return notFound();
|
||||
|
||||
const character:{
|
||||
id: string,
|
||||
name: string,
|
||||
description?: string[],
|
||||
picture: string,
|
||||
gender: string,
|
||||
pronunciation: string,
|
||||
pronouns: string,
|
||||
spec?: string,
|
||||
species: string,
|
||||
age: number,
|
||||
height: number,
|
||||
relationships: {
|
||||
id: string,
|
||||
status: string,
|
||||
}[]
|
||||
} = JSON.parse(charblogs.data.body.replace(/[“”]/g, "\""));
|
||||
const images = await fetch(`https://img.abtmtr.link/api/collections/mtr/posts/${character.picture}`).then(x=>x.json());
|
||||
|
||||
// // REQSPAMMER
|
||||
// let allPosts = [];
|
||||
// let currentPage = 1;
|
||||
|
||||
// const initReq = await fetch(`https://img.abtmtr.link/api/collections/mtr/posts`).then(x=>x.json());
|
||||
// const totalPosts:number = initReq.data.total_posts;
|
||||
|
||||
// while (currentPage * 10 < totalPosts) {
|
||||
// const newPostList = await fetch(`https://img.abtmtr.link/api/collections/mtr/posts?page=${currentPage}`).then(x=>x.json());
|
||||
// allPosts.push(...newPostList.data.posts);
|
||||
|
||||
// currentPage++;
|
||||
// }
|
||||
|
||||
// const myPosts = allPosts.filter(post => post.tags?.includes(character.id));
|
||||
|
||||
return (
|
||||
<MainLayout currentPage="/characters/" title="Characters" backButton>
|
||||
<div style={{display:"block",marginBottom:16,overflow:"auto"}}>
|
||||
<img style={{float:"right",width:"60%", marginLeft:16, marginTop:16}} src={images.data.images[0]}></img>
|
||||
<h1 style={{marginBottom: 0}}>{character.name}</h1>
|
||||
<small>{character.pronunciation}</small>
|
||||
<ConditionalNull condition={character.spec != null}>
|
||||
<p>Spec: <Link href={`https://spec.abtmtr.link/#${character.spec}`} target="_blank">#{character.spec}</Link></p>
|
||||
</ConditionalNull>
|
||||
<p>{character.pronouns}, {character.age}</p>
|
||||
<p>{character.species}, {character.gender}, {HeightConverter(character.height)}</p>
|
||||
<ConditionalNull condition={character.description != null}>
|
||||
<Markdown rehypePlugins={[rehypeRaw]}>{`*${character.description?.join("\n")}*`}</Markdown>
|
||||
</ConditionalNull>
|
||||
</div>
|
||||
<h2>Gallery</h2>
|
||||
<p><i>Feature coming soon...</i></p>
|
||||
{/* <div className={styles.CharacterContainerGrid}>
|
||||
{myPosts.map(async (img:{
|
||||
slug: string,
|
||||
title: string,
|
||||
created: string,
|
||||
images: string[]
|
||||
}) => {
|
||||
return (
|
||||
<Link href={`/gallery/${img.slug}`} className={styles.CharacterContainerLink}>
|
||||
<div style={{
|
||||
backgroundImage: `linear-gradient(180deg, #000c 0%, #000c 50%, #0004 100%), url(${img.images[0]})`,
|
||||
}} className={styles.CharacterContainer}>
|
||||
<h2>{img.title}</h2>
|
||||
<p>{new Date(img.created).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div> */}
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
66
src/app/characters/page.tsx
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { MainLayout } from "@/layout/MainLayout/MainLayout";
|
||||
import styles from "./style.module.css";
|
||||
import Link from "next/link";
|
||||
import { ConditionalNull } from "@/components/utility/Conditional";
|
||||
|
||||
export default async function Home({
|
||||
searchParams
|
||||
}: {
|
||||
searchParams: {
|
||||
page: string
|
||||
}
|
||||
}) {
|
||||
const curPage = parseInt(searchParams?.page) || 1;
|
||||
|
||||
const charblogs = await fetch(`https://blog.abtmtr.link/api/collections/characters/posts?page=${curPage}`).then(x=>x.json());
|
||||
const characters = charblogs.data.posts.map((post:{body:string}) => JSON.parse(post.body.replace(/[“”]/g, "\"")));
|
||||
|
||||
return (
|
||||
<MainLayout currentPage="/characters/" title="Characters" subtitle="">
|
||||
<img style={{
|
||||
marginTop: 16
|
||||
}} src="/headers/characters.png" alt="CHARACTERS"></img>
|
||||
<div className={styles.CharacterContainerGrid}>
|
||||
{characters.map(async (character:{
|
||||
id: string,
|
||||
name: string,
|
||||
description?: string[],
|
||||
picture: string,
|
||||
gender: string,
|
||||
pronunciation: string,
|
||||
pronouns: string,
|
||||
spec?: string,
|
||||
species: string,
|
||||
age: number,
|
||||
height: number,
|
||||
relationships: {
|
||||
id: string,
|
||||
status: string,
|
||||
}[]
|
||||
}) => {
|
||||
const charblogs = await fetch(`https://img.abtmtr.link/api/collections/mtr/posts/${character.picture}`).then(x=>x.json());
|
||||
console.log(charblogs?.data?.images?.[0]);
|
||||
return (
|
||||
<Link href={`/characters/${character.id}`} className={styles.CharacterContainerLink}>
|
||||
<div style={{
|
||||
backgroundImage: `linear-gradient(180deg, #000c 0%, #000c 50%, #0004 100%), url(${charblogs.data.images[0]})`,
|
||||
}} className={styles.CharacterContainer}>
|
||||
<h2>{character.name}</h2>
|
||||
<p>{character.pronouns}</p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p>
|
||||
<ConditionalNull condition={curPage > 1}>
|
||||
<Link href={`?page=${curPage - 1}`}>Previous</Link>
|
||||
</ConditionalNull>
|
||||
{" "}
|
||||
<ConditionalNull condition={curPage * 10 < charblogs.data.total_posts}>
|
||||
<Link href={`?page=${curPage + 1}`}>Next</Link>
|
||||
</ConditionalNull>
|
||||
</p>
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
18
src/app/characters/style.module.css
Normal file
|
@ -0,0 +1,18 @@
|
|||
.CharacterContainerGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.CharacterContainerLink {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.CharacterContainer {
|
||||
aspect-ratio: 1/1;
|
||||
overflow: hidden;
|
||||
padding: 16px;
|
||||
background-size: cover;
|
||||
}
|
Before Width: | Height: | Size: 25 KiB |
27
src/app/gallery/[slug]/page.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { MainLayout } from "@/layout/MainLayout/MainLayout";
|
||||
import { notFound } from "next/navigation";
|
||||
import Markdown from "react-markdown";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
|
||||
export default async function Home({
|
||||
params
|
||||
}:{
|
||||
params: {
|
||||
slug: string
|
||||
}
|
||||
}) {
|
||||
const blog = await fetch(`https://img.abtmtr.link/api/collections/mtr/posts/${params.slug}`).then(x=>x.json());
|
||||
if (blog.data == null)
|
||||
return notFound();
|
||||
return (
|
||||
<MainLayout currentPage={`/gallery/${params.slug}`} title="Gallery" subtitle="via img.abtmtr.link" backButton>
|
||||
<h1>{blog.data.title}</h1>
|
||||
<p><small>Posted {new Date(blog.data.created).toLocaleString()} - {blog.data.views} views</small></p>
|
||||
<div style={{
|
||||
margin: "16px 0"
|
||||
}}>
|
||||
<Markdown rehypePlugins={[rehypeRaw]}>{blog.data.body}</Markdown>
|
||||
</div>
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
56
src/app/gallery/page.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { ConditionalNull } from "@/components/utility/Conditional";
|
||||
import { MainLayout } from "@/layout/MainLayout/MainLayout";
|
||||
import Link from "next/link";
|
||||
import Markdown from "react-markdown";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
|
||||
export default async function Home({
|
||||
searchParams
|
||||
}: {
|
||||
searchParams: {
|
||||
page: string
|
||||
}
|
||||
}) {
|
||||
const curPage = parseInt(searchParams?.page) || 1;
|
||||
|
||||
const blogs = await fetch(`https://img.abtmtr.link/api/collections/mtr/posts?page=${curPage}`).then(x=>x.json());
|
||||
return (
|
||||
<MainLayout currentPage="/gallery/" title="Gallery" subtitle="via img.abtmtr.link">
|
||||
<img style={{
|
||||
marginTop: 16
|
||||
}} src="/headers/gallery.png" alt="GALLERY"></img>
|
||||
{blogs.data.posts.map((post:{
|
||||
title: string,
|
||||
body: string,
|
||||
created: string,
|
||||
updated: string,
|
||||
tags: string[],
|
||||
views: number,
|
||||
slug: string
|
||||
}) => (
|
||||
<div style={{
|
||||
margin: "16px 0"
|
||||
}}>
|
||||
<h2><Link href={`/gallery/${post.slug}`}>{post.title}</Link></h2>
|
||||
<p><small>Posted {new Date(post.created).toLocaleString()} - {post.views} views</small></p>
|
||||
<Markdown
|
||||
rehypePlugins={[rehypeRaw]}
|
||||
allowedElements={[]}
|
||||
unwrapDisallowed={true}
|
||||
>
|
||||
{post.body.split("\n").slice(0, 2).join("\n")}
|
||||
</Markdown>
|
||||
</div>
|
||||
))}
|
||||
<p>
|
||||
<ConditionalNull condition={curPage > 1}>
|
||||
<Link href={`?page=${curPage - 1}`}>Previous</Link>
|
||||
</ConditionalNull>
|
||||
{" "}
|
||||
<ConditionalNull condition={curPage * 10 < blogs.data.total_posts}>
|
||||
<Link href={`?page=${curPage + 1}`}>Next</Link>
|
||||
</ConditionalNull>
|
||||
</p>
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
:root {
|
||||
--max-width: 1100px;
|
||||
--border-radius: 12px;
|
||||
--font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
|
||||
"Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
|
||||
"Fira Mono", "Droid Sans Mono", "Courier New", monospace;
|
||||
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
|
||||
--primary-glow: conic-gradient(
|
||||
from 180deg at 50% 50%,
|
||||
#16abff33 0deg,
|
||||
#0885ff33 55deg,
|
||||
#54d6ff33 120deg,
|
||||
#0071ff33 160deg,
|
||||
transparent 360deg
|
||||
);
|
||||
--secondary-glow: radial-gradient(
|
||||
rgba(255, 255, 255, 1),
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
|
||||
--tile-start-rgb: 239, 245, 249;
|
||||
--tile-end-rgb: 228, 232, 233;
|
||||
--tile-border: conic-gradient(
|
||||
#00000080,
|
||||
#00000040,
|
||||
#00000030,
|
||||
#00000020,
|
||||
#00000010,
|
||||
#00000010,
|
||||
#00000080
|
||||
);
|
||||
|
||||
--callout-rgb: 238, 240, 241;
|
||||
--callout-border-rgb: 172, 175, 176;
|
||||
--card-rgb: 180, 185, 188;
|
||||
--card-border-rgb: 131, 134, 135;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
|
||||
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
|
||||
--secondary-glow: linear-gradient(
|
||||
to bottom right,
|
||||
rgba(1, 65, 255, 0),
|
||||
rgba(1, 65, 255, 0),
|
||||
rgba(1, 65, 255, 0.3)
|
||||
);
|
||||
|
||||
--tile-start-rgb: 2, 13, 46;
|
||||
--tile-end-rgb: 2, 5, 19;
|
||||
--tile-border: conic-gradient(
|
||||
#ffffff80,
|
||||
#ffffff40,
|
||||
#ffffff30,
|
||||
#ffffff20,
|
||||
#ffffff10,
|
||||
#ffffff10,
|
||||
#ffffff80
|
||||
);
|
||||
|
||||
--callout-rgb: 20, 20, 20;
|
||||
--callout-border-rgb: 108, 108, 108;
|
||||
--card-rgb: 100, 100, 100;
|
||||
--card-border-rgb: 200, 200, 200;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgb(var(--background-end-rgb))
|
||||
)
|
||||
rgb(var(--background-start-rgb));
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import localFont from "next/font/local";
|
||||
import "@/styles/style.css";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
const lexendDeca = localFont({ src: "../../public/fonts/Lexend Deca/Variable.ttf"});
|
||||
const materialSymbols = localFont({ src: "../../public/fonts/Material Symbols/Variable.ttf"});
|
||||
const renogare = localFont({ src: "../../public/fonts/Renogare/Regular.otf"});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "abtmtr.link",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
@ -15,8 +16,12 @@ export default function RootLayout({
|
|||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
<html lang="en" style={{
|
||||
"--font-LexendDeca": lexendDeca.style.fontFamily,
|
||||
"--font-MaterialSymbols": materialSymbols.style.fontFamily,
|
||||
"--font-Renogare": renogare.style.fontFamily
|
||||
} as {[key: string]: string}}>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
22
src/app/links/page.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { MainLayout } from "@/layout/MainLayout/MainLayout";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function Home() {
|
||||
const data = await fetch("https://pronouns.cc/api/v1/users/mtr").then(x=>x.json());
|
||||
|
||||
return (
|
||||
<MainLayout currentPage="/links/" title="Links" subtitle="to other places I've been">
|
||||
<img style={{
|
||||
marginTop: 16
|
||||
}} src="/headers/links.png" alt="LINKS"></img>
|
||||
{data.links.map((link:string) => (
|
||||
<div style={{
|
||||
margin: "16px 0"
|
||||
}}>
|
||||
<h2>{new URL(link).host}</h2>
|
||||
<p><Link href={link} target="_blank">{link}</Link></p>
|
||||
</div>
|
||||
))}
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
9
src/app/not-found.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { MainLayout } from "@/layout/MainLayout/MainLayout";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<MainLayout currentPage="/404/" title="404 - Not found">
|
||||
<p>Page not found.</p>
|
||||
</MainLayout>
|
||||
);
|
||||
}
|
28
src/app/oao/page.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { MainLayout } from "@/layout/MainLayout/MainLayout";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
<MainLayout currentPage="/oao/" title="Opinions and Objections">
|
||||
<img style={{
|
||||
marginTop: 16
|
||||
}} src="/headers/oao.png" alt="OPINIONS & OBJECTIONS"></img>
|
||||
<p>As a sentient, living being with thoughts, I have opinions on certain things. Here are mine.</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
<b>I don't mind Chromium-based browsers.</b> Google is evil, sure, but I think it's good to design for what's popular. Plus, I like using Vivaldi over Firefox.
|
||||
Vivaldi crashes less <i>(except when I open a private tab for some reason)</i>, works smoothly, is incredibly customizable, and even though it's not open source, the community is small enough where I can get a reasonable amount of dynamic contact with the creators/maintainers.
|
||||
</p>
|
||||
<p>Really, it's all a matter of personal opinion. (oh my gosh like the page name !!!)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<b>I do mind it when people don't put a CW certain things.</b> USpol, lewdness, that kind of thing.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>More may be coming soon.</p>
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
|
@ -1,230 +0,0 @@
|
|||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: inherit;
|
||||
justify-content: inherit;
|
||||
align-items: inherit;
|
||||
font-size: 0.85rem;
|
||||
max-width: var(--max-width);
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.description a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.description p {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
background-color: rgba(var(--callout-rgb), 0.5);
|
||||
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.code {
|
||||
font-weight: 700;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(25%, auto));
|
||||
max-width: 100%;
|
||||
width: var(--max-width);
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem 1.2rem;
|
||||
border-radius: var(--border-radius);
|
||||
background: rgba(var(--card-rgb), 0);
|
||||
border: 1px solid rgba(var(--card-border-rgb), 0);
|
||||
transition: background 200ms, border 200ms;
|
||||
}
|
||||
|
||||
.card span {
|
||||
display: inline-block;
|
||||
transition: transform 200ms;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin: 0;
|
||||
opacity: 0.6;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
max-width: 30ch;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
.center::before {
|
||||
background: var(--secondary-glow);
|
||||
border-radius: 50%;
|
||||
width: 480px;
|
||||
height: 360px;
|
||||
margin-left: -400px;
|
||||
}
|
||||
|
||||
.center::after {
|
||||
background: var(--primary-glow);
|
||||
width: 240px;
|
||||
height: 180px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.center::before,
|
||||
.center::after {
|
||||
content: "";
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
filter: blur(45px);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: relative;
|
||||
}
|
||||
/* Enable hover only on non-touch devices */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.card:hover {
|
||||
background: rgba(var(--card-rgb), 0.1);
|
||||
border: 1px solid rgba(var(--card-border-rgb), 0.15);
|
||||
}
|
||||
|
||||
.card:hover span {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
.card:hover span {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 700px) {
|
||||
.content {
|
||||
padding: 4rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
margin-bottom: 120px;
|
||||
max-width: 320px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem 2.5rem;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.center {
|
||||
padding: 8rem 0 6rem;
|
||||
}
|
||||
|
||||
.center::before {
|
||||
transform: none;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.description a {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.description p,
|
||||
.description div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.description p {
|
||||
align-items: center;
|
||||
inset: 0 0 auto;
|
||||
padding: 2rem 1rem 1.4rem;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--background-start-rgb), 1),
|
||||
rgba(var(--callout-rgb), 0.5)
|
||||
);
|
||||
background-clip: padding-box;
|
||||
backdrop-filter: blur(24px);
|
||||
}
|
||||
|
||||
.description div {
|
||||
align-items: flex-end;
|
||||
pointer-events: none;
|
||||
inset: auto 0 0;
|
||||
padding: 2rem;
|
||||
height: 200px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
rgb(var(--background-end-rgb)) 40%
|
||||
);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet and Smaller Desktop */
|
||||
@media (min-width: 701px) and (max-width: 1120px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 50%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.vercelLogo {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
220
src/app/page.tsx
|
@ -1,95 +1,133 @@
|
|||
import Image from "next/image";
|
||||
import styles from "./page.module.css";
|
||||
import { MainLayout } from "@/layout/MainLayout/MainLayout";
|
||||
import Link from "next/link";
|
||||
import buttons from "@/buttons.json";
|
||||
|
||||
export default function Home() {
|
||||
export default async function Home() {
|
||||
const data = await fetch("https://pronouns.cc/api/v1/users/mtr").then(x=>x.json());
|
||||
const domainreq = await fetch("https://blog.abtmtr.link/api/collections/paste/posts/domain")
|
||||
.then(x=>x.json());
|
||||
const domains = JSON.parse(domainreq?.data?.body.replace(/[“”]/g, "\"")) ?? [];
|
||||
|
||||
const goodNames = data.names.filter((x:{status:string})=>x.status == "okay");
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.description}>
|
||||
<p>
|
||||
Get started by editing
|
||||
<code className={styles.code}>src/app/page.tsx</code>
|
||||
</p>
|
||||
<div>
|
||||
<a
|
||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
By{" "}
|
||||
<Image
|
||||
src="/vercel.svg"
|
||||
alt="Vercel Logo"
|
||||
className={styles.vercelLogo}
|
||||
width={100}
|
||||
height={24}
|
||||
priority
|
||||
/>
|
||||
</a>
|
||||
<MainLayout currentPage="/" title="Root">
|
||||
<img style={{
|
||||
marginTop: 16
|
||||
}} src="/welcome.png" alt="WELCOME! Enjoy your stay at abtmtr.link!"></img>
|
||||
<p>abtmtr.link is a domain for a suite of services, ranging from micro-blogging to web search.</p>
|
||||
<p>I'm {goodNames[0].value}, otherwise known as {goodNames.slice(1).map((name:{value:string}, idx:number) => (
|
||||
idx == goodNames.length - 2 ? (<>or <b>{name.value}</b></>) : (<><b>{name.value}</b>{goodNames.length <= 2 ? ", " : " "}</>)
|
||||
))}.</p>
|
||||
<p>It proves very useful for anything and everything that I want to do. It also tends to provide service for others when they need it.</p>
|
||||
<h2>Subdomains</h2>
|
||||
{domains.map((domain:{
|
||||
name: string,
|
||||
description: string[],
|
||||
url: string,
|
||||
src: string
|
||||
}) => (
|
||||
<div style={{
|
||||
margin: "16px 0"
|
||||
}}>
|
||||
<h2><Link href={domain.url}>{domain.name}</Link></h2>
|
||||
<p>{domain.description.join("\n")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.center}>
|
||||
<Image
|
||||
className={styles.logo}
|
||||
src="/next.svg"
|
||||
alt="Next.js Logo"
|
||||
width={180}
|
||||
height={37}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.grid}>
|
||||
<a
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2>
|
||||
Docs <span>-></span>
|
||||
</h2>
|
||||
<p>Find in-depth information about Next.js features and API.</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2>
|
||||
Learn <span>-></span>
|
||||
</h2>
|
||||
<p>Learn about Next.js in an interactive course with quizzes!</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2>
|
||||
Templates <span>-></span>
|
||||
</h2>
|
||||
<p>Explore starter templates for Next.js.</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2>
|
||||
Deploy <span>-></span>
|
||||
</h2>
|
||||
<p>
|
||||
Instantly deploy your Next.js site to a shareable URL with Vercel.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
))}
|
||||
<div style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
flexWrap: "wrap",
|
||||
padding: 8,
|
||||
gap: 8
|
||||
}}>{buttons.map((button) => (
|
||||
<Link href={button.href} target="_blank">
|
||||
<img src={button.img} title={button.alt} alt={button.alt} width="88" height="31" />
|
||||
</Link>
|
||||
))}</div>
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
"id": "clhd8desmggedm3q1t3g",
|
||||
"id_new": "251576323070604865",
|
||||
"sid": "qftsk",
|
||||
"name": "mtr",
|
||||
"display_name": "abtmtr.link: FURRY REVOLUTION",
|
||||
"bio": "https://abtmtr.link/\nminor (16)\nabtmtr.link sysadmin\n🇩🇴 & 🏳️⚧️-ish & pluralflux ND ",
|
||||
"member_title": "Managers",
|
||||
"avatar": "dfa7038db301508df749772f07ae9302fb0b9d71dff6770f9a6fefb6cf16f2e3",
|
||||
"links": [
|
||||
"https://last.fm/user/MeowcaTheoRange",
|
||||
"https://liberapay.com/abtmtr.link/",
|
||||
"https://beta.trollcall.xyz/clan/meowcatheorange",
|
||||
"https://movie-web.app/",
|
||||
"https://picrew.me/en/image_maker/92646",
|
||||
"https://pronouns.cc/@mtr"
|
||||
],
|
||||
"names": [
|
||||
{
|
||||
"value": "MeowcaTheoRange",
|
||||
"status": "okay"
|
||||
},
|
||||
{
|
||||
"value": "abtmtr.link",
|
||||
"status": "okay"
|
||||
},
|
||||
{
|
||||
"value": "Theo",
|
||||
"status": "okay"
|
||||
}
|
||||
],
|
||||
"pronouns": [
|
||||
{
|
||||
"pronouns": "[any]",
|
||||
"display_text": null,
|
||||
"status": "okay"
|
||||
}
|
||||
],
|
||||
"members": [
|
||||
{
|
||||
"id": "cng1h37m985pb3k8ohe0",
|
||||
"id_new": "286009441537261082",
|
||||
"sid": "shdvoo",
|
||||
"name": "MeowcaTheoRange",
|
||||
"display_name": "MeowcaTheoRange",
|
||||
"bio": "totality in association.",
|
||||
"avatar": "a686a0bfeeb19ce6591a3bef6bb0f4ff38586f0d75b857cb21c3d86ae42b39e4",
|
||||
"links": [
|
||||
|
||||
],
|
||||
"names": [
|
||||
{
|
||||
"value": "Theo",
|
||||
"status": "okay"
|
||||
},
|
||||
{
|
||||
"value": "Thea",
|
||||
"status": "okay"
|
||||
}
|
||||
],
|
||||
"pronouns": [
|
||||
{
|
||||
"pronouns": "[any]",
|
||||
"display_text": null,
|
||||
"status": "okay"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
|
||||
],
|
||||
"custom_preferences": {
|
||||
|
||||
},
|
||||
"flags": [
|
||||
|
||||
],
|
||||
"badges": 0,
|
||||
"utc_offset": -18000
|
||||
}
|
||||
*/
|
17
src/app/projects/item/[slug]/page.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { notFound, redirect, RedirectType } from "next/navigation";
|
||||
|
||||
export default async function Home({
|
||||
params
|
||||
}:{
|
||||
params: {
|
||||
slug: string
|
||||
}
|
||||
}) {
|
||||
const projects = await fetch("https://projects.abtmtr.link/projects.json").then(x=>x.json());
|
||||
|
||||
const curProject = projects.items.find((x:{url:string}) => x.url.includes("projects.abtmtr.link/item/" + params.slug));
|
||||
|
||||
if (curProject)
|
||||
return redirect(curProject.url, RedirectType.replace);
|
||||
return notFound();
|
||||
}
|
30
src/app/projects/page.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { MainLayout } from "@/layout/MainLayout/MainLayout";
|
||||
import Link from "next/link";
|
||||
import Markdown from "react-markdown";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
|
||||
export default async function Home() {
|
||||
const projects = await fetch("https://projects.abtmtr.link/projects.json").then(x=>x.json());
|
||||
return (
|
||||
<MainLayout currentPage="/projects/" title="Projects" subtitle="by MeowcaTheoRange">
|
||||
<img style={{
|
||||
marginTop: 16
|
||||
}} src="/headers/projects.png" alt="PROJECTS"></img>
|
||||
{projects.items.map((project:{
|
||||
name: string,
|
||||
date: number,
|
||||
description: string[],
|
||||
url: string,
|
||||
hotlink?: boolean
|
||||
}) => (
|
||||
<div style={{
|
||||
margin: "16px 0"
|
||||
}}>
|
||||
<h2><Link href={project.url} target="_blank">{project.name}</Link></h2>
|
||||
<p><small>Hosted on {new URL(project.url).host} - Created {new Date(project.date).toLocaleDateString()}</small></p>
|
||||
<Markdown rehypePlugins={[rehypeRaw]}>{project.description.join("\n")}</Markdown>
|
||||
</div>
|
||||
))}
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
27
src/app/stories/[slug]/page.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { MainLayout } from "@/layout/MainLayout/MainLayout";
|
||||
import { notFound } from "next/navigation";
|
||||
import Markdown from "react-markdown";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
|
||||
export default async function Home({
|
||||
params
|
||||
}:{
|
||||
params: {
|
||||
slug: string
|
||||
}
|
||||
}) {
|
||||
const blog = await fetch(`https://blog.abtmtr.link/api/collections/stories/posts/${params.slug}`).then(x=>x.json());
|
||||
if (blog.data == null)
|
||||
return notFound();
|
||||
return (
|
||||
<MainLayout currentPage={`/stories/${params.slug}`} title="Stories" subtitle="via blog.abtmtr.link" backButton>
|
||||
<h1>{blog.data.title}</h1>
|
||||
<p><small>Posted {new Date(blog.data.created).toLocaleString()} - {blog.data.views} views</small></p>
|
||||
<div style={{
|
||||
margin: "16px 0"
|
||||
}}>
|
||||
<Markdown rehypePlugins={[rehypeRaw]}>{blog.data.body}</Markdown>
|
||||
</div>
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
55
src/app/stories/page.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { ConditionalNull } from "@/components/utility/Conditional";
|
||||
import { MainLayout } from "@/layout/MainLayout/MainLayout";
|
||||
import Link from "next/link";
|
||||
import Markdown from "react-markdown";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
|
||||
export default async function Home({
|
||||
searchParams
|
||||
}: {
|
||||
searchParams: {
|
||||
page: string
|
||||
}
|
||||
}) {
|
||||
const curPage = parseInt(searchParams?.page) || 1;
|
||||
const blogs = await fetch(`https://blog.abtmtr.link/api/collections/stories/posts?page=${curPage}`).then(x=>x.json());
|
||||
return (
|
||||
<MainLayout currentPage="/stories/" title="Stories" subtitle="via blog.abtmtr.link">
|
||||
<img style={{
|
||||
marginTop: 16
|
||||
}} src="/headers/stories.png" alt="STORIES"></img>
|
||||
{blogs.data.posts.map((post:{
|
||||
title: string,
|
||||
body: string,
|
||||
created: string,
|
||||
updated: string,
|
||||
tags: string[],
|
||||
views: number,
|
||||
slug: string
|
||||
}) => (
|
||||
<div style={{
|
||||
margin: "16px 0"
|
||||
}}>
|
||||
<h2><Link href={`/stories/${post.slug}`}>{post.title}</Link></h2>
|
||||
<p><small>Posted {new Date(post.created).toLocaleString()} - {post.views} views</small></p>
|
||||
<Markdown
|
||||
rehypePlugins={[rehypeRaw]}
|
||||
allowedElements={[]}
|
||||
unwrapDisallowed={true}
|
||||
>
|
||||
{post.body.split("\n").slice(0, 2).join("\n")}
|
||||
</Markdown>
|
||||
</div>
|
||||
))}
|
||||
<p>
|
||||
<ConditionalNull condition={curPage > 1}>
|
||||
<Link href={`?page=${curPage - 1}`}>Previous</Link>
|
||||
</ConditionalNull>
|
||||
{" "}
|
||||
<ConditionalNull condition={curPage * 10 < blogs.data.total_posts}>
|
||||
<Link href={`?page=${curPage + 1}`}>Next</Link>
|
||||
</ConditionalNull>
|
||||
</p>
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
92
src/buttons.json
Normal file
|
@ -0,0 +1,92 @@
|
|||
[
|
||||
{
|
||||
"href": "https://jaiden.sh/",
|
||||
"img": "/88x31/jaiden_sh.png",
|
||||
"alt": "jaiden.sh"
|
||||
},
|
||||
{
|
||||
"href": "https://owenzimmerman.com/",
|
||||
"img": "/88x31/owenzimmerman_com.png",
|
||||
"alt": "owenzimmerman.com"
|
||||
},
|
||||
{
|
||||
"href": "https://acidicalchemist.neocities.org/",
|
||||
"img": "/88x31/acidicalchemist_neocities_org.gif",
|
||||
"alt": "acidicalchemist.neocities.org"
|
||||
},
|
||||
{
|
||||
"href": "https://arimelody.me/",
|
||||
"img": "/88x31/arimelody_me.gif",
|
||||
"alt": "arimelody.me"
|
||||
},
|
||||
{
|
||||
"href": "https://freeplay.floof.company/",
|
||||
"img": "/88x31/freeplay_floof_company.png",
|
||||
"alt": "freeplay.floof.company"
|
||||
},
|
||||
{
|
||||
"href": "https://invoxiplaygames.uk/",
|
||||
"img": "/88x31/invoxiplaygames_uk.png",
|
||||
"alt": "invoxiplaygames.uk"
|
||||
},
|
||||
{
|
||||
"href": "https://ioletsgo.gay/",
|
||||
"img": "/88x31/ioletsgo_gay.gif",
|
||||
"alt": "ioletsgo.gay"
|
||||
},
|
||||
{
|
||||
"href": "https://mae.wtf/",
|
||||
"img": "/88x31/mae_wtf.png",
|
||||
"alt": "mae.wtf"
|
||||
},
|
||||
{
|
||||
"href": "https://micro.pages.gay/",
|
||||
"img": "/88x31/micro_pages_gay.png",
|
||||
"alt": "micro.pages.gay"
|
||||
},
|
||||
{
|
||||
"href": "https://sneexy.pages.gay/",
|
||||
"img": "/88x31/sneexy_pages_gay.gif",
|
||||
"alt": "sneexy.pages.gay"
|
||||
},
|
||||
{
|
||||
"href": "https://whois.slipfox.xyz/",
|
||||
"img": "/88x31/whois_slipfox_xyz.png",
|
||||
"alt": "whois.slipfox.xyz"
|
||||
},
|
||||
{
|
||||
"href": "https://moth.monster/",
|
||||
"img": "/88x31/moth_monster.png",
|
||||
"alt": "moth.monster"
|
||||
},
|
||||
{
|
||||
"href": "https://translunar.academy/",
|
||||
"img": "/88x31/translunar_academy.png",
|
||||
"alt": "translunar.academy"
|
||||
},
|
||||
{
|
||||
"href": "https://ultramarine-linux.org/",
|
||||
"img": "/88x31/esoteric/gnu-linux.gif",
|
||||
"alt": "Made on GNU/Linux"
|
||||
},
|
||||
{
|
||||
"href": "https://abtmtr.link/projects/item/normalize",
|
||||
"img": "/88x31/esoteric/html.gif",
|
||||
"alt": "<HTML> - Learn it today!"
|
||||
},
|
||||
{
|
||||
"href": "https://vivaldi.com",
|
||||
"img": "/88x31/esoteric/vivaldi.gif",
|
||||
"alt": "I use Vivaldi"
|
||||
},
|
||||
{
|
||||
"href": "https://ublockorigin.com",
|
||||
"img": "/88x31/esoteric/ublock.png",
|
||||
"alt": "uBlock Origin Now!"
|
||||
},
|
||||
{
|
||||
"href": "https://channelstore.roku.com/details/7da3fa0c2209746730df8a4e21e83b02",
|
||||
"img": "/88x31/esoteric/xkcd.gif",
|
||||
"alt": "xkcd"
|
||||
}
|
||||
]
|
35
src/components/utility/Conditional/index.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import React from "react";
|
||||
|
||||
export function Conditional({
|
||||
condition,
|
||||
truthy,
|
||||
falsy
|
||||
}:{
|
||||
condition: boolean,
|
||||
truthy: React.ReactNode,
|
||||
falsy: React.ReactNode
|
||||
}) {
|
||||
return condition ? truthy : falsy;
|
||||
}
|
||||
|
||||
export function ConditionalNull({
|
||||
condition,
|
||||
children
|
||||
}:{
|
||||
condition: boolean,
|
||||
children: React.ReactNode,
|
||||
}) {
|
||||
return condition ? children : <></>;
|
||||
}
|
||||
|
||||
export function ConditionalParent({
|
||||
condition,
|
||||
parent,
|
||||
children
|
||||
}:{
|
||||
condition: boolean,
|
||||
parent: (childNode:React.ReactNode) => React.ReactNode,
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return condition ? parent(children) : children;
|
||||
}
|
62
src/layout/MainLayout/Desktop.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
'use client';
|
||||
|
||||
import { ConditionalNull } from "@/components/utility/Conditional";
|
||||
import { useState } from "react";
|
||||
import styles from "./MainLayout.module.css";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export function Desktop({
|
||||
title,
|
||||
subtitle,
|
||||
backButton = false,
|
||||
children,
|
||||
sidebar
|
||||
}:{
|
||||
title: string,
|
||||
subtitle?: string,
|
||||
backButton?: boolean,
|
||||
children: React.ReactNode,
|
||||
sidebar: React.ReactNode
|
||||
}) {
|
||||
const [menuOpen, setMenuOpen] = useState<boolean>(false);
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className={`${styles.MainLayout}`}>
|
||||
<aside className={`${styles.MainLayout_Aside} ${menuOpen ? styles.Modifier_Open : ""}`}>
|
||||
{sidebar}
|
||||
</aside>
|
||||
<div className={styles.MainLayout_Inner}>
|
||||
<div className={`${
|
||||
styles.MainLayout_Inner_Header
|
||||
} ${
|
||||
backButton ? styles.Modifier_Back : ""
|
||||
}`}>
|
||||
<ConditionalNull condition={!backButton}>
|
||||
<div className={`${styles.MainLayout_Inner_Header_Back} ${styles.MainLayout_Inner_Header_Menu}`}>
|
||||
<button className={styles.MainLayout_Inner_Header_Back_Button} onClick={() => setMenuOpen(!menuOpen)}>
|
||||
menu
|
||||
</button>
|
||||
</div>
|
||||
</ConditionalNull>
|
||||
<ConditionalNull condition={backButton}>
|
||||
<div className={styles.MainLayout_Inner_Header_Back}>
|
||||
<button className={styles.MainLayout_Inner_Header_Back_Button} onClick={() => router.back()}>
|
||||
arrow_back
|
||||
</button>
|
||||
</div>
|
||||
</ConditionalNull>
|
||||
<div className={styles.MainLayout_Inner_Header_Title}>
|
||||
<h1>{title}</h1>
|
||||
<span>{subtitle}</span>
|
||||
</div>
|
||||
</div>
|
||||
<main className={styles.MainLayout_Inner_Main}>
|
||||
<div className={`${styles.MainLayout_Inner_Main_Content} MainContent`}>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
186
src/layout/MainLayout/MainLayout.module.css
Normal file
|
@ -0,0 +1,186 @@
|
|||
@keyframes slidein {
|
||||
0% {
|
||||
position: relative;
|
||||
top: 10px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
99% {
|
||||
position: relative;
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
position: static;
|
||||
top: unset;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slidein_header {
|
||||
0% {
|
||||
position: relative;
|
||||
left: -10px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
99% {
|
||||
position: relative;
|
||||
left: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
position: static;
|
||||
top: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.MainLayout {
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
grid-template-columns: 256px 1fr;
|
||||
}
|
||||
|
||||
.MainLayout_Aside {
|
||||
padding: 4px 16px;
|
||||
background-color: var(--bg-1);
|
||||
color: var(--fg-1);
|
||||
border-right: 1px solid var(--neutral);
|
||||
}
|
||||
|
||||
.MainLayout_Inner {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
display: grid;
|
||||
grid-template-rows: 48px auto;
|
||||
}
|
||||
|
||||
.MainLayout_Inner_Header {
|
||||
padding: 6px 16px;
|
||||
background-color: var(--bg-2);
|
||||
color: var(--fg-2);
|
||||
border-bottom: 1px solid var(--neutral);
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.MainLayout_Inner_Header>* {
|
||||
animation-duration: 0.25s;
|
||||
animation-name: slidein_header;
|
||||
}
|
||||
|
||||
.MainLayout_Inner_Header.Modifier_Back {
|
||||
padding: 6px 8px;
|
||||
grid-template-columns: 32px auto;
|
||||
}
|
||||
|
||||
.MainLayout_Inner_Header_Back_Button {
|
||||
font-family: var(--font-MaterialSymbols);
|
||||
font-size: 24px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.MainLayout_Inner_Header_Menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.MainLayout_Inner_Header_Title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.MainLayout_Inner_Header_Title h1 {
|
||||
margin: 0;
|
||||
vertical-align: middle;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.MainLayout_Inner_Header_Title span {
|
||||
margin: 0;
|
||||
vertical-align: middle;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.MainLayout_Inner_Main {
|
||||
display: inline-block;
|
||||
background-color: var(--bg-1);
|
||||
color: var(--fg-1);
|
||||
width: 100%;
|
||||
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.MainLayout_Inner_Main_Content {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
background-color: var(--bg-2);
|
||||
color: var(--fg-2);
|
||||
padding: 0 16px;
|
||||
max-width: 50em;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
margin-bottom: -4px;
|
||||
|
||||
/* border-left: 1px solid var(--neutral);
|
||||
border-right: 1px solid var(--neutral); */
|
||||
}
|
||||
|
||||
.MainLayout_Inner_Main_Content>* {
|
||||
animation-duration: 0.25s;
|
||||
animation-name: slidein;
|
||||
}
|
||||
|
||||
.MainLayout_Inner_Main_Content img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.MainLayout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.MainLayout_Aside {
|
||||
position: absolute;
|
||||
height: calc(100% - 48px);
|
||||
width: 100%;
|
||||
top: 48px;
|
||||
z-index: 50;
|
||||
left: -100%;
|
||||
transition: left 0.125s;
|
||||
}
|
||||
|
||||
.MainLayout_Inner_Header_Menu {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.MainLayout_Inner_Header {
|
||||
padding: 6px 8px;
|
||||
grid-template-columns: 32px auto;
|
||||
}
|
||||
|
||||
.MainLayout_Aside.Modifier_Open {
|
||||
left: 0;
|
||||
}
|
||||
}
|
22
src/layout/MainLayout/MainLayout.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { SidebarMain } from "./Sidebar/Main/Main";
|
||||
import { Desktop } from "./Desktop";
|
||||
|
||||
export function MainLayout({
|
||||
title,
|
||||
subtitle,
|
||||
backButton = false,
|
||||
children,
|
||||
sidebar,
|
||||
currentPage
|
||||
}:{
|
||||
title: string,
|
||||
subtitle?: string,
|
||||
backButton?: boolean,
|
||||
children: React.ReactNode,
|
||||
sidebar?: React.ReactNode,
|
||||
currentPage: string
|
||||
}) {
|
||||
return (<>
|
||||
<Desktop title={title} subtitle={subtitle} backButton={backButton} sidebar={sidebar ?? SidebarMain({currentPage})}>{children}</Desktop>
|
||||
</>)
|
||||
}
|
26
src/layout/MainLayout/Sidebar/Main/Main.module.css
Normal file
|
@ -0,0 +1,26 @@
|
|||
.Main_Image {
|
||||
text-align: center;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.Main_Image img {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.Main_List {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.Main_List_CurrentLink button {
|
||||
background-color: var(--bg-2);
|
||||
}
|
||||
|
||||
.Main_List_Button {
|
||||
transition: scale 0.0625s;
|
||||
}
|
||||
|
||||
.Main_List_Button:active {
|
||||
scale: 0.95;
|
||||
}
|
58
src/layout/MainLayout/Sidebar/Main/Main.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import Link from "next/link";
|
||||
import styles from "./Main.module.css";
|
||||
|
||||
export function SidebarMain({currentPage}:{currentPage:string}) {
|
||||
return <>
|
||||
<h1>abtmtr.link</h1>
|
||||
{/* <div className={styles.Main_Image}>
|
||||
<img src="/favicon.ico" width="128" height="128" />
|
||||
</div> */}
|
||||
<div className={styles.Main_List}>
|
||||
<Link href="/" className={currentPage === "/" ? styles.Main_List_CurrentLink : ""}>
|
||||
<button className={`fw ${styles.Main_List_Button}`}>
|
||||
Root
|
||||
</button>
|
||||
</Link>
|
||||
<Link href="/characters/" className={currentPage.includes("/characters/") ? styles.Main_List_CurrentLink : ""}>
|
||||
<button className={`fw ${styles.Main_List_Button}`}>
|
||||
Characters
|
||||
</button>
|
||||
</Link>
|
||||
<Link href="/links/" className={currentPage.includes("/links/") ? styles.Main_List_CurrentLink : ""}>
|
||||
<button className={`fw ${styles.Main_List_Button}`}>
|
||||
Links
|
||||
</button>
|
||||
</Link>
|
||||
<Link href="/oao/" className={currentPage.includes("/oao/") ? styles.Main_List_CurrentLink : ""}>
|
||||
<button className={`fw ${styles.Main_List_Button}`}>
|
||||
Opinions & Objections
|
||||
</button>
|
||||
</Link>
|
||||
<Link href="/projects/" className={currentPage.includes("/projects/") ? styles.Main_List_CurrentLink : ""}>
|
||||
<button className={`fw ${styles.Main_List_Button}`}>
|
||||
Projects
|
||||
</button>
|
||||
</Link>
|
||||
<Link href="/blog/" className={currentPage.includes("/blog/") ? styles.Main_List_CurrentLink : ""}>
|
||||
<button className={`fw ${styles.Main_List_Button}`}>
|
||||
Blog
|
||||
</button>
|
||||
</Link>
|
||||
<Link href="/stories/" className={currentPage.includes("/stories/") ? styles.Main_List_CurrentLink : ""}>
|
||||
<button className={`fw ${styles.Main_List_Button}`}>
|
||||
Stories
|
||||
</button>
|
||||
</Link>
|
||||
<Link href="/gallery/" className={currentPage.includes("/gallery/") ? styles.Main_List_CurrentLink : ""}>
|
||||
<button className={`fw ${styles.Main_List_Button}`}>
|
||||
Gallery
|
||||
</button>
|
||||
</Link>
|
||||
<Link href="/about/" className={currentPage.includes("/about/") ? styles.Main_List_CurrentLink : ""}>
|
||||
<button className={`fw ${styles.Main_List_Button}`}>
|
||||
About
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
}
|
12
src/lib/utility/LanguageUtils.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
export class LanguageUtils_ENG {
|
||||
static possess(owner: string, possession: string) {
|
||||
return owner + (owner.endsWith("s") ? "'" : "'s") + " " + possession;
|
||||
}
|
||||
static pluralize(string: string) {
|
||||
if (string.match(/(?:s|ch|sh|x|z|[^aeiou]o)$/)) return string + "es";
|
||||
else if (string.match(/[aeiou](?:y|o)$/)) return string + "s";
|
||||
else if (string.match(/[^aeiou]y$/)) return string.slice(0, -1) + "ies";
|
||||
else if (string.match(/(?:f|fe)$/)) return string.slice(0, -1) + "ves";
|
||||
else return string + "s";
|
||||
}
|
||||
}
|
47
src/lib/utility/TypeUtils.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
export class ArrayUtils {
|
||||
static pick<T>(array: T[]): T {
|
||||
return array[Math.round(Math.random() * (array.length - 1))];
|
||||
}
|
||||
static pickMultiple<T>(array: T[], amount: number): T[] {
|
||||
return array.toSorted(() => 0.5 - Math.random()).slice(0, amount);
|
||||
}
|
||||
}
|
||||
|
||||
export class NumberUtils {
|
||||
static clamp(number: number, min: number, max: number) {
|
||||
return Math.max(min, Math.min(number, max));
|
||||
}
|
||||
static isBetween(number: number, min: number, max: number) {
|
||||
return number >= min && number <= max;
|
||||
}
|
||||
static pickRandom(min: number, max: number) {
|
||||
return min + (Math.random() * (max - min));
|
||||
}
|
||||
}
|
||||
|
||||
export class StringUtils {
|
||||
static truncate(string: string, length: number) {
|
||||
if (string.length <= length) {
|
||||
return string;
|
||||
} else {
|
||||
return string.slice(0, length - 3) + "...";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ObjectUtils {
|
||||
static isObject(object: any) {
|
||||
if (object == null) return false;
|
||||
return object.constructor === Object;
|
||||
}
|
||||
|
||||
static isEmpty(object: any) {
|
||||
return Object.keys(object).length === 0;
|
||||
}
|
||||
|
||||
static navigatePath(object: any, path: string) {
|
||||
return path.split(".").reduce((objnav: any, pathseg: string) => {
|
||||
return objnav[pathseg];
|
||||
}, object);
|
||||
}
|
||||
}
|
62
src/lib/utility/language.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
export function AdaptivePossessive(owner: string, possession: string) {
|
||||
return owner + (owner.endsWith("s") ? "'" : "'s") + " " + possession;
|
||||
}
|
||||
|
||||
export function PronounGrouper(
|
||||
pronouns: [string, string, string][],
|
||||
sep?: string,
|
||||
amount?: number
|
||||
) {
|
||||
if (pronouns.length > 1)
|
||||
return pronouns.map(pronounSet => pronounSet[0]).join("/");
|
||||
else return pronouns[0].slice(0, amount ?? 2).join(sep ?? "/");
|
||||
}
|
||||
|
||||
export function HeightConverter(inches: number) {
|
||||
var feetandinches = Math.floor(inches / 12) + "'" + (inches % 12) + '"';
|
||||
var meters = Math.floor((inches / 39.37) * 100) / 100 + "m";
|
||||
return feetandinches + " (" + meters + ")";
|
||||
}
|
||||
export function HeightConverterImperial(inches: number) {
|
||||
return Math.floor(inches / 12) + "'" + (inches % 12) + '"';
|
||||
}
|
||||
|
||||
export function HeightConverterMetric(inches: number) {
|
||||
return Math.floor((inches / 39.37) * 100) / 100 + "m";
|
||||
}
|
||||
|
||||
export function AgeConverter(age: number, years?: boolean) {
|
||||
if (years) return `${Math.round((age / 2.1667) * 10) / 10} sweeps`;
|
||||
else if (years == null) return `${Math.round(age)} years`;
|
||||
else return `${Math.round(age * 2.1667 * 10) / 10} years`;
|
||||
}
|
||||
|
||||
export function Pluralize(stringe: string) {
|
||||
if (stringe.match(/(?:s|ch|sh|x|z|[^aeiou]o)$/)) return stringe + "es";
|
||||
else if (stringe.match(/[aeiou](?:y|o)$/)) return stringe + "s";
|
||||
else if (stringe.match(/[^aeiou]y$/)) return stringe.slice(0, -1) + "ies";
|
||||
else if (stringe.match(/(?:f|fe)$/)) return stringe.slice(0, -1) + "ves";
|
||||
else return stringe + "s";
|
||||
}
|
||||
|
||||
export function ArraySample(array: any[]) {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
|
||||
export function ProperNounCase(string: string) {
|
||||
return string
|
||||
.split(" ")
|
||||
.map(x => x.charAt(0).toUpperCase() + x.slice(1).toLowerCase())
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
export function PesterchumNameFormatter(string: string) {
|
||||
return (
|
||||
string +
|
||||
" [" +
|
||||
string
|
||||
.replace(/^(([a-z])[a-z]+)(([A-Z])[a-z]+)$/, "$2$4")
|
||||
.toUpperCase() +
|
||||
"]"
|
||||
);
|
||||
}
|
84
src/styles/style.css
Normal file
|
@ -0,0 +1,84 @@
|
|||
:root {
|
||||
--accent-color: hsl(120, 40%, 75%);
|
||||
--bg-0: hsl(120, 20%, 0%);
|
||||
--bg-1: hsl(120, 20%, 6.25%);
|
||||
--bg-2: hsl(120, 20%, 12.5%);
|
||||
--bg-3: hsl(120, 20%, 25%);
|
||||
--neutral: #80808080;
|
||||
--fg-0: hsl(120, 20%, 50%);
|
||||
--fg-1: hsl(120, 20%, 62.5%);
|
||||
--fg-2: hsl(120, 20%, 75%);
|
||||
--fg-3: hsl(120, 20%, 87.5%);
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--bg-2);
|
||||
color: var(--fg-2);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: var(--font-Renogare);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
p {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
p {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: transparent;
|
||||
color: unset;
|
||||
font-family: unset;
|
||||
font-size: unset;
|
||||
cursor: pointer;
|
||||
transition: all 0.125s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
button.fw {
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button.fw a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button.fw:hover {
|
||||
background-color: var(--bg-2);
|
||||
opacity: 1;
|
||||
}
|