Initial Release
@@ -0,0 +1 @@
|
|||||||
|
.development
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"golang.go"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"editor.tabSize": 4,
|
||||||
|
"editor.insertSpaces": true,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.rulers": [
|
||||||
|
120
|
||||||
|
],
|
||||||
|
"files.trimTrailingWhitespace": true,
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
|
"files.associations": {},
|
||||||
|
"[css]": {
|
||||||
|
"editor.defaultFormatter": "vscode.css-language-features"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
-------------------------------------------------------------------------------
|
||||||
|
ABOUT
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Creating websites is a fun way to refine my skills and practice UI/UX.
|
||||||
|
|
||||||
|
Each version represents my psyche at the current time alongside which tooling
|
||||||
|
and aesthetics interested me.
|
||||||
|
|
||||||
|
I tend to change homepage regularly, and I don't attempt to make each version
|
||||||
|
better than the last these things just tend to happen naturally.
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
HISTORY
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
* pancakz-v1-caution:
|
||||||
|
Caution Tape Design, inspired by txt.osk.sh
|
||||||
|
|
||||||
|
* pancakz-v2-crossroads:
|
||||||
|
Every aesthetic, inspired by a lost twitter post
|
||||||
|
|
||||||
|
* pancakz-v3-beam:
|
||||||
|
Natural Evolution of V2. Uses a static site instead of a
|
||||||
|
webserver. Inspired by Dance Delightful.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.env
|
||||||
|
data
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Build
|
||||||
|
FROM golang:1.25.2-alpine3.22 AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
RUN go build -o homepage.elf main.go
|
||||||
|
|
||||||
|
# Runtime
|
||||||
|
FROM alpine:latest AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /app/homepage.elf .
|
||||||
|
|
||||||
|
ENV HTTP_ADDRESS="0.0.0.0:8080"
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["./homepage.elf"]
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FILE_MODE = os.FileMode(0770)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
HTTP_TLS *tls.Config // http: TLS Configuration
|
||||||
|
HTTP_ADDRESS = envString("HTTP_ADDRESS", "localhost:8080") // http: Address to Listen for Requests on
|
||||||
|
TLS_ENABLED = envString("TLS_ENABLED", "false") == "true" // http: Enable TLS?
|
||||||
|
TLS_CERT = envString("TLS_CERT", "tls_crt.pem") // http: Path to TLS Certificate
|
||||||
|
TLS_KEY = envString("TLS_KEY", "tls_key.pem") // http: Path to TLS Key
|
||||||
|
TLS_CA = envString("TLS_CA", "tls_ca.pem") // http: Path to TLS CA Bundle
|
||||||
|
VERSION = fmt.Sprintf("%X", time.Now().Unix())
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log.Println("[env] Using Version Hash:", VERSION)
|
||||||
|
|
||||||
|
// Load and Parse TLS Configuration from Disk
|
||||||
|
if TLS_ENABLED {
|
||||||
|
cert, err := tls.LoadX509KeyPair(TLS_CERT, TLS_KEY)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("[env/tls] Cannot Load Keypair", err)
|
||||||
|
}
|
||||||
|
caBytes, err := os.ReadFile(TLS_CA)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("[env/tls] Cannot Read CA File", err)
|
||||||
|
}
|
||||||
|
caPool := x509.NewCertPool()
|
||||||
|
if !caPool.AppendCertsFromPEM(caBytes) {
|
||||||
|
log.Fatalln("[env/tls] Cannot Append Certificates")
|
||||||
|
}
|
||||||
|
HTTP_TLS = &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
ClientCAs: caPool,
|
||||||
|
MinVersion: tls.VersionTLS13,
|
||||||
|
MaxVersion: tls.VersionTLS13,
|
||||||
|
CipherSuites: []uint16{
|
||||||
|
tls.TLS_AES_128_GCM_SHA256,
|
||||||
|
tls.TLS_AES_256_GCM_SHA384,
|
||||||
|
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads String from Environment
|
||||||
|
func envString(key, defaultValue string) string {
|
||||||
|
systemValue := os.Getenv(key)
|
||||||
|
if systemValue == "" {
|
||||||
|
if defaultValue == "\x00" {
|
||||||
|
fmt.Printf("[env] Environment Variable '%s' is undefined\n", key)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return systemValue
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type databaseRoot struct {
|
||||||
|
Site databaseSite `json:"site"`
|
||||||
|
Articles []databaseArticle `json:"articles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type databaseSite struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Locale string `json:"locale"`
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
Color string `json:"color"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type databaseArticle struct {
|
||||||
|
BaseTemplate string `json:"base_template"`
|
||||||
|
BaseResources string `json:"base_resources"`
|
||||||
|
BaseBanner string `json:"base_banner"`
|
||||||
|
Color string `json:"color"`
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed database.json
|
||||||
|
embedDatabase []byte
|
||||||
|
Database databaseRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if err := json.Unmarshal(embedDatabase, &Database); err != nil {
|
||||||
|
log.Fatalln("[env/db] Parse Database Error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"site": {
|
||||||
|
"host": "https://panca.kz",
|
||||||
|
"name": "pancakz",
|
||||||
|
"description": "Tech, Chunibyo \u0026 Other Delusions",
|
||||||
|
"category": "Personal Blogs",
|
||||||
|
"locale": "en-US",
|
||||||
|
"owner": "bakonpancakz",
|
||||||
|
"color": "77, 88, 255"
|
||||||
|
},
|
||||||
|
"articles": [
|
||||||
|
{
|
||||||
|
"base_template": "001-personal-email-server.html",
|
||||||
|
"base_resources": "setting-up-an-email-api",
|
||||||
|
"base_banner": "banner.gif",
|
||||||
|
"color": "94, 167, 1",
|
||||||
|
"slug": "setting-up-an-email-api",
|
||||||
|
"date": "2024-11-06T19:26:40Z",
|
||||||
|
"title": "Setting Up an Email API",
|
||||||
|
"description": "Freaking emails, how do they work?",
|
||||||
|
"author": "bakonpancakz",
|
||||||
|
"tags": [
|
||||||
|
"PERSONAL",
|
||||||
|
"GO"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module bakonpancakz/homepage
|
||||||
|
|
||||||
|
go 1.25.2
|
||||||
|
|
||||||
|
require github.com/joho/godotenv v1.5.1
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package include
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed public/*
|
||||||
|
embedPublic embed.FS
|
||||||
|
//go:embed templates/*
|
||||||
|
embedTemplates embed.FS
|
||||||
|
|
||||||
|
Embedded = true
|
||||||
|
Templates fs.FS
|
||||||
|
Public fs.FS
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if strings.Contains(os.Args[0], "go-build") {
|
||||||
|
Embedded = false
|
||||||
|
}
|
||||||
|
if Embedded {
|
||||||
|
Templates = embedTemplates
|
||||||
|
Public = embedPublic
|
||||||
|
} else {
|
||||||
|
log.Println("[include] In Debug Mode, using local filesystem!")
|
||||||
|
Templates = os.DirFS("include/templates")
|
||||||
|
Public = os.DirFS("include/public")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PreparePath(fsys fs.FS, filepath string) string {
|
||||||
|
filepath = path.Clean(filepath)
|
||||||
|
if Embedded {
|
||||||
|
// If using embed, prepend the folder name automatically if missing
|
||||||
|
switch {
|
||||||
|
case fsys == Public && !strings.HasPrefix(filepath, "public/"):
|
||||||
|
filepath = path.Join("public", filepath)
|
||||||
|
case fsys == Templates && !strings.HasPrefix(filepath, "templates/"):
|
||||||
|
filepath = path.Join("templates", filepath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filepath
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadFile(fsys fs.FS, filepath string) ([]byte, error) {
|
||||||
|
f, err := fsys.Open(PreparePath(fsys, filepath))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
return io.ReadAll(f)
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 99.11 94.5" fill="#f0f0f0">
|
||||||
|
<path d="M99.01,35.85c-.24-.72-.86-1.25-1.62-1.36l-31.83-4.57L51.29,1.11c-.34-.68-1.03-1.11-1.79-1.11h0c-.76,0-1.46.43-1.79,1.12l-14.18,28.85L1.71,34.65c-.75.11-1.38.64-1.61,1.36-.23.72-.04,1.52.51,2.05l23.06,22.41-5.38,31.7c-.13.75.18,1.51.8,1.96.35.25.76.38,1.17.38.32,0,.64-.08.93-.23l28.43-15.01,28.48,14.92c.67.35,1.49.29,2.11-.16.62-.45.92-1.21.79-1.96l-5.49-31.68,22.99-22.48c.54-.53.74-1.33.5-2.05Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 502 B |
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
# \_/
|
||||||
|
# ()o_o) <( beep boop )
|
||||||
|
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
(() => { // Table of Content Highlights
|
||||||
|
|
||||||
|
/** @type {HTMLDivElement | null} */
|
||||||
|
const parentChapter = document.querySelector("div.article-chapters")
|
||||||
|
/** @type {HTMLDivElement | null} */
|
||||||
|
const parentArticle = document.querySelector("div.article-content")
|
||||||
|
|
||||||
|
if (!parentChapter || !parentArticle) return
|
||||||
|
|
||||||
|
/** @type {Array<{ o: number; e: Array<Element | null> }>} */
|
||||||
|
let scrollSections = []
|
||||||
|
let scrollHeader = null
|
||||||
|
let scrollOffset = 0
|
||||||
|
|
||||||
|
// Generate Table of Contents
|
||||||
|
for (const child of parentArticle.children) {
|
||||||
|
const offset = (child.getBoundingClientRect().top + window.scrollY + scrollOffset) | 0
|
||||||
|
const normal = child.textContent.toString().toLocaleLowerCase().replaceAll(" ", "-")
|
||||||
|
|
||||||
|
if (child.classList.contains("element-header")) {
|
||||||
|
const anchor = document.createElement("a")
|
||||||
|
anchor.classList.add("chapter")
|
||||||
|
anchor.textContent = child.textContent
|
||||||
|
anchor.href = "#" + normal
|
||||||
|
child.id = normal
|
||||||
|
parentChapter.appendChild(anchor)
|
||||||
|
|
||||||
|
scrollSections.push({ o: offset, e: [anchor] })
|
||||||
|
scrollHeader = anchor
|
||||||
|
scrollOffset += 36 // bandaid fix for desync
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child.classList.contains("element-subheader")) {
|
||||||
|
const anchor = document.createElement("a")
|
||||||
|
anchor.classList.add("section")
|
||||||
|
anchor.textContent = child.textContent
|
||||||
|
anchor.href = "#" + normal
|
||||||
|
child.id = normal
|
||||||
|
parentChapter.appendChild(anchor)
|
||||||
|
|
||||||
|
scrollSections.push({ o: offset, e: [scrollHeader, anchor] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Highlight when Page Scrolls
|
||||||
|
function pageScroll() {
|
||||||
|
const winoffset = window.innerHeight / 5
|
||||||
|
const position = window.scrollY + winoffset
|
||||||
|
let closest = null
|
||||||
|
let closestDist = Infinity
|
||||||
|
|
||||||
|
// Find Closest Header
|
||||||
|
for (const section of scrollSections) {
|
||||||
|
const dist = Math.abs(position - section.o)
|
||||||
|
if (dist < closestDist) {
|
||||||
|
closestDist = dist
|
||||||
|
closest = section
|
||||||
|
}
|
||||||
|
for (const elem of section.e) {
|
||||||
|
// Reset Styling
|
||||||
|
elem && elem.removeAttribute("selected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply Header Styling
|
||||||
|
if (closest) {
|
||||||
|
for (const elem of closest.e) {
|
||||||
|
elem && elem.setAttribute("selected", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener("scroll", pageScroll)
|
||||||
|
pageScroll()
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
||||||
|
(() => { // Share Button
|
||||||
|
|
||||||
|
/** @type {HTMLButtonElement | null} */
|
||||||
|
const shareButton = document.querySelector("a#button-share")
|
||||||
|
/** @type {HTMLDialogElement | null} */
|
||||||
|
const shareModal = document.querySelector("dialog.layout-dialog")
|
||||||
|
|
||||||
|
// UX: Close Modal if Background is clicked or pressed
|
||||||
|
shareModal && shareModal.addEventListener("click", ev => {
|
||||||
|
if (ev.target === shareModal) {
|
||||||
|
shareModal.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// UX: Open Native Modal on Mobile, Custom Modal on Desktop
|
||||||
|
shareButton && shareButton.addEventListener("click", ev => {
|
||||||
|
ev.preventDefault()
|
||||||
|
|
||||||
|
// Use Native Modal
|
||||||
|
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent)
|
||||||
|
if (isMobile) {
|
||||||
|
const metaTitle = document.querySelector(`meta[property="og:title"]`)?.getAttribute("content")
|
||||||
|
const metaURL = document.querySelector(`meta[property="og:url"]`)?.getAttribute("content")
|
||||||
|
if (typeof (metaURL) !== "string" || typeof (metaTitle) !== "string") {
|
||||||
|
alert("Missing Required Metadata")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
navigator.share({ title: metaTitle, url: metaURL })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Desktop Modal
|
||||||
|
if (shareModal) {
|
||||||
|
shareModal.showModal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})();
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
div.layout-content {
|
||||||
|
margin-bottom: 50vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
dialog.layout-dialog {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 999;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(var(--rgb-background), .5);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.layout-modal {
|
||||||
|
width: 480px;
|
||||||
|
height: fit-content;
|
||||||
|
position: relative;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
padding: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: start;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modal-header>p {
|
||||||
|
color: rgba(var(--rgb-accent), 1);
|
||||||
|
font-size: x-large;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modal-header>form>button {
|
||||||
|
background: transparent;
|
||||||
|
color: rgba(var(--rgb-text), 1);
|
||||||
|
font-size: x-large;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modal-header>form>button:hover,
|
||||||
|
div.modal-header>form>button:focus-visible {
|
||||||
|
color: rgba(var(--rgb-accent), 1);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modal-content {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modal-content>input {
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid rgba(var(--rgb-accent), 1);
|
||||||
|
background: transparent;
|
||||||
|
flex-basis: 100%;
|
||||||
|
font-family: monospace;
|
||||||
|
color: rgba(var(--rgb-text), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modal-content>a {
|
||||||
|
fill: rgba(var(--rgb-text), 1);
|
||||||
|
border: 1px solid rgba(var(--rgb-accent), 1);
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 8px 0;
|
||||||
|
flex-basis: 100%;
|
||||||
|
max-width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modal-content>a:hover,
|
||||||
|
div.modal-content>a:focus-visible {
|
||||||
|
background: rgba(var(--rgb-accent), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modal-content>a>svg {
|
||||||
|
fill: rgba(var(--rgb-accent), 1);
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modal-content>a:hover>svg,
|
||||||
|
div.modal-content>a:focus-visible>svg {
|
||||||
|
fill: rgba(var(--rgb-text), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation */
|
||||||
|
div.article-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.article-actions>a {
|
||||||
|
border: 2px solid rgba(var(--rgb-accent), 1);
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex-basis: 100%;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.article-actions>a>svg {
|
||||||
|
fill: rgba(var(--rgb-accent), 1);
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.article-actions>a:hover,
|
||||||
|
div.article-actions>a:focus-visible {
|
||||||
|
background: rgba(var(--rgb-accent), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.article-actions>a:hover>svg,
|
||||||
|
div.article-actions>a:focus-visible>svg {
|
||||||
|
fill: rgba(var(--rgb-text), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.article-chapters {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.article-chapters>a.chapter,
|
||||||
|
div.article-chapters>a.section {
|
||||||
|
transition: var(--transition-time) ease-in-out all;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.article-chapters>a.chapter {
|
||||||
|
margin-left: 16px;
|
||||||
|
transform: skew(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.article-chapters>a.chapter:hover,
|
||||||
|
div.article-chapters>a.chapter:focus-visible {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.article-chapters>a.chapter[selected] {
|
||||||
|
position: relative;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.article-chapters>a.chapter[selected]::before {
|
||||||
|
content: '>';
|
||||||
|
color: rgba(var(--rgb-accent), 1);
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
position: absolute;
|
||||||
|
left: -16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.article-chapters>a.section {
|
||||||
|
margin-left: 32px;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.article-chapters>a.section:hover,
|
||||||
|
div.article-chapters>a.section:focus-visible {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.article-chapters>a.section[selected] {
|
||||||
|
color: rgba(var(--rgb-accent), 1);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Article */
|
||||||
|
div.article-header {
|
||||||
|
width: 100%;
|
||||||
|
height: fit-content;
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.header-banner {
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
aspect-ratio: 3/1;
|
||||||
|
border-bottom: 4px solid rgba(var(--rgb-accent), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.header-title {
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 16px 0 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.header-chevrons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.article-content {
|
||||||
|
padding: 16px;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Elements */
|
||||||
|
div.element-divider {
|
||||||
|
position: relative;
|
||||||
|
background-color: rgba(var(--rgb-accent), 1);
|
||||||
|
height: 2px;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.element-divider::before {
|
||||||
|
content: '';
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
position: absolute;
|
||||||
|
height: 16px;
|
||||||
|
width: 24px;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
background: url('/public/divider.svg') rgba(var(--rgb-background), 1) no-repeat center;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.element-header {
|
||||||
|
font-size: x-large;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgb(var(--rgb-accent));
|
||||||
|
}
|
||||||
|
|
||||||
|
p.element-subheader {
|
||||||
|
text-decoration: underline;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgb(var(--rgb-accent));
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/* Section: Introduction */
|
||||||
|
div.browser-introduction {
|
||||||
|
display: grid;
|
||||||
|
padding: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.intro-header {
|
||||||
|
font-size: large;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.intro-banner {
|
||||||
|
margin: auto;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.intro-disclaimer {
|
||||||
|
font-size: small;
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(var(--rgb-text), 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.intro-dots {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section: Items */
|
||||||
|
/* div.browser-content {} */
|
||||||
|
|
||||||
|
a.browser-item {
|
||||||
|
display: grid;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.browser-item:not(:hover) .effect-rhombus-animated {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item-background,
|
||||||
|
div.item-foreground,
|
||||||
|
a.item-redirect {
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item-background {
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
background-image: var(--background);
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
filter: grayscale(1) brightness(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item-foreground {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.item-description {
|
||||||
|
font-size: small;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section: Footer */
|
||||||
|
div.browser-footer {
|
||||||
|
padding: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.browser-footer>p {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-family: monospace;
|
||||||
|
color: rgba(var(--rgb-text), 0.8);
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
:root {
|
||||||
|
--transition-time: 200ms;
|
||||||
|
--rgb-background: 0, 0, 0;
|
||||||
|
--rgb-text: 240, 240, 240;
|
||||||
|
--rgb-accent: 70, 70, 70;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: rgba(var(--rgb-background), 1);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
p,
|
||||||
|
span {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
line-height: 1.5em;
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(var(--rgb-text), 1);
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Global Effects */
|
||||||
|
.effect-caution {
|
||||||
|
position: relative;
|
||||||
|
border: 4px solid rgba(var(--rgb-accent), 1);
|
||||||
|
background-color: rgba(var(--rgb-background), 1);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-caution::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: -16px;
|
||||||
|
bottom: -16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
filter: brightness(0.5);
|
||||||
|
background: repeating-linear-gradient(45deg, rgba(var(--rgb-accent), 1) 0, rgba(var(--rgb-accent), 1) 8px, rgba(var(--rgb-background), 1) 8px, rgba(var(--rgb-background), 1) 16px);
|
||||||
|
clip-path: polygon(calc(100% - 11px) 0, 100% 0, 100% 100%, 0 100%, 0 calc(100% - 11px), calc(100% - 11px) calc(100% - 11px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-docked {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
padding: 0 16px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-docked::before {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: rgba(var(--rgb-accent), 0.7);
|
||||||
|
clip-path: polygon(12px 60%, calc(100% - 12px) 60%, 100% 100%, 0 100%);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-rhombus-animated,
|
||||||
|
.effect-rhombus-left,
|
||||||
|
.effect-rhombus-right {
|
||||||
|
position: relative;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0 16px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-rhombus-animated::before,
|
||||||
|
.effect-rhombus-left::before,
|
||||||
|
.effect-rhombus-right::before {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: rgba(var(--rgb-accent), .8);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-rhombus-left {
|
||||||
|
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 100%, 8px 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-rhombus-right {
|
||||||
|
clip-path: polygon(8px 0, 100% 0, calc(100% - 8px) 100%, 0 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-rhombus-animated {
|
||||||
|
animation: kf-rhombus 1s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes kf-rhombus {
|
||||||
|
0% {
|
||||||
|
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 100%, 8px 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
clip-path: polygon(8px 0, 100% 0, calc(100% - 8px) 100%, 0 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.effect-chevron-container {
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-chevron-point-left,
|
||||||
|
.effect-chevron-point-right {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: rgba(var(--rgb-accent), 1);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
height: 24px;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-chevron-point-right {
|
||||||
|
margin-right: -4px;
|
||||||
|
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 50%, calc(100% - 8px) 100%, 0 100%, 8px 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.effect-chevron-container>.effect-chevron-point-right:first-child {
|
||||||
|
padding-left: 8px;
|
||||||
|
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 50%, calc(100% - 8px) 100%, 0 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-chevron-point-left {
|
||||||
|
margin-left: -4px;
|
||||||
|
clip-path: polygon(8px 0, 100% 0, calc(100% - 8px) 50%, 100% 100%, 8px 100%, 0 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.effect-chevron-container>.effect-chevron-point-left:last-child {
|
||||||
|
padding-right: 8px;
|
||||||
|
clip-path: polygon(8px 0, 100% 0, 100% 100%, 8px 100%, 0 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
span.effect-dot {
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
position: relative;
|
||||||
|
align-self: center;
|
||||||
|
border-radius: 100%;
|
||||||
|
background-color: rgba(var(--color), 0.8);
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.effect-dot::before {
|
||||||
|
content: '';
|
||||||
|
height: 10px;
|
||||||
|
width: 10px;
|
||||||
|
border-radius: 100%;
|
||||||
|
position: absolute;
|
||||||
|
background-color: rgba(var(--color), 1);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page Layout */
|
||||||
|
div.layout-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
min-width: 1024px;
|
||||||
|
max-width: 1024px;
|
||||||
|
width: 1024px;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.layout-navigation {
|
||||||
|
min-width: 286px;
|
||||||
|
max-width: 286px;
|
||||||
|
width: 100%;
|
||||||
|
height: fit-content;
|
||||||
|
position: sticky;
|
||||||
|
top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.layout-content {
|
||||||
|
width: 100%;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout: Navigation */
|
||||||
|
div.navigation-header {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.navigation-title {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.navigation-description {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 0;
|
||||||
|
text-align: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* div.navigation-links {} */
|
||||||
|
|
||||||
|
a.navigation-item {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-bottom: 4px solid rgba(var(--rgb-accent), 1);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
background-size: 100% 300%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-image: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
a.navigation-item:first-child {
|
||||||
|
border-top: 4px solid rgba(var(--rgb-accent), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.navigation-content {
|
||||||
|
padding: 16px;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.navigation-footer {
|
||||||
|
display: flex;
|
||||||
|
padding: 16px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.footer-item {
|
||||||
|
color: rgba(var(--rgb-accent), 1);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.footer-item:hover,
|
||||||
|
a.footer-item:focus-visible {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.footer-views {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.footer-views img {
|
||||||
|
height: 24px;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/* Local Version of following stylesheet to save on bandwidth while developing locally :3 */
|
||||||
|
/* https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap */
|
||||||
|
|
||||||
|
/* devanagari */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/pxiEyp8kv8JHgFVrJJbecmNE.woff2) format('woff2');
|
||||||
|
unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/pxiEyp8kv8JHgFVrJJnecmNE.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/pxiEyp8kv8JHgFVrJJfecg.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* devanagari */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/pxiByp8kv8JHgFVrLGT9Z11lFc-K.woff2) format('woff2');
|
||||||
|
unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/pxiByp8kv8JHgFVrLGT9Z1JlFc-K.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/pxiByp8kv8JHgFVrLGT9Z1xlFQ.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* devanagari */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/pxiByp8kv8JHgFVrLEj6Z11lFc-K.woff2) format('woff2');
|
||||||
|
unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/pxiByp8kv8JHgFVrLEj6Z1JlFc-K.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/pxiByp8kv8JHgFVrLEj6Z1xlFQ.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* devanagari */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/pxiByp8kv8JHgFVrLCz7Z11lFc-K.woff2) format('woff2');
|
||||||
|
unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/pxiByp8kv8JHgFVrLCz7Z1JlFc-K.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/pxiByp8kv8JHgFVrLCz7Z1xlFQ.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
{{ define "ArticleContent" }}
|
||||||
|
|
||||||
|
<p class="element-header">Introduction</p>
|
||||||
|
<p class="element-paragraph">
|
||||||
|
If you're reading this... I haven't written the article yet :P
|
||||||
|
</p>
|
||||||
|
<p class="element-paragraph">
|
||||||
|
Luctus montes donec mauris. Vehicula suspendisse condimentum massa fringilla at conubia.
|
||||||
|
Senectus laoreet praesent in, porttitor sollicitudin luctus orci scelerisque egestas malesuada faucibus a.
|
||||||
|
Arcu imperdiet libero imperdiet nulla libero natoque eu fusce ultricies vehicula nisi.
|
||||||
|
Torquent lacus sodales inceptos dignissim sed, mauris feugiat erat.
|
||||||
|
Phasellus at tincidunt sociosqu potenti risus aliquam non litora.
|
||||||
|
Commodo mi fermentum, id arcu dictum convallis.
|
||||||
|
Vehicula convallis, proin fames molestie potenti congue penatibus feugiat porttitor.
|
||||||
|
Habitant justo lacinia, curabitur nascetur!
|
||||||
|
Dolor nunc accumsan imperdiet penatibus?
|
||||||
|
Amet cursus faucibus dui vestibulum nascetur quam.
|
||||||
|
</p>
|
||||||
|
<div class="element-divider"></div>
|
||||||
|
|
||||||
|
<p class="element-header">Prerequisites</p>
|
||||||
|
<p class="element-paragraph">
|
||||||
|
Praesent augue ligula amet nisl. Litora lectus consectetur aliquet.
|
||||||
|
Sociis iaculis dolor nostra pulvinar felis condimentum netus.
|
||||||
|
Suscipit enim habitant sed eros etiam, ut odio sed.
|
||||||
|
Fringilla quam tempus nisl natoque parturient dolor curabitur tellus.
|
||||||
|
Lacus conubia quis inceptos tristique ultricies cum consequat.
|
||||||
|
Consequat phasellus proin dolor molestie nec quam cursus mollis vel?
|
||||||
|
Eros tempor sollicitudin tortor litora nunc.
|
||||||
|
Sem scelerisque semper id ac auctor fusce quis erat imperdiet convallis donec eget!
|
||||||
|
Pellentesque tempor nunc ipsum imperdiet quam ipsum habitant feugiat.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="element-subheader">Hosting</p>
|
||||||
|
<p class="element-paragraph">
|
||||||
|
Odio nibh penatibus erat sociosqu eget iaculis. Ante curabitur bibendum ligula?
|
||||||
|
Netus aliquet cursus interdum pharetra. Magnis elementum nunc luctus eu nam sit habitant etiam vestibulum in.
|
||||||
|
Lacus nisl ullamcorper molestie! Dignissim, vulputate porta pulvinar duis posuere.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="element-subheader">Domains</p>
|
||||||
|
<p class="element-paragraph">
|
||||||
|
Curae; laoreet cum duis turpis, feugiat per integer.
|
||||||
|
Sit in blandit himenaeos. Penatibus fringilla imperdiet, luctus dapibus elementum.
|
||||||
|
Felis senectus pulvinar a natoque risus.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="element-subheader">Credentials</p>
|
||||||
|
<p class="element-paragraph">
|
||||||
|
Mauris habitasse nulla accumsan fermentum fermentum.
|
||||||
|
Magnis amet amet risus senectus in curae;.
|
||||||
|
Elementum ac montes sagittis vivamus malesuada elementum senectus ultrices scelerisque fusce.
|
||||||
|
In nulla platea.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="element-divider"></div>
|
||||||
|
|
||||||
|
<p class="element-header">Setup DNS</p>
|
||||||
|
<p class="element-paragraph">
|
||||||
|
Nisi vitae vehicula etiam nam auctor, accumsan purus ridiculus molestie.
|
||||||
|
Ante malesuada condimentum aliquam etiam auctor arcu porttitor parturient nullam accumsan bibendum?
|
||||||
|
Vitae arcu at mattis. Volutpat himenaeos class vitae elit, luctus sociosqu semper?
|
||||||
|
Praesent taciti lobortis tortor laoreet magnis tortor sagittis porta facilisi vivamus dictum scelerisque.
|
||||||
|
Consectetur elit enim malesuada commodo proin facilisi sagittis!
|
||||||
|
Malesuada quam duis non elit himenaeos sem.
|
||||||
|
Luctus suspendisse senectus mollis aliquet aliquam pellentesque conubia condimentum suspendisse aptent litora potenti.
|
||||||
|
Curabitur sociis lacus turpis, laoreet ligula consequat luctus eu auctor auctor sagittis.
|
||||||
|
Turpis nibh feugiat lorem donec quis bibendum.
|
||||||
|
Himenaeos sapien interdum aptent.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="element-header">Setup Linux</p>
|
||||||
|
<p class="element-paragraph">
|
||||||
|
Convallis pharetra vitae tincidunt phasellus bibendum porta, praesent dictum aliquet volutpat.
|
||||||
|
Lobortis semper eget consectetur elementum elit hendrerit natoque mauris taciti ante ac augue.
|
||||||
|
Ullamcorper viverra vestibulum vehicula nisi metus vestibulum curabitur vehicula et.
|
||||||
|
Phasellus ligula vitae posuere penatibus ullamcorper lorem phasellus metus.
|
||||||
|
Scelerisque ac lobortis gravida primis?
|
||||||
|
Lacus porttitor ullamcorper quisque viverra molestie.
|
||||||
|
Lacinia bibendum facilisi tempor cubilia felis eros!
|
||||||
|
Etiam blandit lacinia leo urna.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{ end }}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="theme-color" content="{{ .Site.Color }}">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="description" content="A small corner of the web for odd tech experiments and the occasional questionable idea. Enjoy in moderation!">
|
||||||
|
<link rel="stylesheet" href="/public/styles/default.css?v={{ .Version }}">
|
||||||
|
<link rel="stylesheet" href="/public/styles/font_poppins.css?v={{ .Version }}">
|
||||||
|
{{ block "PageStyles" . }}{{ end }}
|
||||||
|
<title>{{ .Title }} - {{ .Site.Name }}</title>
|
||||||
|
<meta property="og:locale" content="{{ .Site.Locale }}">
|
||||||
|
{{ block "PageHead" . }}{{ end }}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="--rgb-accent: {{ if .Article }}{{ .Article.Color }}{{ else }}{{ .Site.Color }}{{ end }}">
|
||||||
|
<div class="layout-wrapper">
|
||||||
|
|
||||||
|
<!-- Navigation -->
|
||||||
|
<div class="layout-navigation effect-caution">
|
||||||
|
<div class="navigation-header">
|
||||||
|
<a class="navigation-title effect-docked" href="/">{{ .Site.Name }}</a>
|
||||||
|
<p class="navigation-description">{{ .Site.Description }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="navigation-links" style="--background: url('/public/anchors.gif?v={{ .Version}}');">
|
||||||
|
<a style="background-position-y: 0%;" class="navigation-item" href="/blog">Blog</a>
|
||||||
|
<a style="background-position-y: 50%;" class="navigation-item" href="http://git.panca.kz/">Codebase</a>
|
||||||
|
<a style="background-position-y: 100%;" class="navigation-item" href="http://stickers.panca.kz/">Stickerboard</a>
|
||||||
|
</div>
|
||||||
|
<div class="navigation-content">
|
||||||
|
{{ block "NavContent" . }}{{ end }}
|
||||||
|
</div>
|
||||||
|
<div class="navigation-footer">
|
||||||
|
<a class="footer-item" href="/rss.xml">RSS</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="layout-content effect-caution">
|
||||||
|
{{ block "PageContent" . }}{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{{ block "PageBody" . }}{{ end }}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
{{ define "PageStyles" }}
|
||||||
|
<link rel="stylesheet" href="/public/styles/blog_article.css?v={{ .Version }}">
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "PageHead" }}
|
||||||
|
<meta property="og:type" content="article">
|
||||||
|
<meta property="og:title" content="{{ .Article.Title }}">
|
||||||
|
<meta property="og:image" content="{{ .Site.Host }}{{ .Article.BaseBanner }}">
|
||||||
|
<meta property="og:url" content="{{ .Site.Host }}/blog/{{ .Article.Slug }}">
|
||||||
|
<meta property="og:description" content="{{ .Article.Description }}">
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "PageBody" }}
|
||||||
|
<dialog class="layout-dialog">
|
||||||
|
<div class="layout-modal effect-caution">
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<p>Share</p>
|
||||||
|
<form method="dialog" autofocus>
|
||||||
|
<button>✕</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-content">
|
||||||
|
<input type="text" value="{{ .Site.Host }}/blog/{{ .Article.Slug }}" onclick="this.select()" readonly>
|
||||||
|
<a title="Share on X" target="_blank" href="https://twitter.com/intent/tweet?url={{ .Site.Host }}/blog/{{ .Article.Slug }}&text={{ .Article.Title }}&via={{ .Site.Owner }}">
|
||||||
|
{{ template "logo_x.svg" . }}
|
||||||
|
</a>
|
||||||
|
<a title="Share on Facebook" target="_blank" href="https://www.facebook.com/sharer/sharer.php?u={{ .Site.Host }}/blog/{{ .Article.Slug }}">
|
||||||
|
{{ template "logo_facebook.svg" . }}
|
||||||
|
</a>
|
||||||
|
<a title="Share on LinkedIn" target="_blank" href="https://www.linkedin.com/shareArticle?url={{ .Site.Host }}/blog/{{ .Article.Slug }}">
|
||||||
|
{{ template "logo_linkedin.svg" . }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
<script src="/public/scripts/blog_article.js?v={{ .Version }}"></script>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "NavContent" }}
|
||||||
|
<div class="article-actions">
|
||||||
|
<a title="Visit Homepage" href="/blog">
|
||||||
|
{{ template "icon_home.svg" . }}
|
||||||
|
</a>
|
||||||
|
<a title="Return To Top" href="#" class="mobile-hide">
|
||||||
|
{{ template "icon_top.svg" . }}
|
||||||
|
</a>
|
||||||
|
<a title="Share Article" href="{{ .Site.Host }}/blog/{{ .Article.Slug }}" id="button-share">
|
||||||
|
{{ template "icon_share.svg" . }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="article-chapters">
|
||||||
|
<!-- To Be Generated by Article Script -->
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "PageContent" }}
|
||||||
|
<h1 id="#" hidden><!-- Top Anchor --></h1>
|
||||||
|
|
||||||
|
<div class="article-header">
|
||||||
|
<img class="header-banner" alt="Article Banner" src="/public/articles/{{ .Article.BaseResources }}/{{ .Article.BaseBanner }}?v={{ $.Version }}">
|
||||||
|
<p class="header-title effect-docked">{{ .Article.Title }}</p>
|
||||||
|
<div class="header-chevrons">
|
||||||
|
<div class="effect-chevron-container header-about">
|
||||||
|
<span class="effect-chevron-point-right">{{ .Article.Author }}</span>
|
||||||
|
<span class="effect-chevron-point-right">{{ .Article.Date.Format "Jan 02 2006" }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="effect-chevron-container header-tags">
|
||||||
|
{{ range .Article.Tags }}
|
||||||
|
<span class="effect-chevron-point-left">{{ . }}</span>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="article-content">
|
||||||
|
{{ block "ArticleContent" . }}{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
{{ template "base.html" . }}
|
||||||
|
|
||||||
|
{{ define "PageStyles" }}
|
||||||
|
<link rel="stylesheet" href="/public/styles/blog_browser.css?v={{ .Version }}">
|
||||||
|
<link rel="stylesheet" href="/public/styles/blog_article.css?v={{ .Version }}">
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "PageContent" }}
|
||||||
|
|
||||||
|
<!-- Introduction & Disclaimer -->
|
||||||
|
<div class="browser-introduction">
|
||||||
|
<p class="intro-header effect-rhombus-right">Welcome to my blog!</p>
|
||||||
|
<img class="intro-banner" src="/public/banner_blog.gif?v={{ .Version }}" alt="Blog Banner" width="520" height="292">
|
||||||
|
<p class="intro-paragraph">
|
||||||
|
This corner of the internet contains ramblings about random experiments
|
||||||
|
that more than likely occured on a bright sunday afternoon. Nothing here
|
||||||
|
is meant to be perfect, but I do hope to provide the odd spark of insight.
|
||||||
|
<br><br>
|
||||||
|
I assume no responsibility for bodily harm, wasted time, or raised eyebrows
|
||||||
|
that may result from consuming these articles. Enjoy in Moderation! Have fun!
|
||||||
|
</p>
|
||||||
|
<div class="element-divider"></div>
|
||||||
|
<div class="intro-dots">
|
||||||
|
<span class="effect-dot" style="--color: 94, 167, 1;"></span>
|
||||||
|
<p>Personal</p>
|
||||||
|
<span class="effect-dot" style="--color: 0, 167, 170;"></span>
|
||||||
|
<p>Update</p>
|
||||||
|
<span class="effect-dot" style="--color: 255, 0, 91;"></span>
|
||||||
|
<p>Guide</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Article List -->
|
||||||
|
<div class="browser-content">
|
||||||
|
|
||||||
|
{{ range $.Articles }}
|
||||||
|
<a class="browser-item" href="/blog/{{ .Slug }}" style="--rgb-accent: {{ .Color }};">
|
||||||
|
<div class="item-background" style="--background: url('/public/articles/{{ .BaseResources }}/{{ .BaseBanner }}?v={{ $.Version }}');"></div>
|
||||||
|
<div class="item-foreground">
|
||||||
|
<p class="effect-rhombus-left effect-rhombus-animated">{{ .Title }}</p>
|
||||||
|
<div class="item-tags effect-chevron-container">
|
||||||
|
{{ range .Tags }}
|
||||||
|
<span class="effect-chevron-point-left">{{ . }}</span>
|
||||||
|
{{ end }}
|
||||||
|
<span class="effect-chevron-point-left">{{ .Date.Format "Jan 02 2006" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fortune Cookie -->
|
||||||
|
<div class="browser-footer">
|
||||||
|
<p id="browser-message">If you're reading this, you have JavaScript disabled. Paranoid much? :P</p>
|
||||||
|
<script>
|
||||||
|
const monitor = document.querySelector("p#browser-message")
|
||||||
|
const messages = [
|
||||||
|
"Did you eat something sweet today? Everybody deserves a sweet treat, especially you. :)",
|
||||||
|
"Make sure you clean your keyboard, it's amazingly disgusting what you can find in there.",
|
||||||
|
"Religion aside even God rested on the 7th day, are you treating yourself well?",
|
||||||
|
"If bored try learning about how your favorite Game Console OS works, or is that weird?",
|
||||||
|
"Your projects miss you... maybe poke around? You might rediscover something!",
|
||||||
|
"If somebody has done it before, you can too! And if they got close, you can get closer!",
|
||||||
|
"Bad Idea Counter -> {{ len .Articles }}",
|
||||||
|
]
|
||||||
|
monitor.textContent = messages[Math.floor(Math.random() * messages.length)]
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ end }}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
<title>Page Not Found</title>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--color: {{ .Site.Color }};
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
p {
|
||||||
|
font-family: "MS Gothic", monospace;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #f0f0f0;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
a:visited {
|
||||||
|
color: rgba(var(--color), 1);
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
width: 50%;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover,
|
||||||
|
a:focus-visible {
|
||||||
|
text-decoration: underline;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
width: 200px;
|
||||||
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<p>404: Page Not Found </p>
|
||||||
|
<br>
|
||||||
|
<p>.-.</p>
|
||||||
|
<br>
|
||||||
|
<a href="javascript:window.history.back()">Go Back</a><a href="/">Homepage</a>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// Note: In MS Gothic, backslash shows as ¥ on Windows
|
||||||
|
const catalog = [
|
||||||
|
".(^Д^)/", "(・.・)", "(°Δ°)b", "(・_・)", "(^_^)", "(>_<)",
|
||||||
|
"(o^^)o", "(;-;)", ".(≧o≦)/", "(o_o)", "(^-^*)", "(='X'=)",
|
||||||
|
"( ̄へ ̄)", "(★ω★)", "ᓚᘏᗢ", "✿◠‿◠", "(●'◡'●)", "◑﹏◐"
|
||||||
|
];
|
||||||
|
const kaomoji = catalog[Math.floor(Math.random() * catalog.length)];
|
||||||
|
document.querySelectorAll("p")[1].textContent = kaomoji;
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
<channel>
|
||||||
|
<atom:link href="{{ .Site.Host}}/rss.xml" rel="self" type="application/rss+xml"></atom:link>
|
||||||
|
<link>{{ .Site.Host }}</link>
|
||||||
|
<title><![CDATA[{{ .Site.Name }}]]></title>
|
||||||
|
<description><![CDATA[{{ .Site.Description }}]]></description>
|
||||||
|
<category>{{ .Site.Category }}</category>
|
||||||
|
<language>{{ .Site.Locale }} </language>
|
||||||
|
<generator>teto</generator>
|
||||||
|
<copyright>Copyright 2025 {{ .Site.Owner }}</copyright>
|
||||||
|
<lastBuildDate>{{ .BuildDate.UTC.Format .RFC1123 }}</lastBuildDate>
|
||||||
|
</channel>
|
||||||
|
{{ range $index, $elem := .Articles }}
|
||||||
|
<item>
|
||||||
|
<link>{{ $.Site.Host }}/{{ $elem.Slug }}</link>
|
||||||
|
<guid isPermaLink="true">{{ $.Site.Host }}/{{ $elem.Slug }}</guid>
|
||||||
|
<title><![CDATA[{{ $elem.Title }}]]></title>
|
||||||
|
<description><![CDATA[{{ $elem.Description }}]]></description>
|
||||||
|
<pubDate>{{ $elem.Date.UTC.Format $.RFC1123 }}</pubDate>
|
||||||
|
{{ range $i, $tag := $elem.Tags }}
|
||||||
|
<category>{{ $tag }}</category>
|
||||||
|
{{ end }}
|
||||||
|
</item>
|
||||||
|
{{ end }}
|
||||||
|
</rss>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{{ template "base.html" . }}
|
||||||
|
|
||||||
|
{{ define "PageContent" }}
|
||||||
|
<style>
|
||||||
|
div.placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 480px;
|
||||||
|
display: grid;
|
||||||
|
justify-content: center;
|
||||||
|
justify-items: center;
|
||||||
|
align-content: center;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="placeholder" style="text-align: center;">
|
||||||
|
<img style="image-rendering: pixelated;" src="/public/banner_home.gif?v={{ .Version }}" alt="Lost Sprite">
|
||||||
|
<p>I intended to put something here, but I couldn't come up with anything....</p>
|
||||||
|
<p>So check out my <a href="/blog">blog</a>, <a href="https://git.panca.kz/">code</a> or <a href="https://stickers.panca.kz/">stickerboard</a>!</p>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 32">
|
||||||
|
<!-- @bakonpancakz -->
|
||||||
|
<polygon points="20 0 0 14 6 14 6 32 16 32 16 21 24 21 24 32 34 32 34 14 40 14 20 0" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 186 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30.99 30.99">
|
||||||
|
<!-- @bakonpancakz -->
|
||||||
|
<polygon points="26.22 14.92 26.22 27.25 3.73 27.25 3.73 4.75 16.05 4.75 12.31 1.01 0 1.01 0 1.03 0 3.82 0 28.18 0 30.97 0 30.99 29.95 30.99 29.95 30.97 29.98 30.97 29.98 18.68 26.22 14.92" />
|
||||||
|
<polygon points="14.99 0 21.57 6.59 13.57 14.59 16.4 17.41 24.4 9.41 30.99 16 30.99 0 14.99 0" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 399 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 17.51">
|
||||||
|
<!-- @bakonpancakz -->
|
||||||
|
<polygon points="15 17.51 3 17.51 0 8.01 18 8.01 15 17.51" />
|
||||||
|
<rect y="4" width="18" height="3" />
|
||||||
|
<rect width="18" height="3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 239 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||||
|
<!-- @bakonpancakz -->
|
||||||
|
<polygon points="4.69 20.68 13 20.69 13 32 18.99 31.99 18.99 20.68 27.31 20.68 16 6.74 4.69 20.68" />
|
||||||
|
<rect width="32" height="3.74" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 238 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
|
||||||
|
<mask id="icon">
|
||||||
|
<path fill="white" d="M500,250C500,111.93,388.07,0,250,0S0,111.93,0,250c0,117.24,80.72,215.62,189.61,242.64v-166.24h-51.55v-76.4h51.55v-32.92c0-85.09,38.51-124.53,122.05-124.53,15.84,0,43.17,3.11,54.35,6.21v69.25c-5.9-.62-16.15-.93-28.88-.93-40.99,0-56.83,15.53-56.83,55.9v27.02h81.66l-14.03,76.4h-67.63v171.77c123.79-14.95,219.71-120.35,219.71-248.17Z" />
|
||||||
|
<path fill="black" d="M347.92,326.4l14.03-76.4h-81.66v-27.02c0-40.37,15.84-55.9,56.83-55.9,12.73,0,22.98.31,28.88.93v-69.25c-11.18-3.11-38.51-6.21-54.35-6.21-83.54,0-122.05,39.44-122.05,124.53v32.92h-51.55v76.4h51.55v166.24c19.34,4.8,39.57,7.36,60.39,7.36,10.25,0,20.36-.63,30.29-1.83v-171.77h67.63Z" />
|
||||||
|
</mask>
|
||||||
|
<rect mask="url(#icon)" width="500" height="500" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 835 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 382 382">
|
||||||
|
<path d="M347.445,0H34.555C15.471,0,0,15.471,0,34.555v312.889C0,366.529,15.471,382,34.555,382h312.889 C366.529,382,382,366.529,382,347.444V34.555C382,15.471,366.529,0,347.445,0z M118.207,329.844c0,5.554-4.502,10.056-10.056,10.056 H65.345c-5.554,0-10.056-4.502-10.056-10.056V150.403c0-5.554,4.502-10.056,10.056-10.056h42.806 c5.554,0,10.056,4.502,10.056,10.056V329.844z M86.748,123.432c-22.459,0-40.666-18.207-40.666-40.666S64.289,42.1,86.748,42.1 s40.666,18.207,40.666,40.666S109.208,123.432,86.748,123.432z M341.91,330.654c0,5.106-4.14,9.246-9.246,9.246H286.73 c-5.106,0-9.246-4.14-9.246-9.246v-84.168c0-12.556,3.683-55.021-32.813-55.021c-28.309,0-34.051,29.066-35.204,42.11v97.079 c0,5.106-4.139,9.246-9.246,9.246h-44.426c-5.106,0-9.246-4.14-9.246-9.246V149.593c0-5.106,4.14-9.246,9.246-9.246h44.426 c5.106,0,9.246,4.14,9.246,9.246v15.655c10.497-15.753,26.097-27.912,59.312-27.912c73.552,0,73.131,68.716,73.131,106.472 L341.91,330.654L341.91,330.654z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300.251">
|
||||||
|
<path d="M178.57 127.15 290.27 0h-26.46l-97.03 110.38L89.34 0H0l117.13 166.93L0 300.25h26.46l102.4-116.59 81.8 116.59h89.34M36.01 19.54H76.66l187.13 262.13h-40.66"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 243 B |
@@ -0,0 +1,122 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"bakonpancakz/homepage/env"
|
||||||
|
"bakonpancakz/homepage/include"
|
||||||
|
"bakonpancakz/homepage/routes"
|
||||||
|
|
||||||
|
_ "github.com/joho/godotenv/autoload"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Startup Services
|
||||||
|
var stopCtx, stop = context.WithCancel(context.Background())
|
||||||
|
var stopWg sync.WaitGroup
|
||||||
|
go SetupHTTP(stopCtx, &stopWg)
|
||||||
|
|
||||||
|
// Await Shutdown Signal
|
||||||
|
cancel := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(cancel, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-cancel
|
||||||
|
stop()
|
||||||
|
|
||||||
|
// Begin Shutdown Process
|
||||||
|
timeout, finish := context.WithTimeout(context.Background(), time.Minute)
|
||||||
|
defer finish()
|
||||||
|
go func() {
|
||||||
|
<-timeout.Done()
|
||||||
|
if timeout.Err() == context.DeadlineExceeded {
|
||||||
|
log.Fatalln("[main] Cleanup timeout! Exiting now.")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
stopWg.Wait()
|
||||||
|
log.Println("[main] All done, bye bye!")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetupHTTP(stop context.Context, await *sync.WaitGroup) {
|
||||||
|
|
||||||
|
notFoundHandler := routes.ServeStaticTemplate(&routes.ServeTemplateInfo{
|
||||||
|
FileSystem: include.Templates,
|
||||||
|
FilePaths: []string{"generate_404.html"},
|
||||||
|
Filename: "generate_404.html",
|
||||||
|
Literals: map[string]any{
|
||||||
|
"Site": env.Database.Site,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/", routes.GET_Index(notFoundHandler))
|
||||||
|
mux.HandleFunc("/blog", routes.ServeStaticTemplate(&routes.ServeTemplateInfo{
|
||||||
|
FileSystem: include.Templates,
|
||||||
|
FilePaths: []string{"base.html", "blog_browser.html"},
|
||||||
|
Filename: "blog_browser.html",
|
||||||
|
Literals: map[string]any{
|
||||||
|
"Title": "Browser",
|
||||||
|
"Version": env.VERSION,
|
||||||
|
"Site": env.Database.Site,
|
||||||
|
"Articles": env.Database.Articles,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
mux.HandleFunc("/rss.xml", routes.ServeStaticTemplate(&routes.ServeTemplateInfo{
|
||||||
|
FileSystem: include.Templates,
|
||||||
|
FilePaths: []string{"generate_rss.html"},
|
||||||
|
Filename: "generate_rss.html",
|
||||||
|
ContentType: "application/rss+xml",
|
||||||
|
Literals: map[string]any{
|
||||||
|
"Site": env.Database.Site,
|
||||||
|
"Articles": env.Database.Articles,
|
||||||
|
"RFC1123": time.RFC1123,
|
||||||
|
"BuildDate": time.Now(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
mux.HandleFunc("/blog/{slug}", routes.PrepareBlogArticles(notFoundHandler))
|
||||||
|
mux.HandleFunc("/favicon.ico", routes.ServeStaticFile("favicon.ico"))
|
||||||
|
mux.HandleFunc("/robots.txt", routes.ServeStaticFile("robots.txt"))
|
||||||
|
mux.HandleFunc("/public/{path...}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
routes.ServePublicFile(w, r, path.Clean(r.PathValue("path")))
|
||||||
|
})
|
||||||
|
|
||||||
|
svr := http.Server{
|
||||||
|
Handler: mux,
|
||||||
|
Addr: env.HTTP_ADDRESS,
|
||||||
|
TLSConfig: env.HTTP_TLS,
|
||||||
|
MaxHeaderBytes: 4096,
|
||||||
|
IdleTimeout: 5 * time.Second,
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 30 * time.Second,
|
||||||
|
ReadTimeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown Logic
|
||||||
|
await.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer await.Done()
|
||||||
|
<-stop.Done()
|
||||||
|
svr.Shutdown(context.Background())
|
||||||
|
log.Println("[http] Cleaned up HTTP")
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Server Startup
|
||||||
|
var err error
|
||||||
|
if env.TLS_ENABLED {
|
||||||
|
log.Printf("[http] Bound HTTPS - %s\n", svr.Addr)
|
||||||
|
err = svr.ListenAndServeTLS("", "")
|
||||||
|
} else {
|
||||||
|
log.Printf("[http] Bound HTTP - %s\n", svr.Addr)
|
||||||
|
err = svr.ListenAndServe()
|
||||||
|
}
|
||||||
|
if err != http.ErrServerClosed {
|
||||||
|
log.Fatalln("[http] Listen Error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bakonpancakz/homepage/env"
|
||||||
|
"bakonpancakz/homepage/include"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
var articleCache sync.Map
|
||||||
|
|
||||||
|
func PrepareBlogArticles(notFoundHandler HttpHandler) HttpHandler {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
for _, art := range env.Database.Articles {
|
||||||
|
if strings.Compare(art.Slug, r.PathValue("slug")) == 0 {
|
||||||
|
|
||||||
|
compress := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
|
||||||
|
cacheKeyStandard := fmt.Sprintf("art_%s_%t", art.Slug, true)
|
||||||
|
cacheKeyCompress := fmt.Sprintf("art_%s_%t", art.Slug, false)
|
||||||
|
|
||||||
|
// Check Browser Cache
|
||||||
|
if r.Header.Get("If-None-Match") == env.VERSION {
|
||||||
|
w.WriteHeader(http.StatusNotModified)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Template Cache
|
||||||
|
w.Header().Set("ETag", env.VERSION)
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=3600, must-revalidate")
|
||||||
|
if compress {
|
||||||
|
if v, ok := articleCache.Load(cacheKeyCompress); ok {
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
w.Write(v.([]byte))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if v, ok := articleCache.Load(cacheKeyStandard); ok {
|
||||||
|
w.Write(v.([]byte))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Document
|
||||||
|
tmpl, err := template.ParseFS(
|
||||||
|
include.Templates,
|
||||||
|
include.PreparePath(include.Templates, "base.html"),
|
||||||
|
include.PreparePath(include.Templates, "blog_article.html"),
|
||||||
|
include.PreparePath(include.Templates, fmt.Sprint("articles/", art.BaseTemplate)),
|
||||||
|
include.PreparePath(include.Templates, "icons/logo_facebook.svg"),
|
||||||
|
include.PreparePath(include.Templates, "icons/logo_linkedin.svg"),
|
||||||
|
include.PreparePath(include.Templates, "icons/logo_x.svg"),
|
||||||
|
include.PreparePath(include.Templates, "icons/icon_top.svg"),
|
||||||
|
include.PreparePath(include.Templates, "icons/icon_home.svg"),
|
||||||
|
include.PreparePath(include.Templates, "icons/icon_share.svg"),
|
||||||
|
include.PreparePath(include.Templates, "icons/icon_stack.svg"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("[routes/blog] Template Error:", err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err = tmpl.ExecuteTemplate(&b, "base.html", map[string]any{
|
||||||
|
"Title": art.Title,
|
||||||
|
"Version": env.VERSION,
|
||||||
|
"Site": env.Database.Site,
|
||||||
|
"Article": art,
|
||||||
|
}); err != nil {
|
||||||
|
log.Println("[routes/blog] Template Error:", err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compress Document
|
||||||
|
var c bytes.Buffer
|
||||||
|
g := gzip.NewWriter(&c)
|
||||||
|
g.Write(b.Bytes())
|
||||||
|
g.Close()
|
||||||
|
|
||||||
|
if include.Embedded {
|
||||||
|
articleCache.Store(cacheKeyStandard, b.Bytes())
|
||||||
|
articleCache.Store(cacheKeyCompress, c.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send Document
|
||||||
|
if compress {
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
w.Write(c.Bytes())
|
||||||
|
} else {
|
||||||
|
w.Write(b.Bytes())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notFoundHandler(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bakonpancakz/homepage/env"
|
||||||
|
"bakonpancakz/homepage/include"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GET_Index(notFoundHandler HttpHandler) HttpHandler {
|
||||||
|
|
||||||
|
handler := ServeStaticTemplate(&ServeTemplateInfo{
|
||||||
|
FileSystem: include.Templates,
|
||||||
|
FilePaths: []string{"base.html", "homepage.html"},
|
||||||
|
Filename: "homepage.html",
|
||||||
|
Literals: map[string]any{
|
||||||
|
"Title": "Home",
|
||||||
|
"Version": env.VERSION,
|
||||||
|
"Site": env.Database.Site,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/" {
|
||||||
|
handler(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
notFoundHandler(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bakonpancakz/homepage/include"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"crypto/md5"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HttpHandler func(w http.ResponseWriter, r *http.Request)
|
||||||
|
|
||||||
|
var hashTable sync.Map
|
||||||
|
|
||||||
|
type ServeTemplateInfo struct {
|
||||||
|
FileSystem fs.FS
|
||||||
|
FilePaths []string
|
||||||
|
Filename string
|
||||||
|
Literals map[string]any
|
||||||
|
StatusCode int
|
||||||
|
ContentType string
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareStaticTemplate(info *ServeTemplateInfo, ds *[]byte, dc *[]byte, dh *string) error {
|
||||||
|
|
||||||
|
// Render Document
|
||||||
|
tmpl, err := template.ParseFS(info.FileSystem, info.FilePaths...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := tmpl.ExecuteTemplate(&b, info.Filename, info.Literals); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compress Document
|
||||||
|
var c bytes.Buffer
|
||||||
|
g := gzip.NewWriter(&c)
|
||||||
|
g.Write(b.Bytes())
|
||||||
|
g.Close()
|
||||||
|
|
||||||
|
// Store Document
|
||||||
|
*ds = b.Bytes()
|
||||||
|
*dc = c.Bytes()
|
||||||
|
*dh = fmt.Sprintf("%X", md5.Sum(b.Bytes()))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ServeStaticTemplate(info *ServeTemplateInfo) func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
if info.StatusCode == 0 {
|
||||||
|
info.StatusCode = http.StatusOK
|
||||||
|
}
|
||||||
|
if info.ContentType == "" {
|
||||||
|
info.ContentType = "text/html"
|
||||||
|
}
|
||||||
|
for i, p := range info.FilePaths {
|
||||||
|
info.FilePaths[i] = include.PreparePath(info.FileSystem, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Document
|
||||||
|
var (
|
||||||
|
documentStandard []byte
|
||||||
|
documentCompress []byte
|
||||||
|
documentHash string
|
||||||
|
)
|
||||||
|
if err := prepareStaticTemplate(info, &documentStandard, &documentCompress, &documentHash); err != nil {
|
||||||
|
log.Fatalln("[routes/template] Cannot Prepare Template:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve Function
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !include.Embedded {
|
||||||
|
if err := prepareStaticTemplate(info, &documentStandard, &documentCompress, &documentHash); err != nil {
|
||||||
|
log.Println("[routes/template] Refresh Error:", err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Browser Cache
|
||||||
|
if r.Header.Get("If-None-Match") == documentHash {
|
||||||
|
w.WriteHeader(http.StatusNotModified)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send Document
|
||||||
|
w.Header().Set("ETag", documentHash)
|
||||||
|
w.Header().Set("Content-Type", info.ContentType)
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=3600, must-revalidate")
|
||||||
|
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
w.Write(documentCompress)
|
||||||
|
} else {
|
||||||
|
w.Write(documentStandard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ServeStaticFile(filepath string) HttpHandler {
|
||||||
|
var (
|
||||||
|
fileHash string
|
||||||
|
fileData []byte
|
||||||
|
fileMime string
|
||||||
|
)
|
||||||
|
|
||||||
|
// Prepare File
|
||||||
|
b, err := include.ReadFile(include.Public, filepath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("[routes/static] Cannot Prepare File:", filepath, err)
|
||||||
|
}
|
||||||
|
fileMime = mime.TypeByExtension(path.Ext(filepath))
|
||||||
|
fileHash = fmt.Sprintf("%X", md5.Sum(b))
|
||||||
|
fileData = b
|
||||||
|
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Header.Get("If-None-Match") == fileHash {
|
||||||
|
w.WriteHeader(http.StatusNotModified)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Add("ETag", fileHash)
|
||||||
|
w.Header().Add("X-Robots-Tag", "noindex")
|
||||||
|
w.Header().Add("Content-Type", fileMime)
|
||||||
|
w.Header().Add("Cache-Control", "public, max-age=604800, immutable")
|
||||||
|
w.Write(fileData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ServePublicFile(w http.ResponseWriter, r *http.Request, filepath string) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Cache Hash
|
||||||
|
if hash, ok := hashTable.Load(filepath); ok {
|
||||||
|
if r.Header.Get("If-None-Match") == hash.(string) {
|
||||||
|
w.WriteHeader(http.StatusNotModified)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read File Contents
|
||||||
|
filedata, err := include.ReadFile(include.Public, filepath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println("[http] Read Asset Error:", err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate Cache Hash
|
||||||
|
filehash := fmt.Sprintf("%X", md5.Sum(filedata))
|
||||||
|
hashTable.Store(filepath, filehash)
|
||||||
|
fileMime := mime.TypeByExtension(path.Ext(filepath))
|
||||||
|
|
||||||
|
// Send Contents
|
||||||
|
w.Header().Add("ETag", filehash)
|
||||||
|
w.Header().Add("X-Robots-Tag", "noindex")
|
||||||
|
w.Header().Add("Content-Type", fileMime)
|
||||||
|
w.Header().Add("Cache-Control", "public, max-age=604800, immutable")
|
||||||
|
|
||||||
|
if len(filedata) < (1<<20) &&
|
||||||
|
!strings.Contains(fileMime, "image") &&
|
||||||
|
!strings.Contains(fileMime, "audio") &&
|
||||||
|
!strings.Contains(fileMime, "video") &&
|
||||||
|
strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
g := gzip.NewWriter(w)
|
||||||
|
g.Write(filedata)
|
||||||
|
g.Close()
|
||||||
|
} else {
|
||||||
|
w.Write(filedata)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Build
|
||||||
|
FROM golang:1.25.2-alpine3.22 AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
RUN go build -o homepage.elf main.go
|
||||||
|
#RUN touch homepage.elf
|
||||||
|
|
||||||
|
# Runtime
|
||||||
|
FROM alpine:latest AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /app/homepage.elf .
|
||||||
|
COPY --from=build /app/templates ./templates
|
||||||
|
COPY --from=build /app/public ./public
|
||||||
|
|
||||||
|
ENV HTTP_ADDRESS="0.0.0.0:8080"
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["./homepage.elf"]
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ManifestArticle struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
BannerMain string `json:"banner_main"`
|
||||||
|
BannerList string `json:"banner_list"`
|
||||||
|
BannerDescription string `json:"banner_description"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ManifestNode struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Manifest struct {
|
||||||
|
Links []ManifestNode `json:"links"`
|
||||||
|
Authors []ManifestNode `json:"authors"`
|
||||||
|
Tags []ManifestNode `json:"tags"`
|
||||||
|
Articles []ManifestArticle `json:"articles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CacheInfo struct {
|
||||||
|
HeaderETag string
|
||||||
|
HeaderContentType string
|
||||||
|
ContentPlainText []byte
|
||||||
|
ContentCompressed []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
MANIFEST Manifest
|
||||||
|
MANIFEST_RAW string
|
||||||
|
HTTP_ADDRESS = os.Getenv("HTTP_ADDRESS")
|
||||||
|
CACHE_VERSION = fmt.Sprintf("%X", time.Now().Unix())
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Default Values
|
||||||
|
if HTTP_ADDRESS == "" {
|
||||||
|
HTTP_ADDRESS = "127.0.0.1:8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manifest Parsing
|
||||||
|
data, err := os.ReadFile("public/blog/manifest.json")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Cannot Read Manifest JSON:", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &MANIFEST); err != nil {
|
||||||
|
log.Fatalln("Cannot Parse Manifest JSON:", err)
|
||||||
|
}
|
||||||
|
MANIFEST_RAW = string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Startup Services
|
||||||
|
var stopCtx, stop = context.WithCancel(context.Background())
|
||||||
|
var stopWg sync.WaitGroup
|
||||||
|
go SetupHTTP(stopCtx, &stopWg)
|
||||||
|
|
||||||
|
// Await Shutdown Signal
|
||||||
|
cancel := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(cancel, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-cancel
|
||||||
|
stop()
|
||||||
|
|
||||||
|
// Begin Shutdown Process
|
||||||
|
timeout, finish := context.WithTimeout(context.Background(), time.Minute)
|
||||||
|
defer finish()
|
||||||
|
go func() {
|
||||||
|
<-timeout.Done()
|
||||||
|
if timeout.Err() == context.DeadlineExceeded {
|
||||||
|
log.Fatalln("[main] Cleanup timeout! Exiting now.")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
stopWg.Wait()
|
||||||
|
log.Println("[main] All done, bye bye!")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetupHTTP(stop context.Context, await *sync.WaitGroup) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
loc := map[string]any{
|
||||||
|
"Version": CACHE_VERSION,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Webpage Endpoints //
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/" {
|
||||||
|
ServeTemplate(w, r, "templates/home.html", loc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
ServeTemplate(w, r, "templates/404.html", loc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/blog/{slug...}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
// Although this is a SPA, we still need to render
|
||||||
|
// the OG meta tags for SEO / Social Media Sites
|
||||||
|
|
||||||
|
var article *ManifestArticle
|
||||||
|
for _, art := range MANIFEST.Articles {
|
||||||
|
if strings.EqualFold(art.ID, r.PathValue("slug")) {
|
||||||
|
article = &art
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ServeTemplate(w, r, "templates/blog.html", map[string]any{
|
||||||
|
"Version": CACHE_VERSION,
|
||||||
|
"ManifestRaw": html.EscapeString(MANIFEST_RAW),
|
||||||
|
"Manifest": MANIFEST,
|
||||||
|
"Article": article,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Secret Endpoints //
|
||||||
|
mux.HandleFunc("/secret/sprites", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ServeTemplate(w, r, "templates/secret_sprites.html", loc)
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/secret/picross", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ServeTemplate(w, r, "templates/secret_picross_board.html", loc)
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/secret/picross/award", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ServeTemplate(w, r, "templates/secret_picross_award.html", loc)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Special Endpoints //
|
||||||
|
mux.HandleFunc("/blog/rss.xml", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ServeTemplate(w, r, "templates/rss.xml", map[string]any{
|
||||||
|
"Version": CACHE_VERSION,
|
||||||
|
"Manifest": MANIFEST,
|
||||||
|
"Date": time.Now().UTC().Format(time.RFC1123),
|
||||||
|
"RFC1123": time.RFC1123,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/sitemap.xml", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ServeTemplate(w, r, "templates/sitemap.xml", map[string]any{
|
||||||
|
"Version": CACHE_VERSION,
|
||||||
|
"Manifest": MANIFEST,
|
||||||
|
"Date": time.Now().UTC().Format("2006-01-02"),
|
||||||
|
"Locations": []string{
|
||||||
|
"https://panca.kz/",
|
||||||
|
"https://panca.kz/blog",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/qr", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
redirects := []string{
|
||||||
|
"https://www.youtube.com/watch?v=dQw4w9WgXcQ", // Risk Astley - Never Gonna Give You Up
|
||||||
|
"https://www.youtube.com/watch?v=DnmHG3mJv1o", // Masayoshi Takanaka - Tropic Birds
|
||||||
|
"https://www.youtube.com/watch?v=ZZCt92zLmLI", // Blur - Intermission
|
||||||
|
"https://www.twitch.tv/robcdee", // awesome sauce
|
||||||
|
"https://piapro.net/intl/en.html", // mikudayooo
|
||||||
|
}
|
||||||
|
index := int(time.Now().Unix()/60) % len(redirects) // rotate every minute
|
||||||
|
http.Redirect(w, r, redirects[index], http.StatusTemporaryRedirect)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Resource Endpoints //
|
||||||
|
mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ServeFile(w, r, "favicon.ico")
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ServeFile(w, r, "robots.txt")
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/public/{path...}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ServeFile(w, r, path.Clean(r.PathValue("path")))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create HTTP Server //
|
||||||
|
svr := http.Server{
|
||||||
|
Handler: mux,
|
||||||
|
Addr: HTTP_ADDRESS,
|
||||||
|
MaxHeaderBytes: 4096,
|
||||||
|
IdleTimeout: 5 * time.Second,
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 30 * time.Second,
|
||||||
|
ReadTimeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
await.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer await.Done()
|
||||||
|
<-stop.Done()
|
||||||
|
svr.Shutdown(context.Background())
|
||||||
|
log.Println("[http] Cleaned up HTTP")
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Printf("[http] Listening @ %s\n", svr.Addr)
|
||||||
|
if err := svr.ListenAndServe(); err != http.ErrServerClosed {
|
||||||
|
log.Fatalln("[http] Listen Error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func ServeTemplate(w http.ResponseWriter, r *http.Request, filepath string, locals any) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clientCacheKey := r.Header.Get("If-None-Match")
|
||||||
|
clientCompress := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
|
||||||
|
|
||||||
|
// Render Template //
|
||||||
|
html := bytes.Buffer{}
|
||||||
|
tmpl, err := template.ParseFiles(filepath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[http] Cannot parse template '%s': %s\n", filepath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := tmpl.Execute(&html, locals); err != nil {
|
||||||
|
log.Printf("[http] Cannot render template '%s': %s \n", filepath, err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache Validation //
|
||||||
|
serverCacheKey := fmt.Sprintf("%x", md5.Sum(html.Bytes()))
|
||||||
|
serverFileType := mime.TypeByExtension(path.Ext(filepath))
|
||||||
|
if strings.EqualFold(clientCacheKey, serverCacheKey) {
|
||||||
|
w.WriteHeader(http.StatusNotModified)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream Contents //
|
||||||
|
w.Header().Set("ETag", serverCacheKey)
|
||||||
|
w.Header().Set("Content-Type", serverFileType)
|
||||||
|
// w.Header().Set("Cache-Control", "public, max-age=3600, must-revalidate")
|
||||||
|
|
||||||
|
if clientCompress {
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
pack := gzip.NewWriter(w)
|
||||||
|
pack.Write(html.Bytes())
|
||||||
|
pack.Close()
|
||||||
|
} else {
|
||||||
|
w.Write(html.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ServeFile(w http.ResponseWriter, r *http.Request, filepath string) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open File //
|
||||||
|
file, err := os.Open("./public/" + filepath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("[http] Cannot open file '%s': %s\n", filepath, err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Skip Compression on Large/Media Files //
|
||||||
|
serverFileType := mime.TypeByExtension(path.Ext(filepath))
|
||||||
|
clientCompress := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") &&
|
||||||
|
!strings.Contains(serverFileType, "image") &&
|
||||||
|
!strings.Contains(serverFileType, "audio") &&
|
||||||
|
!strings.Contains(serverFileType, "video")
|
||||||
|
|
||||||
|
// Stream Contents //
|
||||||
|
w.Header().Set("Content-Type", serverFileType)
|
||||||
|
w.Header().Set("X-Robots-Tag", "noindex")
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||||
|
|
||||||
|
if clientCompress {
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
pack := gzip.NewWriter(w)
|
||||||
|
io.Copy(pack, file)
|
||||||
|
pack.Close()
|
||||||
|
} else {
|
||||||
|
io.Copy(w, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="uuid-bba056a5-9009-46f9-846a-ccbc6d996e35" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18.32 30">
|
||||||
|
<g>
|
||||||
|
<path d="M11.55,7.48c-.37-.92-1.57-1.98-2.38-1.98s-2.01,1.06-2.38,1.98c-1.75-.19-3.12-1.68-3.12-3.48S5.03.71,6.77.52c.2.49.62.92.83,1.12.28.26,1,.86,1.68.86.82-.03,1.92-1.07,2.27-1.98,1.75.2,3.11,1.68,3.11,3.48s-1.37,3.29-3.12,3.48Z" style="fill: #2bb673;"/>
|
||||||
|
<path d="M6.5,1.07c.6,1.02,1.89,1.93,2.79,1.93h.04s.02,0,.02,0c.88-.03,1.97-.95,2.5-1.92,1.33.31,2.32,1.5,2.32,2.92s-1,2.62-2.33,2.93c-.57-.99-1.74-1.93-2.67-1.93s-2.1.93-2.67,1.93c-1.33-.3-2.33-1.5-2.33-2.93s1-2.62,2.34-2.93M11.16,0c0,.68-1.17,1.97-1.85,2,0,0-.02,0-.02,0-.74,0-2.12-1.27-2.12-2C4.95,0,3.16,1.79,3.16,4s1.79,4,4,4c0-.71,1.29-2,2-2s2,1.29,2,2c2.21,0,4-1.79,4-4S13.37,0,11.16,0h0Z" style="fill: #231f20;"/>
|
||||||
|
</g>
|
||||||
|
<path d="M17.16,6c3.48,5.64-1.37,24-8,24S-2.32,11.64,1.16,6c1.18-1.9,5.76-2,8-2s6.82.1,8,2Z" style="fill: #f7941d;"/>
|
||||||
|
<g>
|
||||||
|
<path d="M12,27.79c-.37-.69-1.46-2.5-3-2.5-1.54,0-2.62,1.81-3,2.5h6Z" style="fill: #fbb040;"/>
|
||||||
|
<path d="M12.42,28.04h-6.84l.2-.37c.53-.98,1.65-2.63,3.22-2.63h0c1.56,0,2.69,1.65,3.22,2.63l.2.37ZM6.44,27.54h5.14c-.53-.88-1.43-2-2.57-2h0c-1.13,0-2.04,1.13-2.57,2Z" style="fill: #fbb040;"/>
|
||||||
|
</g>
|
||||||
|
<path d="M1.16,12.09c1-.69,3.88-2.5,7.99-2.5,4.12,0,7.01,1.81,8.01,2.5" style="fill: none; stroke: #fbb040; stroke-miterlimit: 10; stroke-width: 3px;"/>
|
||||||
|
<path d="M1.66,17.74c.94-.69,3.64-2.5,7.49-2.5,3.86,0,6.57,1.81,7.51,2.5" style="fill: none; stroke: #fbb040; stroke-miterlimit: 10; stroke-width: 2.5px;"/>
|
||||||
|
<path d="M3.84,23.5l-1.36-1.47c1.12-1.03,3.47-2.76,6.67-2.77h0c3.21,0,5.56,1.73,6.68,2.77l-1.36,1.47c-.9-.83-2.79-2.23-5.32-2.23h0c-2.52,0-4.41,1.4-5.31,2.23Z" style="fill: #fbb040;"/>
|
||||||
|
<path d="M9.16,6c5.53,0,6.27,1.01,6.3,1.05,1.85,2.99.61,11.88-2.41,17.29-1.28,2.29-2.73,3.66-3.89,3.66s-2.61-1.37-3.89-3.66C2.25,18.93,1.01,10.04,2.86,7.05c.03-.04.77-1.05,6.3-1.05M9.16,4c-2.24,0-6.82.1-8,2-3.48,5.64,1.37,24,8,24S20.64,11.64,17.16,6c-1.18-1.9-5.76-2-8-2h0Z" style="fill: #231f20;"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,541 @@
|
|||||||
|
:root {
|
||||||
|
--corner-thickness: 1px;
|
||||||
|
--corner-margin: -16px;
|
||||||
|
--corner-color: #606060;
|
||||||
|
--glass-tint: rgba(255, 255, 255, .075);
|
||||||
|
--glass-blur: 4px;
|
||||||
|
--color-accent: #323232;
|
||||||
|
--color-background: #000;
|
||||||
|
--color-text-default: #f0f0f0;
|
||||||
|
--color-text-hidden: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
span,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
p,
|
||||||
|
a {
|
||||||
|
font-family: 'Terminus', sans-serif;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: 1.5em;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--color-text-default);
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover,
|
||||||
|
a:focus-visible {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
|
||||||
|
div.layout-wrapper {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.layout-background {
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.layout-foreground {
|
||||||
|
margin: auto;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1024px;
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.layout-pane {
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 1;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation Links */
|
||||||
|
|
||||||
|
nav.section-navigation {
|
||||||
|
width: 300px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
max-height: calc(100vh - 64px);
|
||||||
|
padding: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.nav-header {
|
||||||
|
position: relative;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
padding: 4px;
|
||||||
|
padding-right: 16px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.nav-header.open {
|
||||||
|
border-color: var(--color-text-hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.nav-header img {
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.nav-message {
|
||||||
|
color: var(--color-text-hidden);
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.item-nav {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
padding: 12px 8px 12px 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item-nav-text {
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item-nav-text p.item-nav-text-anchor,
|
||||||
|
div.item-nav-text p.item-nav-text-content {
|
||||||
|
font-family: 'Terminus', serif;
|
||||||
|
color: var(--color-text-default);
|
||||||
|
line-height: 1em;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item-nav-text p.item-nav-text-content {
|
||||||
|
color: var(--color-text-hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item-nav-icon {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item-nav-icon div.item-nav-icon-background,
|
||||||
|
div.item-nav-icon img.item-nav-icon-foreground {
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 1;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item-nav-icon div.item-nav-icon-background {
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
z-index: -1;
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item-nav-icon img.item-nav-icon-foreground {
|
||||||
|
width: 16px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation Chapters */
|
||||||
|
|
||||||
|
div.nav-list#chapters {
|
||||||
|
display: grid;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.nav-list#chapters a.section {
|
||||||
|
font-size: small;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Article List */
|
||||||
|
|
||||||
|
div.item-article-count {
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item-article-count p.item-article-count-text {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 8px;
|
||||||
|
line-height: 32px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--color-text-hidden);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item-article-count span.item-article-count-cursor {
|
||||||
|
color: var(--color-text-hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
a.item-article {
|
||||||
|
display: grid;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item-article-layer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item-article-meta {
|
||||||
|
padding: 8px 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.item-article-meta-title {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.item-article-meta-description {
|
||||||
|
color: var(--color-text-hidden);
|
||||||
|
padding-left: 10px;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item-article-meta-tags {
|
||||||
|
display: flex;
|
||||||
|
padding-left: 4px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.item-article-meta-tags>span {
|
||||||
|
color: var(--color-text-hidden);
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.item-article-banner {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
height: 80px;
|
||||||
|
object-fit: cover;
|
||||||
|
aspect-ratio: 3 / 1;
|
||||||
|
mask-image: linear-gradient(-90deg, rgba(0, 0, 0, .5) 0%, transparent 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Article Header */
|
||||||
|
|
||||||
|
article.section-article {
|
||||||
|
width: 600px;
|
||||||
|
max-height: calc(100vh - 64px);
|
||||||
|
overflow-y: scroll;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.article-header {
|
||||||
|
width: 100%;
|
||||||
|
height: fit-content;
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.header-chevrons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.article-content:has(*) {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
img#header-banner {
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
aspect-ratio: 3 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1#header-title {
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 16px 0 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Article Elements */
|
||||||
|
|
||||||
|
h2.element-header {
|
||||||
|
font-size: x-large;
|
||||||
|
font-weight: bold;
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding-bottom: 8px 0;
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3.element-subheader {
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: underline;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.element-paragraph {
|
||||||
|
line-height: 1.25;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
|
||||||
|
.animation-blink {
|
||||||
|
animation: kf-blink 1s infinite step-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes kf-blink {
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-scrollin {
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
text-wrap-mode: nowrap;
|
||||||
|
animation: kf-scrollin 750ms forwards linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes kf-scrollin {
|
||||||
|
0% {
|
||||||
|
width: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-fadein {
|
||||||
|
animation: kf-fadein 500ms forwards linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes kf-fadein {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Effects */
|
||||||
|
|
||||||
|
.effect-glass-wrapper {
|
||||||
|
position: relative;
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-docked {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
padding: 0 16px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-docked::before {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: var(--color-accent);
|
||||||
|
clip-path: polygon(12px 60%, calc(100% - 12px) 60%, 100% 100%, 0 100%);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.effect-chevron-container {
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-chevron-point-left,
|
||||||
|
.effect-chevron-point-right {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
height: 24px;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-chevron-point-right {
|
||||||
|
margin-right: -4px;
|
||||||
|
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 50%, calc(100% - 8px) 100%, 0 100%, 8px 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.effect-chevron-container>.effect-chevron-point-right:first-child {
|
||||||
|
padding-left: 8px;
|
||||||
|
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 50%, calc(100% - 8px) 100%, 0 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-chevron-point-left {
|
||||||
|
margin-left: -4px;
|
||||||
|
clip-path: polygon(8px 0, 100% 0, calc(100% - 8px) 50%, 100% 100%, 8px 100%, 0 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.effect-chevron-container>.effect-chevron-point-left:last-child {
|
||||||
|
padding-right: 8px;
|
||||||
|
clip-path: polygon(8px 0, 100% 0, 100% 100%, 8px 100%, 0 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-glass-container {
|
||||||
|
background: var(--glass-tint);
|
||||||
|
backdrop-filter: blur(var(--glass-blur));
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-glass-corner {
|
||||||
|
position: absolute;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-glass-corner:nth-child(1) {
|
||||||
|
/* Top-left */
|
||||||
|
top: var(--corner-margin);
|
||||||
|
left: var(--corner-margin);
|
||||||
|
border-top: var(--corner-thickness) solid var(--corner-color);
|
||||||
|
border-left: var(--corner-thickness) solid var(--corner-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-glass-corner:nth-child(2) {
|
||||||
|
/* Top-right */
|
||||||
|
top: var(--corner-margin);
|
||||||
|
right: var(--corner-margin);
|
||||||
|
border-top: var(--corner-thickness) solid var(--corner-color);
|
||||||
|
border-right: var(--corner-thickness) solid var(--corner-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-glass-corner:nth-child(3) {
|
||||||
|
/* Bottom-left */
|
||||||
|
bottom: var(--corner-margin);
|
||||||
|
left: var(--corner-margin);
|
||||||
|
border-bottom: var(--corner-thickness) solid var(--corner-color);
|
||||||
|
border-left: var(--corner-thickness) solid var(--corner-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-glass-corner:nth-child(4) {
|
||||||
|
/* Bottom-right */
|
||||||
|
bottom: var(--corner-margin);
|
||||||
|
right: var(--corner-margin);
|
||||||
|
border-bottom: var(--corner-thickness) solid var(--corner-color);
|
||||||
|
border-right: var(--corner-thickness) solid var(--corner-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
|
||||||
|
div.layout-foreground {
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.section-navigation {
|
||||||
|
width: 100%;
|
||||||
|
max-height: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.section-article {
|
||||||
|
width: 100%;
|
||||||
|
max-height: unset;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make Header Slightly Smaller */
|
||||||
|
h1#header-title {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.header-chevrons {
|
||||||
|
display: grid;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.effect-chevron-container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-chevron-point-left,
|
||||||
|
.effect-chevron-point-right {
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-chevron-point-right {
|
||||||
|
margin-right: unset;
|
||||||
|
clip-path: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.effect-chevron-container>.effect-chevron-point-right:first-child {
|
||||||
|
padding-left: 16px;
|
||||||
|
clip-path: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-chevron-point-left {
|
||||||
|
margin-left: unset;
|
||||||
|
clip-path: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.effect-chevron-container>.effect-chevron-point-left:last-child {
|
||||||
|
padding-right: 16px;
|
||||||
|
clip-path: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove Filters from Navigation */
|
||||||
|
button.nav-header[for='authors'],
|
||||||
|
button.nav-header[for='tags'],
|
||||||
|
div.nav-list#authors,
|
||||||
|
div.nav-list#tags {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-glass-wrapper {
|
||||||
|
margin: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-glass-corner {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-glass-container {
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"id": "/blog/",
|
||||||
|
"icon": "script:icon_top",
|
||||||
|
"name": "LATEST",
|
||||||
|
"description": "BROWSE ARTICLES"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "/blog/rss.xml",
|
||||||
|
"icon": "script:icon_rss",
|
||||||
|
"name": "RSS",
|
||||||
|
"description": "FOR NERDS WITH READERS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "/",
|
||||||
|
"icon": "script:icon_cross",
|
||||||
|
"name": "EXIT",
|
||||||
|
"description": "RETURN TO HOMEPAGE"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"id": "bakonpancakz",
|
||||||
|
"icon": "script:author_bakonpancakz",
|
||||||
|
"name": "BAKONPANCAKZ",
|
||||||
|
"description": "THE BOSS"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"id": "development",
|
||||||
|
"icon": "script:tags_development",
|
||||||
|
"name": "DEVELOPMENT",
|
||||||
|
"description": "BINARY MAGIC / SPELLS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "personal",
|
||||||
|
"icon": "script:tags_personal",
|
||||||
|
"name": "PERSONAL",
|
||||||
|
"description": "PROJECTS AND TIDBITS"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"articles": [
|
||||||
|
{
|
||||||
|
"id": "setting-up-an-email-server",
|
||||||
|
"banner_main": "/public/blog/setting-up-an-email-server/banner_main.gif",
|
||||||
|
"banner_list": "/public/blog/setting-up-an-email-server/banner_list.gif",
|
||||||
|
"date": "2024-11-06T00:00:00Z",
|
||||||
|
"title": "Setting Up an Email API",
|
||||||
|
"description": "Freaking emails, how do they work?",
|
||||||
|
"author": "bakonpancakz",
|
||||||
|
"tags": [
|
||||||
|
"development",
|
||||||
|
"personal"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
document.querySelector("div.layout-background").appendChild((() => {
|
||||||
|
|
||||||
|
const COLOR_PARTICLE = 0x363636
|
||||||
|
const COLOR_FOREGROUND = 0x282828
|
||||||
|
const COLOR_BACKGROUND = 0x000000
|
||||||
|
const PARTICLE_COUNT = 320
|
||||||
|
const FRAME_TIME = 1000 / 15
|
||||||
|
|
||||||
|
let gl, canvas
|
||||||
|
let planeProg, particleProg
|
||||||
|
let planeBuf, planeIdxBuf, planeOrigBuf
|
||||||
|
let particlePosBuf, particleColorBuf
|
||||||
|
let planeVerts, planeOrig, planeIdx
|
||||||
|
let particlePos, particleColor, particleData = []
|
||||||
|
let lastTime = 0, lastFrame = 0, time = 0
|
||||||
|
let planeW, planeH, planeSegX, planeSegY
|
||||||
|
let uTimePlane, uMVPPlane, uFogColorPlane, uFogNearPlane, uFogFarPlane
|
||||||
|
let uMVPParticle, uFogColorParticle, uFogNearParticle, uFogFarParticle
|
||||||
|
|
||||||
|
// --- Math helpers ---
|
||||||
|
function mat4() { return new Float32Array(16) }
|
||||||
|
function mat4Identity(m) {
|
||||||
|
m[0] = 1; m[1] = 0; m[2] = 0; m[3] = 0;
|
||||||
|
m[4] = 0; m[5] = 1; m[6] = 0; m[7] = 0;
|
||||||
|
m[8] = 0; m[9] = 0; m[10] = 1; m[11] = 0;
|
||||||
|
m[12] = 0; m[13] = 0; m[14] = 0; m[15] = 1;
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
function mat4Multiply(out, a, b) {
|
||||||
|
for (let i = 0; i < 4; i++)
|
||||||
|
for (let j = 0; j < 4; j++) {
|
||||||
|
out[j * 4 + i] = 0
|
||||||
|
for (let k = 0; k < 4; k++) out[j * 4 + i] += a[k * 4 + i] * b[j * 4 + k]
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
function mat4Perspective(m, fovY, aspect, near, far) {
|
||||||
|
const f = 1.0 / Math.tan(fovY / 2)
|
||||||
|
m[0] = f / aspect; m[1] = 0; m[2] = 0; m[3] = 0
|
||||||
|
m[4] = 0; m[5] = f; m[6] = 0; m[7] = 0
|
||||||
|
m[8] = 0; m[9] = 0; m[10] = (far + near) / (near - far); m[11] = -1
|
||||||
|
m[12] = 0; m[13] = 0; m[14] = (2 * far * near) / (near - far); m[15] = 0
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
function mat4RotateX(m, angle) {
|
||||||
|
const c = Math.cos(angle), s = Math.sin(angle)
|
||||||
|
const t = mat4Identity(mat4())
|
||||||
|
t[5] = c; t[6] = s; t[9] = -s; t[10] = c
|
||||||
|
return mat4Multiply(mat4(), t, m)
|
||||||
|
}
|
||||||
|
function mat4Translate(m, x, y, z) {
|
||||||
|
const t = mat4Identity(mat4())
|
||||||
|
t[12] = x; t[13] = y; t[14] = z
|
||||||
|
return mat4Multiply(mat4(), t, m)
|
||||||
|
}
|
||||||
|
function randFloat(lo, hi) { return lo + Math.random() * (hi - lo) }
|
||||||
|
function randFloatSpread(range) { return randFloat(-range / 2, range / 2) }
|
||||||
|
function degToRad(d) { return d * Math.PI / 180 }
|
||||||
|
function intToRGB(i) {
|
||||||
|
return [
|
||||||
|
((i >> 16) & 0xFF) / 255,
|
||||||
|
((i >> 8) & 0xFF) / 255,
|
||||||
|
((i >> 0) & 0xFF) / 255
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Shader sources ---
|
||||||
|
const PLANE_VERT = `
|
||||||
|
attribute vec3 aPosition;
|
||||||
|
uniform mat4 uMVP;
|
||||||
|
uniform float uTime;
|
||||||
|
varying float vDist;
|
||||||
|
void main() {
|
||||||
|
float dist = sqrt(aPosition.x*aPosition.x + aPosition.z*aPosition.z);
|
||||||
|
float wave = sin(dist * 0.5 - uTime);
|
||||||
|
float cave = -exp(-dist * 0.1) * 3.5;
|
||||||
|
vec3 pos = vec3(aPosition.x, aPosition.y + wave + cave, aPosition.z);
|
||||||
|
vDist = length((uMVP * vec4(pos, 1.0)).xyz);
|
||||||
|
gl_Position = uMVP * vec4(pos, 1.0);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const PLANE_FRAG = `
|
||||||
|
precision mediump float;
|
||||||
|
uniform vec3 uFogColor;
|
||||||
|
uniform float uFogNear;
|
||||||
|
uniform float uFogFar;
|
||||||
|
varying float vDist;
|
||||||
|
void main() {
|
||||||
|
float fog = clamp((vDist - uFogNear) / (uFogFar - uFogNear), 0.0, 1.0);
|
||||||
|
vec3 color = mix(vec3(${intToRGB(COLOR_FOREGROUND).join(',')}), uFogColor, fog);
|
||||||
|
gl_FragColor = vec4(color, 1.0);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const PARTICLE_VERT = `
|
||||||
|
attribute vec3 aPosition;
|
||||||
|
attribute vec4 aColor;
|
||||||
|
uniform mat4 uMVP;
|
||||||
|
varying vec4 vColor;
|
||||||
|
varying float vDist;
|
||||||
|
void main() {
|
||||||
|
vec4 pos = uMVP * vec4(aPosition, 1.0);
|
||||||
|
vDist = length(pos.xyz);
|
||||||
|
vColor = aColor;
|
||||||
|
gl_PointSize = 3.0;
|
||||||
|
gl_Position = pos;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const PARTICLE_FRAG = `
|
||||||
|
precision mediump float;
|
||||||
|
uniform vec3 uFogColor;
|
||||||
|
uniform float uFogNear;
|
||||||
|
uniform float uFogFar;
|
||||||
|
varying vec4 vColor;
|
||||||
|
varying float vDist;
|
||||||
|
void main() {
|
||||||
|
float fog = clamp((vDist - uFogNear) / (uFogFar - uFogNear), 0.0, 1.0);
|
||||||
|
float alpha = vColor.a * (1.0 - fog);
|
||||||
|
gl_FragColor = vec4(mix(vColor.rgb, uFogColor, fog), alpha);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
function compileShader(src, type) {
|
||||||
|
const s = gl.createShader(type)
|
||||||
|
gl.shaderSource(s, src)
|
||||||
|
gl.compileShader(s)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
function createProgram(vert, frag) {
|
||||||
|
const p = gl.createProgram()
|
||||||
|
gl.attachShader(p, compileShader(vert, gl.VERTEX_SHADER))
|
||||||
|
gl.attachShader(p, compileShader(frag, gl.FRAGMENT_SHADER))
|
||||||
|
gl.linkProgram(p)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPlane(ratio) {
|
||||||
|
const scale = 24
|
||||||
|
planeSegX = Math.round(12 * ratio)
|
||||||
|
planeSegY = 12
|
||||||
|
planeW = scale * ratio
|
||||||
|
planeH = scale
|
||||||
|
|
||||||
|
const nx = planeSegX + 1, ny = planeSegY + 1
|
||||||
|
planeVerts = new Float32Array(nx * ny * 3)
|
||||||
|
planeOrig = new Float32Array(nx * ny * 3)
|
||||||
|
|
||||||
|
let vi = 0
|
||||||
|
for (let iy = 0; iy < ny; iy++) {
|
||||||
|
for (let ix = 0; ix < nx; ix++) {
|
||||||
|
const x = (ix / planeSegX - 0.5) * planeW
|
||||||
|
const z = (iy / planeSegY - 0.5) * planeH
|
||||||
|
planeVerts[vi] = x
|
||||||
|
planeVerts[vi + 1] = 0
|
||||||
|
planeVerts[vi + 2] = z
|
||||||
|
planeOrig[vi] = x
|
||||||
|
planeOrig[vi + 1] = 0
|
||||||
|
planeOrig[vi + 2] = z
|
||||||
|
vi += 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wireframe indices — two triangles per quad
|
||||||
|
const lines = []
|
||||||
|
for (let iy = 0; iy < ny; iy++) {
|
||||||
|
for (let ix = 0; ix < nx; ix++) {
|
||||||
|
const idx = iy * nx + ix
|
||||||
|
if (ix < planeSegX) { lines.push(idx, idx + 1) }
|
||||||
|
if (iy < planeSegY) { lines.push(idx, idx + nx) }
|
||||||
|
if (ix < planeSegX && iy < planeSegY) { lines.push(idx, idx + nx + 1) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
planeIdx = new Uint16Array(lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
function spawnParticle(i) {
|
||||||
|
const x = randFloat(-planeW, planeW)
|
||||||
|
const z = randFloat(-planeH, planeH)
|
||||||
|
particleData[i] = {
|
||||||
|
x, y: 0, z,
|
||||||
|
vx: randFloatSpread(0.05),
|
||||||
|
vy: randFloat(0.02, 0.05),
|
||||||
|
vz: randFloatSpread(0.05),
|
||||||
|
life: randFloat(3.0, 6.0),
|
||||||
|
maxLife: 0
|
||||||
|
}
|
||||||
|
particleData[i].maxLife = particleData[i].life
|
||||||
|
const p = i * 3
|
||||||
|
particlePos[p] = x; particlePos[p + 1] = 0; particlePos[p + 2] = z
|
||||||
|
const c = i * 4
|
||||||
|
const [r, g, b] = intToRGB(COLOR_PARTICLE)
|
||||||
|
particleColor[c + 0] = r
|
||||||
|
particleColor[c + 1] = g
|
||||||
|
particleColor[c + 2] = b
|
||||||
|
particleColor[c + 3] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
canvas.width = Math.floor(window.innerWidth * 0.25)
|
||||||
|
canvas.height = Math.floor(window.innerHeight * 0.25)
|
||||||
|
canvas.style.width = window.innerWidth + 'px'
|
||||||
|
canvas.style.height = window.innerHeight + 'px'
|
||||||
|
canvas.style.imageRendering = 'pixelated'
|
||||||
|
|
||||||
|
const ratio = canvas.width / canvas.height
|
||||||
|
|
||||||
|
gl.viewport(0, 0, canvas.width, canvas.height)
|
||||||
|
gl.clearColor(...intToRGB(COLOR_BACKGROUND), 1)
|
||||||
|
gl.enable(gl.BLEND)
|
||||||
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||||
|
|
||||||
|
buildPlane(ratio)
|
||||||
|
|
||||||
|
// Upload plane buffers
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, planeBuf)
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, planeVerts, gl.DYNAMIC_DRAW)
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, planeOrigBuf)
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, planeOrig, gl.STATIC_DRAW)
|
||||||
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, planeIdxBuf)
|
||||||
|
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, planeIdx, gl.STATIC_DRAW)
|
||||||
|
|
||||||
|
// Init particles if first time
|
||||||
|
if (particleData.length === 0) {
|
||||||
|
particlePos = new Float32Array(PARTICLE_COUNT * 3)
|
||||||
|
particleColor = new Float32Array(PARTICLE_COUNT * 4)
|
||||||
|
for (let i = 0; i < PARTICLE_COUNT; i++) spawnParticle(i)
|
||||||
|
}
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, particlePosBuf)
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, particlePos, gl.DYNAMIC_DRAW)
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, particleColorBuf)
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, particleColor, gl.DYNAMIC_DRAW)
|
||||||
|
|
||||||
|
// Build MVP
|
||||||
|
const proj = mat4Perspective(mat4(), degToRad(70), ratio, 0.1, 100)
|
||||||
|
let view = mat4Identity(mat4())
|
||||||
|
view = mat4Translate(view, 0, -7.5, -15)
|
||||||
|
view = mat4RotateX(view, degToRad(33.75))
|
||||||
|
const mvp = mat4Multiply(mat4(), proj, view)
|
||||||
|
|
||||||
|
gl.useProgram(planeProg)
|
||||||
|
gl.uniformMatrix4fv(uMVPPlane, false, mvp)
|
||||||
|
gl.uniform3f(uFogColorPlane, ...intToRGB(COLOR_BACKGROUND))
|
||||||
|
gl.uniform1f(uFogNearPlane, 2)
|
||||||
|
gl.uniform1f(uFogFarPlane, 22)
|
||||||
|
|
||||||
|
gl.useProgram(particleProg)
|
||||||
|
gl.uniformMatrix4fv(uMVPParticle, false, mvp)
|
||||||
|
gl.uniform3f(uFogColorParticle, ...intToRGB(COLOR_BACKGROUND))
|
||||||
|
gl.uniform1f(uFogNearParticle, 6)
|
||||||
|
gl.uniform1f(uFogFarParticle, 22)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePlane() {
|
||||||
|
for (let i = 0; i < planeVerts.length; i += 3) {
|
||||||
|
const x = planeOrig[i], z = planeOrig[i + 2]
|
||||||
|
const dist = Math.sqrt(x * x + z * z)
|
||||||
|
planeVerts[i] = x
|
||||||
|
planeVerts[i + 1] = Math.sin(dist * 0.5 - time) * 0.5 + (-Math.exp(-dist * 0.1) * 3.5)
|
||||||
|
planeVerts[i + 2] = z
|
||||||
|
}
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, planeBuf)
|
||||||
|
gl.bufferSubData(gl.ARRAY_BUFFER, 0, planeVerts)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateParticles(delta) {
|
||||||
|
for (let i = 0; i < PARTICLE_COUNT; i++) {
|
||||||
|
const p = particleData[i]
|
||||||
|
p.life -= delta
|
||||||
|
if (p.life <= 0) { spawnParticle(i); continue }
|
||||||
|
const t = p.life / p.maxLife
|
||||||
|
p.x += p.vx * delta * 20
|
||||||
|
p.y += p.vy * delta * 20
|
||||||
|
p.z += p.vz * delta * 20
|
||||||
|
const pi = i * 3
|
||||||
|
particlePos[pi] = p.x; particlePos[pi + 1] = p.y; particlePos[pi + 2] = p.z
|
||||||
|
particleColor[i * 4 + 3] = Math.min(t * 1.2, 1)
|
||||||
|
}
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, particlePosBuf)
|
||||||
|
gl.bufferSubData(gl.ARRAY_BUFFER, 0, particlePos)
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, particleColorBuf)
|
||||||
|
gl.bufferSubData(gl.ARRAY_BUFFER, 0, particleColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawPlane() {
|
||||||
|
gl.useProgram(planeProg)
|
||||||
|
gl.uniform1f(uTimePlane, time)
|
||||||
|
|
||||||
|
const aPos = gl.getAttribLocation(planeProg, 'aPosition')
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, planeBuf)
|
||||||
|
gl.enableVertexAttribArray(aPos)
|
||||||
|
gl.vertexAttribPointer(aPos, 3, gl.FLOAT, false, 0, 0)
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, planeIdxBuf)
|
||||||
|
gl.drawElements(gl.LINES, planeIdx.length, gl.UNSIGNED_SHORT, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawParticles() {
|
||||||
|
gl.useProgram(particleProg)
|
||||||
|
|
||||||
|
const aPos = gl.getAttribLocation(particleProg, 'aPosition')
|
||||||
|
const aCol = gl.getAttribLocation(particleProg, 'aColor')
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, particlePosBuf)
|
||||||
|
gl.enableVertexAttribArray(aPos)
|
||||||
|
gl.vertexAttribPointer(aPos, 3, gl.FLOAT, false, 0, 0)
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, particleColorBuf)
|
||||||
|
gl.enableVertexAttribArray(aCol)
|
||||||
|
gl.vertexAttribPointer(aCol, 4, gl.FLOAT, false, 0, 0)
|
||||||
|
|
||||||
|
gl.drawArrays(gl.POINTS, 0, PARTICLE_COUNT)
|
||||||
|
}
|
||||||
|
|
||||||
|
function animate(now) {
|
||||||
|
requestAnimationFrame(animate)
|
||||||
|
if (now - lastFrame < FRAME_TIME) return
|
||||||
|
const delta = (now - lastTime) * 0.00008 || 0
|
||||||
|
lastFrame = now; lastTime = now; time += delta
|
||||||
|
gl.clear(gl.COLOR_BUFFER_BIT)
|
||||||
|
updatePlane()
|
||||||
|
updateParticles(delta)
|
||||||
|
drawPlane()
|
||||||
|
drawParticles()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Bootstrap ---
|
||||||
|
canvas = document.createElement('canvas')
|
||||||
|
gl = canvas.getContext('webgl')
|
||||||
|
|
||||||
|
planeProg = createProgram(PLANE_VERT, PLANE_FRAG)
|
||||||
|
particleProg = createProgram(PARTICLE_VERT, PARTICLE_FRAG)
|
||||||
|
|
||||||
|
planeBuf = gl.createBuffer()
|
||||||
|
planeOrigBuf = gl.createBuffer()
|
||||||
|
planeIdxBuf = gl.createBuffer()
|
||||||
|
particlePosBuf = gl.createBuffer()
|
||||||
|
particleColorBuf = gl.createBuffer()
|
||||||
|
|
||||||
|
uTimePlane = gl.getUniformLocation(planeProg, 'uTime')
|
||||||
|
uMVPPlane = gl.getUniformLocation(planeProg, 'uMVP')
|
||||||
|
uFogColorPlane = gl.getUniformLocation(planeProg, 'uFogColor')
|
||||||
|
uFogNearPlane = gl.getUniformLocation(planeProg, 'uFogNear')
|
||||||
|
uFogFarPlane = gl.getUniformLocation(planeProg, 'uFogFar')
|
||||||
|
uMVPParticle = gl.getUniformLocation(particleProg, 'uMVP')
|
||||||
|
uFogColorParticle = gl.getUniformLocation(particleProg, 'uFogColor')
|
||||||
|
uFogNearParticle = gl.getUniformLocation(particleProg, 'uFogNear')
|
||||||
|
uFogFarParticle = gl.getUniformLocation(particleProg, 'uFogFar')
|
||||||
|
|
||||||
|
if (window.outerWidth >= 1024) {
|
||||||
|
window.addEventListener('resize', init)
|
||||||
|
init()
|
||||||
|
animate(0)
|
||||||
|
} else {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
return canvas
|
||||||
|
})())
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
(() => {
|
||||||
|
const encodeSVG /**/ = svg => "data:image/svg+xml;base64," + btoa(svg)
|
||||||
|
const formatDate /**/ = str => new Date(str).toDateString().slice(4).toUpperCase()
|
||||||
|
const icons = {
|
||||||
|
// NOTE: External Images should have the version parameter appended to them.
|
||||||
|
// e.g. /public/.../image.png?v=${dataVersion}
|
||||||
|
diamond_fill: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon points="8,0 16,8 8,16 0,8" style="fill:#a0a0a0"/></svg>`),
|
||||||
|
diamond_empty: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M8,0L0,8l8,8,8-8L8,0ZM3,8l5-5,5,5-5,5-5-5Z" style="fill: #a0a0a0"/></svg>`),
|
||||||
|
icon_cross: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon style="fill:#c0c0c0" points="0 0 0 3 6 8 0 13 0 16 3 16 8 10 13 16 16 16 16 13 10 8 16 3 16 0 13 0 8 6 3 0 0 0"/></svg>`),
|
||||||
|
icon_rss: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><polygon style="fill:#c0c0c0" points="6.4 64 0 57.6 6.4 48 16 57.6 6.4 64"/><polyline style="fill:#c0c0c0" points="25.6 64 38.4 64 33.6 30.4 0 25.6 0 38.4 20.8 43.2"/><polyline style="fill:#c0c0c0" points="51.2 64 64 64 59.2 4.8 0 0 0 12.8 46.4 17.6"/></svg>`),
|
||||||
|
icon_top: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><polygon style="fill:#c0c0c0" points="1 0 31 0 32 6 0 6 1 0"/><polygon style="fill:#c0c0c0" points="0 32 0 24 16 8 32 24 32 32 16 20 0 32"/></svg>`),
|
||||||
|
tags_development: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><polygon style="fill:#c0c0c0" points="8 2 5 2 4 0 3 2 0 2 2 4 0 8 4 6 8 8 6 4 8 2"/></svg>`),
|
||||||
|
tags_personal: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 13"><polygon style="fill:#c0c0c0" points="9 13 0 4 4 0 9 3 14 0 18 4 9 13"/></svg>`),
|
||||||
|
author_bakonpancakz: /**/ `data:image/gif;base64,R0lGODlhRABOAPAAAAAAAMDAwCH5BAUKAAAALAAAAABEAE4AAAL/hI+pF+0P45p0uQqi3hD79CjcSF6fZzLlyp1iCrKy5KphPOe3SyP6D0N1fMBioGYxKpFE5ZKZcTqR0urxZM0Ks9Utd1r5WimksFgzKWNy5FHavZ61W0m6eeXt4TYfVp9fBxhX8of2AjeoVrjTZHenmNcQKPiIGHm1RzkHmRhkQHhp2Mlo47hpeUr66TeJV6kadaaZCSurdSjVmIsLtjrG++TbBQzUamT887arWxQqR1s8qkOcLM0GXR0rWRqNbZvN/N3NLd7sXf4cjn59vg7a7o4qHJ+uTs85f+8Krz/E3+9JG0B8AgeaymdQlL2EnhgSdCivIMSAE2dJrIgMI7mKPow4OvJICaRCkXpI+rvokdpEZezSTYvyj5SMjWq20ZS3D6Ggjjkz2sy05RwVPTxgQTk6NCDSpYt+Mn2aB0EBADs=`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse Data from Webpage //
|
||||||
|
const dataVersion /**/ = document.querySelector("data[key='version']").value
|
||||||
|
const dataManifest /**/ = JSON.parse(document.querySelector("data[key='manifest']").value)
|
||||||
|
{
|
||||||
|
// Additional Validation //
|
||||||
|
if (!dataManifest.articles) /**/ throw "Missing Key: .articles" // Type: Metadata
|
||||||
|
if (!dataManifest.authors) /**/ throw "Missing Key: .authors" // Type: Filter
|
||||||
|
if (!dataManifest.links) /**/ throw "Missing Key: .links" // Type: Metadata
|
||||||
|
if (!dataManifest.tags) /**/ throw "Missing Key: .tags" // Type: Filter
|
||||||
|
|
||||||
|
const lookupAuthor /**/ = new Set(dataManifest.authors.map(t => t.id))
|
||||||
|
const lookupTags /**/ = new Set(dataManifest.tags.map(t => t.id))
|
||||||
|
console.info("Authors IDs:", [...lookupAuthor].join(", "))
|
||||||
|
console.info("Tags IDs: ", [...lookupTags].join(", "))
|
||||||
|
|
||||||
|
dataManifest.articles = dataManifest.articles.filter((a, i) => {
|
||||||
|
if (!a.date) {
|
||||||
|
console.warn(`Article '${a.id}' has no date and will be hidden.`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (const t of a.tags) {
|
||||||
|
if (!lookupTags.has(t)) {
|
||||||
|
console.warn(`Article '${a.id}' has an unknown tag id (${t})`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!lookupAuthor.has(a.author)) {
|
||||||
|
console.warn(`Article '${a.id}' has an unknown author id (${a.author})`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info(`Article OK: ${a.id}`)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Functions //
|
||||||
|
let articleHeader /**/ = document.querySelector("div.article-header")
|
||||||
|
let articleContent /**/ = document.querySelector("div.article-content")
|
||||||
|
let navigation /**/ = document.querySelector("nav.section-navigation")
|
||||||
|
let listArticles /**/ = document.querySelector("div.article-list")
|
||||||
|
let listChapters /**/ = null
|
||||||
|
|
||||||
|
function updateNavigation() {
|
||||||
|
|
||||||
|
function templateLink(link, name, description, icon) {
|
||||||
|
const image = icon.startsWith("script:")
|
||||||
|
? icons[icon.slice(7)]
|
||||||
|
: icon // use uri
|
||||||
|
const html = `
|
||||||
|
<a class="item-nav animation-fadein" href="${link}">
|
||||||
|
<div class="item-nav-text">
|
||||||
|
<p class="animation-scrollin item-nav-text-anchor">${name}</p>
|
||||||
|
<p class="animation-scrollin item-nav-text-content">${description}</p>
|
||||||
|
</div>
|
||||||
|
<div class="item-nav-icon">
|
||||||
|
<div class="item-nav-icon-background"></div>
|
||||||
|
<img class="item-nav-icon-foreground" alt="Icon for ${name}" src="${image}">
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
`
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
function templateList(name) {
|
||||||
|
let htmlKey /**/ = name.toLocaleLowerCase()
|
||||||
|
let dataKey /**/ = `blog_togglestate_${htmlKey}`
|
||||||
|
let result /**/ = localStorage.getItem(dataKey)
|
||||||
|
let active /**/ = (result === null) ? true : (result === "true")
|
||||||
|
|
||||||
|
// Create Toggle Buttons //
|
||||||
|
navigation.insertAdjacentHTML("beforeend", `
|
||||||
|
<button class="nav-header" for="${htmlKey}">
|
||||||
|
<span>${name}</span>
|
||||||
|
<img alt="Toggle open state for list ${name}">
|
||||||
|
</button>
|
||||||
|
<div class="nav-list" id="${htmlKey}">
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
const list /**/ = navigation.querySelector(`div.nav-list#${htmlKey}`)
|
||||||
|
const header /**/ = navigation.querySelector(`button.nav-header[for="${htmlKey}"]`)
|
||||||
|
const icon /**/ = header.querySelector("img")
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
localStorage.setItem(dataKey, active)
|
||||||
|
list.style.display = active
|
||||||
|
? "" // reset
|
||||||
|
: "none"
|
||||||
|
icon.src = active
|
||||||
|
? icons.diamond_fill
|
||||||
|
: icons.diamond_empty
|
||||||
|
active
|
||||||
|
? header.classList.add("open")
|
||||||
|
: header.classList.remove("open")
|
||||||
|
}
|
||||||
|
header.onclick = () => {
|
||||||
|
active = !active
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
update()
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Nav Links //
|
||||||
|
templateList("LINKS").innerHTML = dataManifest.links
|
||||||
|
.map(l => templateLink(l.id, l.name, l.description, l.icon))
|
||||||
|
.join("\n")
|
||||||
|
templateList("TAGS").innerHTML = dataManifest.tags
|
||||||
|
.map(t => templateLink(`/blog/?filter[tag]=${t.id}`, t.name, t.description, t.icon))
|
||||||
|
.join("\n")
|
||||||
|
templateList("AUTHORS").innerHTML = dataManifest.authors
|
||||||
|
.map(a => templateLink(`/blog/?filter[author]=${a.id}`, a.name, a.description, a.icon))
|
||||||
|
.join("\n")
|
||||||
|
listChapters = templateList("CHAPTERS")
|
||||||
|
|
||||||
|
}
|
||||||
|
updateNavigation()
|
||||||
|
|
||||||
|
function updateArticle() {
|
||||||
|
articleContent.replaceChildren()
|
||||||
|
articleHeader.replaceChildren()
|
||||||
|
listArticles.replaceChildren()
|
||||||
|
listChapters.replaceChildren()
|
||||||
|
|
||||||
|
const slug /**/ = new URL(window.location.href).pathname.slice("/blog/".length)
|
||||||
|
const article /**/ = dataManifest.articles.find(a => a.id == slug)
|
||||||
|
|
||||||
|
if (article) {
|
||||||
|
|
||||||
|
// Display Article //
|
||||||
|
document.title = `panca.kz - Blog - ${article.title}`
|
||||||
|
|
||||||
|
// Download Article Content [ASYNC] //
|
||||||
|
fetch(`/public/blog/${slug}/content.html?v=${dataVersion}`)
|
||||||
|
.then(async resp => {
|
||||||
|
|
||||||
|
// Insert Article Content //
|
||||||
|
// Yes, we are exploiting ourselves (safely). But I want the ability to
|
||||||
|
// implement custom elements and features to specific articles in the future.
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw `Server responded with ${resp.status} ${resp.statusText}`
|
||||||
|
}
|
||||||
|
articleContent.insertAdjacentHTML("beforeend", await resp.text())
|
||||||
|
articleContent.querySelectorAll("script").forEach(s => {
|
||||||
|
eval(s.innerHTML)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Generate Article Chapters //
|
||||||
|
let chapterHTML = ""
|
||||||
|
for (const item of articleContent.children) {
|
||||||
|
const normalize = c => {
|
||||||
|
const v = c.textContent.toLocaleLowerCase().replaceAll(" ", "-")
|
||||||
|
c.id = v
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
if (item.classList.contains("element-header")) {
|
||||||
|
chapterHTML += `<a class="animation-scrollin chapter" href="#${normalize(item)}">${item.textContent}</a>\n`
|
||||||
|
}
|
||||||
|
if (item.classList.contains("element-subheader")) {
|
||||||
|
chapterHTML += `<a class="animation-scrollin section" href="#${normalize(item)}">${item.textContent}</a>\n`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listChapters.innerHTML = chapterHTML
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
// Generic Fetch Error //
|
||||||
|
console.error("<Cannot Download Article!>", err)
|
||||||
|
articleContent.innerHTML += `
|
||||||
|
<p style="color: red; text-align: center;" class="animation-blink">
|
||||||
|
Fetch Error, please see console for more information.
|
||||||
|
</p>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
|
// Render Article Header (Metadata) //
|
||||||
|
const author /**/ = dataManifest.authors.find(a => a.id === article.author)
|
||||||
|
const tags /**/ = article.tags.map(id => dataManifest.tags.find(t => t.id === id))
|
||||||
|
|
||||||
|
listChapters.innerHTML += `
|
||||||
|
<p class="nav-message">... LOADING ARTICLE ...</p>
|
||||||
|
`
|
||||||
|
articleHeader.innerHTML += `
|
||||||
|
<img
|
||||||
|
id="header-banner"
|
||||||
|
fetchpriority="high"
|
||||||
|
alt="Banner for ${article.title}"
|
||||||
|
src="${article.banner_main}?v=${dataVersion}">
|
||||||
|
|
||||||
|
<h1 class="effect-docked" id="header-title">
|
||||||
|
${article.title}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="header-chevrons">
|
||||||
|
<div class="effect-chevron-container" id="header-about">
|
||||||
|
<span class="effect-chevron-point-right" id="article-author">
|
||||||
|
${author.name}
|
||||||
|
</span>
|
||||||
|
<span class="effect-chevron-point-right" id="article-published">
|
||||||
|
${formatDate(article.date)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="effect-chevron-container" id="header-tags">
|
||||||
|
${tags.map(t => `<span class="effect-chevron-point-left">${t.name}</span>`).join("\n")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Display Search //
|
||||||
|
document.title = `panca.kz - Blog`
|
||||||
|
|
||||||
|
// Collect Filters and Articles //
|
||||||
|
const params = new URLSearchParams(window.location.search)
|
||||||
|
const filterTags = new Set(params
|
||||||
|
.getAll("filter[tag]")
|
||||||
|
.filter(s => dataManifest.tags.findIndex(t => t.id.localeCompare(s) === 0) > -1)
|
||||||
|
)
|
||||||
|
const filterAuthor = new Set(params
|
||||||
|
.getAll("filter[author]")
|
||||||
|
.filter(s => dataManifest.authors.findIndex(t => t.id.localeCompare(s) === 0) > -1)
|
||||||
|
)
|
||||||
|
const results = dataManifest.articles
|
||||||
|
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||||
|
.filter(a => {
|
||||||
|
|
||||||
|
// Need Requested Author //
|
||||||
|
if (filterAuthor.size > 0) {
|
||||||
|
if (!filterAuthor.has(a.author)) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need Requested Tags //
|
||||||
|
if (filterTags.size > 0) {
|
||||||
|
let missedTag = false
|
||||||
|
filterTags.forEach(t => {
|
||||||
|
if (a.tags.findIndex(s => s === t) === -1) {
|
||||||
|
missedTag = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (missedTag) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Render Results //
|
||||||
|
listChapters.innerHTML += `
|
||||||
|
<p class="nav-message">... SELECT ARTICLE ...</p>
|
||||||
|
`
|
||||||
|
listArticles.innerHTML += `
|
||||||
|
<div class="item-article-count">
|
||||||
|
<p class="animation-scrollin item-article-count-text">
|
||||||
|
${results.length} RESULT${results.length === 1 ? "" : "s"}
|
||||||
|
${new Array(...filterTags.values()).map(s => `<TAG:${s.toUpperCase()}>`).join(" ")}
|
||||||
|
${new Array(...filterAuthor.values()).map(s => `<AUTHOR:${s.toUpperCase()}>`).join(" ")}
|
||||||
|
<span class="animation-blink item-article-count-cursor">
|
||||||
|
_
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
listArticles.innerHTML += results.map((a, i) => `
|
||||||
|
<a class="item-article" href="/blog/${a.id}" style="animation-delay: ${i * 200}ms;">
|
||||||
|
<div class="item-article-layer">
|
||||||
|
<img
|
||||||
|
class="item-article-banner"
|
||||||
|
onload="this.classList.add('animation-fadein')"
|
||||||
|
alt="Banner for ${a.title}"
|
||||||
|
src="${a.banner_list}?v=${dataVersion}">
|
||||||
|
</div>
|
||||||
|
<div class="item-article-layer item-article-meta">
|
||||||
|
<p class="animation-scrollin item-article-meta-title">${a.title}</p>
|
||||||
|
<p class="animation-fadein item-article-meta-description">${a.description}</p>
|
||||||
|
<div class="animation-fadein item-article-meta-tags">
|
||||||
|
<span>${formatDate(a.date)}</span>
|
||||||
|
${a.tags.map(t => `<span>${t.toUpperCase()}</span>`).join("\n")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
`)
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateArticle()
|
||||||
|
|
||||||
|
|
||||||
|
// History Overrides //
|
||||||
|
window.addEventListener("popstate", () => {
|
||||||
|
updateArticle()
|
||||||
|
})
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
const link = e.target.closest("a")
|
||||||
|
if (!link) return
|
||||||
|
|
||||||
|
const url = new URL(link.href)
|
||||||
|
|
||||||
|
// Ignore non-blog links
|
||||||
|
if (!url.pathname.startsWith("/blog/")) return
|
||||||
|
if (url.pathname.startsWith("/blog/rss.xml")) return
|
||||||
|
|
||||||
|
// Handle chapter links
|
||||||
|
const current = new URL(window.location.href)
|
||||||
|
if (
|
||||||
|
url.pathname === current.pathname &&
|
||||||
|
url.search === current.search &&
|
||||||
|
url.hash
|
||||||
|
) {
|
||||||
|
// Smooth scroll into section
|
||||||
|
e.preventDefault()
|
||||||
|
const target = document.querySelector(url.hash)
|
||||||
|
if (target) {
|
||||||
|
target.scrollIntoView({ behavior: "smooth" })
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Application
|
||||||
|
e.preventDefault()
|
||||||
|
history.pushState(null, "", url.pathname + url.search + url.hash)
|
||||||
|
updateArticle()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
})()
|
||||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,80 @@
|
|||||||
|
<h2 class="element-header">Introduction</h2>
|
||||||
|
<p class="element-paragraph">
|
||||||
|
If you're reading this... I haven't written the article yet :P
|
||||||
|
</p>
|
||||||
|
<p class="element-paragraph">
|
||||||
|
Luctus montes donec mauris. Vehicula suspendisse condimentum massa fringilla at conubia.
|
||||||
|
Senectus laoreet praesent in, porttitor sollicitudin luctus orci scelerisque egestas malesuada faucibus a.
|
||||||
|
Arcu imperdiet libero imperdiet nulla libero natoque eu fusce ultricies vehicula nisi.
|
||||||
|
Torquent lacus sodales inceptos dignissim sed, mauris feugiat erat.
|
||||||
|
Phasellus at tincidunt sociosqu potenti risus aliquam non litora.
|
||||||
|
Commodo mi fermentum, id arcu dictum convallis.
|
||||||
|
Vehicula convallis, proin fames molestie potenti congue penatibus feugiat porttitor.
|
||||||
|
Habitant justo lacinia, curabitur nascetur!
|
||||||
|
Dolor nunc accumsan imperdiet penatibus?
|
||||||
|
Amet cursus faucibus dui vestibulum nascetur quam.
|
||||||
|
</p>
|
||||||
|
<div class="element-divider"></div>
|
||||||
|
|
||||||
|
<h2 class="element-header">Prerequisites</h2>
|
||||||
|
<p class="element-paragraph">
|
||||||
|
Praesent augue ligula amet nisl. Litora lectus consectetur aliquet.
|
||||||
|
Sociis iaculis dolor nostra pulvinar felis condimentum netus.
|
||||||
|
Suscipit enim habitant sed eros etiam, ut odio sed.
|
||||||
|
Fringilla quam tempus nisl natoque parturient dolor curabitur tellus.
|
||||||
|
Lacus conubia quis inceptos tristique ultricies cum consequat.
|
||||||
|
Consequat phasellus proin dolor molestie nec quam cursus mollis vel?
|
||||||
|
Eros tempor sollicitudin tortor litora nunc.
|
||||||
|
Sem scelerisque semper id ac auctor fusce quis erat imperdiet convallis donec eget!
|
||||||
|
Pellentesque tempor nunc ipsum imperdiet quam ipsum habitant feugiat.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="element-subheader">Hosting</h3>
|
||||||
|
<p class="element-paragraph">
|
||||||
|
Odio nibh penatibus erat sociosqu eget iaculis. Ante curabitur bibendum ligula?
|
||||||
|
Netus aliquet cursus interdum pharetra. Magnis elementum nunc luctus eu nam sit habitant etiam vestibulum in.
|
||||||
|
Lacus nisl ullamcorper molestie! Dignissim, vulputate porta pulvinar duis posuere.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="element-subheader">Domains</h3>
|
||||||
|
<p class="element-paragraph">
|
||||||
|
Curae; laoreet cum duis turpis, feugiat per integer.
|
||||||
|
Sit in blandit himenaeos. Penatibus fringilla imperdiet, luctus dapibus elementum.
|
||||||
|
Felis senectus pulvinar a natoque risus.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="element-subheader">Credentials</h3>
|
||||||
|
<p class="element-paragraph">
|
||||||
|
Mauris habitasse nulla accumsan fermentum fermentum.
|
||||||
|
Magnis amet amet risus senectus in curae;.
|
||||||
|
Elementum ac montes sagittis vivamus malesuada elementum senectus ultrices scelerisque fusce.
|
||||||
|
In nulla platea.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="element-divider"></div>
|
||||||
|
|
||||||
|
<h2 class="element-header">Setup DNS</h2>
|
||||||
|
<p class="element-paragraph">
|
||||||
|
Nisi vitae vehicula etiam nam auctor, accumsan purus ridiculus molestie.
|
||||||
|
Ante malesuada condimentum aliquam etiam auctor arcu porttitor parturient nullam accumsan bibendum?
|
||||||
|
Vitae arcu at mattis. Volutpat himenaeos class vitae elit, luctus sociosqu semper?
|
||||||
|
Praesent taciti lobortis tortor laoreet magnis tortor sagittis porta facilisi vivamus dictum scelerisque.
|
||||||
|
Consectetur elit enim malesuada commodo proin facilisi sagittis!
|
||||||
|
Malesuada quam duis non elit himenaeos sem.
|
||||||
|
Luctus suspendisse senectus mollis aliquet aliquam pellentesque conubia condimentum suspendisse aptent litora potenti.
|
||||||
|
Curabitur sociis lacus turpis, laoreet ligula consequat luctus eu auctor auctor sagittis.
|
||||||
|
Turpis nibh feugiat lorem donec quis bibendum.
|
||||||
|
Himenaeos sapien interdum aptent.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 class="element-header">Setup Linux</h2>
|
||||||
|
<p class="element-paragraph">
|
||||||
|
Convallis pharetra vitae tincidunt phasellus bibendum porta, praesent dictum aliquet volutpat.
|
||||||
|
Lobortis semper eget consectetur elementum elit hendrerit natoque mauris taciti ante ac augue.
|
||||||
|
Ullamcorper viverra vestibulum vehicula nisi metus vestibulum curabitur vehicula et.
|
||||||
|
Phasellus ligula vitae posuere penatibus ullamcorper lorem phasellus metus.
|
||||||
|
Scelerisque ac lobortis gravida primis?
|
||||||
|
Lacus porttitor ullamcorper quisque viverra molestie.
|
||||||
|
Lacinia bibendum facilisi tempor cubilia felis eros!
|
||||||
|
Etiam blandit lacinia leo urna.
|
||||||
|
</p>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,152 @@
|
|||||||
|
/* Poppins Thin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/poppins/PoppinsThin.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 100;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/poppins/PoppinsThinItalic.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Poppins ExtraLight */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 200;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/poppins/PoppinsExtraLight.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 200;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/poppins/PoppinsExtraLightItalic.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Poppins Light */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/poppins/PoppinsLight.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/poppins/PoppinsLightItalic.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Poppins Regular */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/poppins/PoppinsRegular.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/poppins/PoppinsItalic.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Poppins Medium */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/poppins/PoppinsMedium.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/poppins/PoppinsMediumItalic.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Poppins SemiBold */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/poppins/PoppinsSemiBold.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/poppins/PoppinsSemiBoldItalic.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Poppins Bold */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/poppins/PoppinsBold.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/poppins/PoppinsBoldItalic.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Poppins ExtraBold */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/poppins/PoppinsExtraBold.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/poppins/PoppinsExtraBoldItalic.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Poppins Black */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/poppins/PoppinsBlack.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/poppins/PoppinsBlackItalic.woff2);
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/* Terminus Regular */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Terminus';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/terminus/TerminusRegular.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Terminus';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/terminus/TerminusItalic.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Terminus Bold */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Terminus';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/terminus/TerminusBold.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Terminus';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/public/fonts/terminus/TerminusBoldItalic.woff2);
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
Copyright (c) 2010 Dimitar Toshkov Zhekov,
|
||||||
|
with Reserved Font Name "Terminus Font".
|
||||||
|
|
||||||
|
Copyright (c) 2011-2023 Tilman Blumenbach,
|
||||||
|
with Reserved Font Name "Terminus (TTF)".
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 152 B |
|
After Width: | Height: | Size: 6.2 KiB |
@@ -0,0 +1,130 @@
|
|||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
a {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1em;
|
||||||
|
text-decoration: none;
|
||||||
|
line-height: 1.5em;
|
||||||
|
color: white;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover,
|
||||||
|
a:focus-visible {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.homepage-wrapper {
|
||||||
|
width: 1024px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.block-links {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
padding: 0 8px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.block-links a {
|
||||||
|
padding: 16px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.block-banner {
|
||||||
|
background-size: contain;
|
||||||
|
position: relative;
|
||||||
|
width: 1024px;
|
||||||
|
height: 340px;
|
||||||
|
margin: -25px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.block-banner #banner-link-picross {
|
||||||
|
position: absolute;
|
||||||
|
/* ~80px box */
|
||||||
|
height: 23.5%;
|
||||||
|
width: 7.8%;
|
||||||
|
left: 63.75%;
|
||||||
|
top: 46.5%;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
div.block-footer {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
padding: 16px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.footer-barcode {
|
||||||
|
height: 64px;
|
||||||
|
width: 64px;
|
||||||
|
padding: 8px 16px 8px 0;
|
||||||
|
border-right: 1px solid grey;
|
||||||
|
margin-right: 16px;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.footer-quotes {
|
||||||
|
flex-basis: 100%;
|
||||||
|
font-size: small;
|
||||||
|
min-width: 400px;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.footer-spacer {
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
p#footer-motd {
|
||||||
|
font-size: x-small;
|
||||||
|
padding-top: 8px;
|
||||||
|
color: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
a#footer-logo {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
|
||||||
|
div.homepage-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.block-links {
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.block-banner {
|
||||||
|
height: 100%;
|
||||||
|
aspect-ratio: 1024 / 340;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.footer-barcode {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.footer-quotes {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.block-footer {
|
||||||
|
display: grid;
|
||||||
|
padding: 0 16px;
|
||||||
|
gap: 16px;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# \_/
|
||||||
|
# ()o_o) <( beep boop )
|
||||||
|
|
||||||
|
Sitemap: https://panca.kz/sitemap.xml
|
||||||
|
|
||||||
|
User-agent: *
|
||||||
|
Disallow: /public/
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
body {
|
||||||
|
margin: 32px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
|
||||||
|
div.layout-buttons {
|
||||||
|
margin: 32px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.layout-document {
|
||||||
|
position: relative;
|
||||||
|
width: 11in;
|
||||||
|
height: 8.5in;
|
||||||
|
border: 2px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.page-background {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Certificate */
|
||||||
|
|
||||||
|
span {
|
||||||
|
position: absolute;
|
||||||
|
background-color: white;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
span#name {
|
||||||
|
left: 3.75in;
|
||||||
|
top: 3in;
|
||||||
|
font-size: 2em;
|
||||||
|
width: 3.5in;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
span#cheat {
|
||||||
|
left: 3.95in;
|
||||||
|
top: 1in;
|
||||||
|
font-size: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
span#time {
|
||||||
|
left: 6in;
|
||||||
|
top: 4in;
|
||||||
|
}
|
||||||
|
|
||||||
|
span#date {
|
||||||
|
left: 4.8in;
|
||||||
|
top: 3.67in;
|
||||||
|
padding: 0 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Printing Fixup */
|
||||||
|
@media print {
|
||||||
|
@page {
|
||||||
|
size: letter landscape;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-print {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
(() => {
|
||||||
|
|
||||||
|
// SUPER SOPHISTICATED ANTICHEAT //
|
||||||
|
const search = new URLSearchParams(document.location.search)
|
||||||
|
let field_name, field_time, field_cheat
|
||||||
|
|
||||||
|
if (localStorage.getItem("secret_picross_complete")) {
|
||||||
|
// Normal Ending :3
|
||||||
|
field_name = search.get("name")
|
||||||
|
field_time = search.get("time")
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Cheater Ending...
|
||||||
|
alert("i know what you are...")
|
||||||
|
field_name = "cheater mccheater pants"
|
||||||
|
field_time = "whatever"
|
||||||
|
field_cheat = "CHEATED"
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector("span#date").textContent = new Date().toDateString()
|
||||||
|
document.querySelector("span#time").textContent =field_time
|
||||||
|
document.querySelector("span#name").textContent = field_name
|
||||||
|
document.querySelector("span#cheat").textContent = field_cheat
|
||||||
|
document.querySelector("div.layout-document").style.opacity = '1'
|
||||||
|
|
||||||
|
})()
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
:root {
|
||||||
|
--transition-time: 200ms;
|
||||||
|
--animation-time: 200ms;
|
||||||
|
--color-background: #1d1e2e;
|
||||||
|
--color-text-default: #f0f0f0;
|
||||||
|
--color-text-hint: #d0d0d0;
|
||||||
|
--color-disabled: #212335;
|
||||||
|
--color-inactive: #2d2f47;
|
||||||
|
--color-active: #a1a8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--color-background);
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
a {
|
||||||
|
display: inline-block;
|
||||||
|
color: var(--color-text-default);
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: normal;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid var(--color-inactive);
|
||||||
|
padding: 4px 8px;
|
||||||
|
min-width: 120px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover,
|
||||||
|
a:focus-visible {
|
||||||
|
border-color: var(--color-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-tip {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-hidden {
|
||||||
|
color: var(--color-text-hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
|
||||||
|
div.layout-container {
|
||||||
|
margin: auto;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 32px 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.layout-header {
|
||||||
|
display: grid;
|
||||||
|
text-align: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.picross-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.layout-content {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 32px 0;
|
||||||
|
justify-content: center;
|
||||||
|
/* visually centered */
|
||||||
|
margin-left: -32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.picross-hint {
|
||||||
|
color: var(--color-text-hint);
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.picross-button {
|
||||||
|
transition: var(--animation-time) ease-in-out background-color;
|
||||||
|
background-color: var(--color-inactive);
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.state-active {
|
||||||
|
background-color: var(--color-active) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.state-ignore {
|
||||||
|
background-color: var(--color-disabled) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.layout-footer {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.layout-timer {
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
(() => {
|
||||||
|
const correctAnswer = 2560660350
|
||||||
|
const soundSet = document.querySelector("audio#audio-set"); soundSet.volume = 0.5
|
||||||
|
const soundDel = document.querySelector("audio#audio-del"); soundDel.volume = 0.5
|
||||||
|
document.
|
||||||
|
querySelector("div.layout-content")
|
||||||
|
.addEventListener(
|
||||||
|
// Prevent annoying context menu popup whenever you miss a grid tile by 1 pixel
|
||||||
|
"contextmenu",
|
||||||
|
e => e.preventDefault()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Setup Board //
|
||||||
|
document.querySelectorAll("button.picross-button").forEach(e => {
|
||||||
|
e.addEventListener("click", () => {
|
||||||
|
e.classList.remove("state-ignore")
|
||||||
|
e.classList.toggle("state-active")
|
||||||
|
? soundSet.play()
|
||||||
|
: soundDel.play()
|
||||||
|
})
|
||||||
|
e.addEventListener("contextmenu", ev => {
|
||||||
|
ev.preventDefault()
|
||||||
|
e.classList.remove("state-active")
|
||||||
|
e.classList.toggle("state-ignore")
|
||||||
|
soundDel.play()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Setup Timer //
|
||||||
|
function getTime() {
|
||||||
|
const f = s => s.toString().padStart(2, "0")
|
||||||
|
const h = Math.floor(t / 3600)
|
||||||
|
const m = Math.floor(t % 3600 / 60)
|
||||||
|
const s = Math.floor(t % 60)
|
||||||
|
return `Time Spent: ${f(h)}:${f(m)}:${f(s)}`
|
||||||
|
}
|
||||||
|
let t = 0
|
||||||
|
const timerElement = document.querySelector("p.layout-timer")
|
||||||
|
setInterval(() => {
|
||||||
|
t++
|
||||||
|
timerElement.textContent = getTime()
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
// Action: Submit //
|
||||||
|
document.querySelector("a#action-send").addEventListener("click", () => {
|
||||||
|
|
||||||
|
// Check Answer
|
||||||
|
let givenAnswer = 0
|
||||||
|
document.querySelectorAll('div.picross-row').forEach((childRow, rowIndex) => {
|
||||||
|
let childIndex = 0
|
||||||
|
for (const child of childRow.children) {
|
||||||
|
if (child.nodeName === 'BUTTON') {
|
||||||
|
if (child.classList.contains('state-active')) {
|
||||||
|
givenAnswer += 1 << ((rowIndex - 3) * 9) + childIndex
|
||||||
|
}
|
||||||
|
childIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (correctAnswer !== givenAnswer) {
|
||||||
|
alert("Nope! Try Again")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to Certificate
|
||||||
|
localStorage.setItem("secret_picross_complete", "true")
|
||||||
|
const time = getTime()
|
||||||
|
const name = prompt("Congratulations you win! Please enter your name for your certificate:", "Anon")
|
||||||
|
window.location.assign(
|
||||||
|
`/secret/picross/award?name=${encodeURIComponent(name)}&time=${encodeURIComponent(time)}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
})()
|
||||||