mirror of
https://github.com/timmypidashev/web.git
synced 2026-04-14 11:03:50 +00:00
migrate to vercel; bump version
This commit is contained in:
20
src/lib/animations/engine.ts
Normal file
20
src/lib/animations/engine.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ANIMATION_IDS, DEFAULT_ANIMATION_ID } from "@/lib/animations";
|
||||
import type { AnimationId } from "@/lib/animations";
|
||||
|
||||
export function getStoredAnimationId(): AnimationId {
|
||||
if (typeof window === "undefined") return DEFAULT_ANIMATION_ID;
|
||||
const stored = localStorage.getItem("animation");
|
||||
if (stored && (ANIMATION_IDS as readonly string[]).includes(stored)) {
|
||||
return stored as AnimationId;
|
||||
}
|
||||
return DEFAULT_ANIMATION_ID;
|
||||
}
|
||||
|
||||
export function saveAnimation(id: AnimationId): void {
|
||||
localStorage.setItem("animation", id);
|
||||
}
|
||||
|
||||
export function getNextAnimation(currentId: AnimationId): AnimationId {
|
||||
const idx = ANIMATION_IDS.indexOf(currentId);
|
||||
return ANIMATION_IDS[(idx + 1) % ANIMATION_IDS.length];
|
||||
}
|
||||
12
src/lib/animations/index.ts
Normal file
12
src/lib/animations/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const ANIMATION_IDS = ["shuffle", "game-of-life", "lava-lamp", "confetti", "asciiquarium", "pipes"] as const;
|
||||
export type AnimationId = (typeof ANIMATION_IDS)[number];
|
||||
export const DEFAULT_ANIMATION_ID: AnimationId = "shuffle";
|
||||
|
||||
export const ANIMATION_LABELS: Record<AnimationId, string> = {
|
||||
"shuffle": "shuffle",
|
||||
"game-of-life": "life",
|
||||
"lava-lamp": "lava",
|
||||
"confetti": "confetti",
|
||||
"asciiquarium": "aquarium",
|
||||
"pipes": "pipes",
|
||||
};
|
||||
7
src/lib/animations/loader.ts
Normal file
7
src/lib/animations/loader.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { ANIMATION_IDS, DEFAULT_ANIMATION_ID } from "@/lib/animations";
|
||||
|
||||
const VALID_IDS = JSON.stringify(ANIMATION_IDS);
|
||||
|
||||
export const ANIMATION_LOADER_SCRIPT = `(function(){var id=localStorage.getItem("animation");if(id&&${VALID_IDS}.indexOf(id)!==-1)document.documentElement.dataset.animation=id;else document.documentElement.dataset.animation="${DEFAULT_ANIMATION_ID}";})();`;
|
||||
|
||||
export const ANIMATION_NAV_SCRIPT = `document.addEventListener("astro:after-navigation",function(){var id=localStorage.getItem("animation");if(id&&${VALID_IDS}.indexOf(id)!==-1)document.documentElement.dataset.animation=id;else document.documentElement.dataset.animation="${DEFAULT_ANIMATION_ID}";});`;
|
||||
37
src/lib/animations/types.ts
Normal file
37
src/lib/animations/types.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export interface AnimationEngine {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
init(
|
||||
width: number,
|
||||
height: number,
|
||||
palette: [number, number, number][],
|
||||
bgColor: string
|
||||
): void;
|
||||
|
||||
update(deltaTime: number): void;
|
||||
|
||||
render(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number
|
||||
): void;
|
||||
|
||||
handleResize(width: number, height: number): void;
|
||||
|
||||
handleMouseMove(x: number, y: number, isDown: boolean): void;
|
||||
|
||||
handleMouseDown(x: number, y: number): void;
|
||||
|
||||
handleMouseUp(): void;
|
||||
|
||||
handleMouseLeave(): void;
|
||||
|
||||
updatePalette(palette: [number, number, number][], bgColor: string): void;
|
||||
|
||||
beginExit(): void;
|
||||
|
||||
isExitComplete(): boolean;
|
||||
|
||||
cleanup(): void;
|
||||
}
|
||||
59
src/lib/structuredData.ts
Normal file
59
src/lib/structuredData.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { type Article, type Person, type WebSite, type WithContext } from "schema-dts";
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
|
||||
export const blogWebsite: WithContext<WebSite> = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
url: `${import.meta.env.SITE}/blog/`,
|
||||
name: "Timothy Pidsashev - Blog",
|
||||
description: "Timothy Pidsashev's blog",
|
||||
inLanguage: "en_US",
|
||||
};
|
||||
|
||||
export const mainWebsite: WithContext<WebSite> = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
url: import.meta.env.SITE,
|
||||
name: "Timothy Pidashev - Personal website",
|
||||
description: "Timothy Pidashev's contact page, portfolio and blog",
|
||||
inLanguage: "en_US",
|
||||
};
|
||||
|
||||
export const personSchema: WithContext<Person> = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Person",
|
||||
name: "Timothy Pidashev",
|
||||
url: "https://timmypidashev.dev",
|
||||
sameAs: [
|
||||
"https://github.com/timmypidashev",
|
||||
"https://www.linkedin.com/in/timothy-pidashev-4353812b8",
|
||||
],
|
||||
jobTitle: "Software Engineer",
|
||||
worksFor: {
|
||||
"@type": "Organization",
|
||||
name: "Fathers House Christian Center",
|
||||
url: "https://fhccenter.org",
|
||||
},
|
||||
};
|
||||
|
||||
export function getArticleSchema(post: CollectionEntry<"blog">) {
|
||||
const articleStructuredData: WithContext<Article> = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: post.data.title,
|
||||
url: `${import.meta.env.SITE}/blog/${post.id}/`,
|
||||
description: post.data.excerpt,
|
||||
datePublished: post.data.date.toString(),
|
||||
publisher: {
|
||||
"@type": "Person",
|
||||
name: "Timothy Pidashev",
|
||||
url: import.meta.env.SITE,
|
||||
},
|
||||
author: {
|
||||
"@type": "Person",
|
||||
name: "Timothy Pidashev",
|
||||
url: import.meta.env.SITE,
|
||||
},
|
||||
};
|
||||
return articleStructuredData;
|
||||
}
|
||||
61
src/lib/themes/engine.ts
Normal file
61
src/lib/themes/engine.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { THEMES, DEFAULT_THEME_ID } from "@/lib/themes";
|
||||
import { CSS_PROPS } from "@/lib/themes/props";
|
||||
import type { Theme } from "@/lib/themes/types";
|
||||
|
||||
export function getStoredThemeId(): string {
|
||||
if (typeof window === "undefined") return DEFAULT_THEME_ID;
|
||||
return localStorage.getItem("theme") || DEFAULT_THEME_ID;
|
||||
}
|
||||
|
||||
export function saveTheme(id: string): void {
|
||||
localStorage.setItem("theme", id);
|
||||
}
|
||||
|
||||
export function getNextTheme(currentId: string): Theme {
|
||||
const list = Object.values(THEMES);
|
||||
const idx = list.findIndex((t) => t.id === currentId);
|
||||
return list[(idx + 1) % list.length];
|
||||
}
|
||||
|
||||
/** Sets CSS vars and notifies canvas, but does NOT persist to localStorage. */
|
||||
export function previewTheme(id: string): void {
|
||||
const theme = THEMES[id];
|
||||
if (!theme) return;
|
||||
|
||||
const root = document.documentElement;
|
||||
for (const [key, prop] of CSS_PROPS) {
|
||||
root.style.setProperty(prop, theme.colors[key]);
|
||||
}
|
||||
|
||||
|
||||
document.dispatchEvent(new CustomEvent("theme-changed", { detail: { id } }));
|
||||
}
|
||||
|
||||
export function applyTheme(id: string): void {
|
||||
const theme = THEMES[id];
|
||||
if (!theme) return;
|
||||
|
||||
// Set CSS vars on :root for immediate visual update
|
||||
const root = document.documentElement;
|
||||
for (const [key, prop] of CSS_PROPS) {
|
||||
root.style.setProperty(prop, theme.colors[key]);
|
||||
}
|
||||
|
||||
// Update <style id="theme-vars"> so Astro view transitions don't revert
|
||||
let el = document.getElementById("theme-vars") as HTMLStyleElement | null;
|
||||
if (!el) {
|
||||
el = document.createElement("style");
|
||||
el.id = "theme-vars";
|
||||
document.head.appendChild(el);
|
||||
}
|
||||
let css = ":root{";
|
||||
for (const [key, prop] of CSS_PROPS) {
|
||||
css += `${prop}:${theme.colors[key]};`;
|
||||
}
|
||||
css += "}";
|
||||
el.textContent = css;
|
||||
|
||||
saveTheme(id);
|
||||
|
||||
document.dispatchEvent(new CustomEvent("theme-changed", { detail: { id } }));
|
||||
}
|
||||
58
src/lib/themes/index.ts
Normal file
58
src/lib/themes/index.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { Theme } from "./types";
|
||||
|
||||
export const DEFAULT_THEME_ID = "darkbox-retro";
|
||||
|
||||
function theme(
|
||||
id: string,
|
||||
name: string,
|
||||
type: "dark" | "light",
|
||||
colors: Theme["colors"],
|
||||
palette: [number, number, number][]
|
||||
): Theme {
|
||||
return { id, name, type, colors, canvasPalette: palette };
|
||||
}
|
||||
|
||||
// Three darkbox variants from darkbox.nvim
|
||||
// Classic (vivid) → Retro (muted) → Dim (deep)
|
||||
// Each variant's "bright" is the next level up's base.
|
||||
|
||||
export const THEMES: Record<string, Theme> = {
|
||||
darkbox: theme("darkbox", "Darkbox Classic", "dark", {
|
||||
background: "0 0 0",
|
||||
foreground: "235 219 178",
|
||||
red: "251 73 52", redBright: "255 110 85",
|
||||
orange: "254 128 25", orangeBright: "255 165 65",
|
||||
green: "184 187 38", greenBright: "210 215 70",
|
||||
yellow: "250 189 47", yellowBright: "255 215 85",
|
||||
blue: "131 165 152", blueBright: "165 195 180",
|
||||
purple: "211 134 155", purpleBright: "235 165 180",
|
||||
aqua: "142 192 124", aquaBright: "175 220 160",
|
||||
surface: "60 56 54",
|
||||
}, [[251,73,52],[184,187,38],[250,189,47],[131,165,152],[211,134,155],[142,192,124]]),
|
||||
|
||||
"darkbox-retro": theme("darkbox-retro", "Darkbox Retro", "dark", {
|
||||
background: "0 0 0",
|
||||
foreground: "189 174 147",
|
||||
red: "204 36 29", redBright: "251 73 52",
|
||||
orange: "214 93 14", orangeBright: "254 128 25",
|
||||
green: "152 151 26", greenBright: "184 187 38",
|
||||
yellow: "215 153 33", yellowBright: "250 189 47",
|
||||
blue: "69 133 136", blueBright: "131 165 152",
|
||||
purple: "177 98 134", purpleBright: "211 134 155",
|
||||
aqua: "104 157 106", aquaBright: "142 192 124",
|
||||
surface: "60 56 54",
|
||||
}, [[204,36,29],[152,151,26],[215,153,33],[69,133,136],[177,98,134],[104,157,106]]),
|
||||
|
||||
"darkbox-dim": theme("darkbox-dim", "Darkbox Dim", "dark", {
|
||||
background: "0 0 0",
|
||||
foreground: "168 153 132",
|
||||
red: "157 0 6", redBright: "204 36 29",
|
||||
orange: "175 58 3", orangeBright: "214 93 14",
|
||||
green: "121 116 14", greenBright: "152 151 26",
|
||||
yellow: "181 118 20", yellowBright: "215 153 33",
|
||||
blue: "7 102 120", blueBright: "69 133 136",
|
||||
purple: "143 63 113", purpleBright: "177 98 134",
|
||||
aqua: "66 123 88", aquaBright: "104 157 106",
|
||||
surface: "60 56 54",
|
||||
}, [[157,0,6],[121,116,14],[181,118,20],[7,102,120],[143,63,113],[66,123,88]]),
|
||||
};
|
||||
25
src/lib/themes/loader.ts
Normal file
25
src/lib/themes/loader.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Generates the inline <script> content for theme loading.
|
||||
* Called at build time in Astro frontmatter.
|
||||
* The script reads "theme" from localStorage, looks up colors, injects a <style> tag.
|
||||
*/
|
||||
import { THEMES } from "@/lib/themes";
|
||||
import { CSS_PROPS } from "@/lib/themes/props";
|
||||
|
||||
// Pre-build a { prop: value } map for each theme at build time
|
||||
const themeVars: Record<string, Record<string, string>> = {};
|
||||
for (const [id, theme] of Object.entries(THEMES)) {
|
||||
const vars: Record<string, string> = {};
|
||||
for (const [key, prop] of CSS_PROPS) {
|
||||
vars[prop] = theme.colors[key];
|
||||
}
|
||||
themeVars[id] = vars;
|
||||
}
|
||||
|
||||
// Sets inline styles on <html> — highest specificity, beats any stylesheet
|
||||
const APPLY = `var v=t[id];if(!v)return;var s=document.documentElement.style;for(var k in v)s.setProperty(k,v[k])`;
|
||||
const LOOKUP = `var id=localStorage.getItem("theme");if(!id)return;var t=${JSON.stringify(themeVars)};`;
|
||||
|
||||
export const THEME_LOADER_SCRIPT = `(function(){${LOOKUP}${APPLY}})();`;
|
||||
|
||||
export const THEME_NAV_SCRIPT = `document.addEventListener("astro:after-navigation",function(){${LOOKUP}${APPLY}});`;
|
||||
21
src/lib/themes/props.ts
Normal file
21
src/lib/themes/props.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { ThemeColors } from "@/lib/themes/types";
|
||||
|
||||
export const CSS_PROPS: [keyof ThemeColors, string][] = [
|
||||
["background", "--color-background"],
|
||||
["foreground", "--color-foreground"],
|
||||
["red", "--color-red"],
|
||||
["redBright", "--color-red-bright"],
|
||||
["orange", "--color-orange"],
|
||||
["orangeBright", "--color-orange-bright"],
|
||||
["green", "--color-green"],
|
||||
["greenBright", "--color-green-bright"],
|
||||
["yellow", "--color-yellow"],
|
||||
["yellowBright", "--color-yellow-bright"],
|
||||
["blue", "--color-blue"],
|
||||
["blueBright", "--color-blue-bright"],
|
||||
["purple", "--color-purple"],
|
||||
["purpleBright", "--color-purple-bright"],
|
||||
["aqua", "--color-aqua"],
|
||||
["aquaBright", "--color-aqua-bright"],
|
||||
["surface", "--color-surface"],
|
||||
];
|
||||
27
src/lib/themes/types.ts
Normal file
27
src/lib/themes/types.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export interface ThemeColors {
|
||||
background: string;
|
||||
foreground: string;
|
||||
red: string;
|
||||
redBright: string;
|
||||
orange: string;
|
||||
orangeBright: string;
|
||||
green: string;
|
||||
greenBright: string;
|
||||
yellow: string;
|
||||
yellowBright: string;
|
||||
blue: string;
|
||||
blueBright: string;
|
||||
purple: string;
|
||||
purpleBright: string;
|
||||
aqua: string;
|
||||
aquaBright: string;
|
||||
surface: string;
|
||||
}
|
||||
|
||||
export interface Theme {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "dark" | "light";
|
||||
colors: ThemeColors;
|
||||
canvasPalette: [number, number, number][];
|
||||
}
|
||||
Reference in New Issue
Block a user