website-xp-phone 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.astro/content-assets.mjs +1 -0
  2. package/.astro/content-modules.mjs +1 -0
  3. package/.astro/content.d.ts +199 -0
  4. package/.astro/data-store.json +1 -0
  5. package/.astro/settings.json +8 -0
  6. package/.astro/types.d.ts +1 -0
  7. package/.devcontainer/devcontainer.json +23 -0
  8. package/.env.firebase.example +8 -0
  9. package/.firebaserc +5 -0
  10. package/.gitattributes +2 -0
  11. package/.github/copilot-instructions.md +131 -0
  12. package/.github/dependabot.yml +11 -0
  13. package/.github/workflows/ci.yml +45 -0
  14. package/.github/workflows/deploy-admin.yml +48 -0
  15. package/.github/workflows/static.yml +43 -0
  16. package/.gitmodules +5 -0
  17. package/FIREBASE_SETUP.md +69 -0
  18. package/README.md +63 -0
  19. package/SECURITY.md +11 -0
  20. package/admin/Admin.csproj +7 -0
  21. package/admin/Dockerfile +14 -0
  22. package/admin/Program.cs +8 -0
  23. package/deploy-admin-cloud-run.md +229 -0
  24. package/eslint.config.js +28 -0
  25. package/firebase.json +5 -0
  26. package/firestore.rules +29 -0
  27. package/index.html +52 -0
  28. package/package.json +48 -0
  29. package/pagerts_output.json +1 -0
  30. package/public/5.html +967 -0
  31. package/public/BAHNSCHRIFT.TTF +0 -0
  32. package/public/Beep.ogg +0 -0
  33. package/public/Clippy.png +0 -0
  34. package/public/Layered Network Security Model for Home Networks (slides).pdf +0 -0
  35. package/public/Layered Network Security Model for Home Networks.pdf +0 -0
  36. package/public/TODO.pdf +0 -0
  37. package/public/WoW_Config.zip +3 -0
  38. package/public/addons/energy-swing.txt +1 -0
  39. package/public/addons/lego-yoda-death-readme.txt +11 -0
  40. package/public/addons/lego-yoda-death.mp3 +0 -0
  41. package/public/addons/mana-blast.txt +1 -0
  42. package/public/addons/rage-volley.txt +1 -0
  43. package/public/addons/rueg-cell.txt +1 -0
  44. package/public/addons/rueg-elvui-profile.txt +1 -0
  45. package/public/addons/rueg-grid2.txt +214 -0
  46. package/public/addons/rueg-plater-smol.txt +1 -0
  47. package/public/addons/rueg-plater.txt +1 -0
  48. package/public/addons/rueg-wa-druid.txt +1 -0
  49. package/public/addons/rueg-wa-priest.txt +1 -0
  50. package/public/addons/rueg-wa-rogue.txt +1 -0
  51. package/public/addons/rueg-wa-shaman.txt +1 -0
  52. package/public/addons/rueg-wa-warrior.txt +1 -0
  53. package/public/addons/spirit-smash.txt +1 -0
  54. package/public/avatar.jpg +0 -0
  55. package/public/avatar.png +0 -0
  56. package/public/crunchy_kick.ogg +0 -0
  57. package/public/documents/resume.html +312 -0
  58. package/public/favicon.ico +0 -0
  59. package/public/images/Ateric1.png +0 -0
  60. package/public/images/Ateric2.png +0 -0
  61. package/public/images/equal1.png +0 -0
  62. package/public/images/hyperawareofwhatacatis.png +0 -0
  63. package/public/images/kogg1.png +0 -0
  64. package/public/images/kogg2.png +0 -0
  65. package/public/images/rueg1.png +0 -0
  66. package/public/images/rueg2.png +0 -0
  67. package/public/incorrect_responses.txt +126 -0
  68. package/public/loading.css +51 -0
  69. package/public/resume.pdf +0 -0
  70. package/public/robots.txt +9 -0
  71. package/public/soundcloud.json +57 -0
  72. package/public/spinner.svg +12 -0
  73. package/public/tada.wav +0 -0
  74. package/public/yooh.mp3 +0 -0
  75. package/render.yaml +5 -0
  76. package/scripts/ensure-blog-worktree.mjs +24 -0
  77. package/scripts/generate-soundcloud-json.mjs +198 -0
  78. package/scripts/git-worktree-helper.mjs +122 -0
  79. package/scripts/hoist-dev-blog-local.mjs +149 -0
  80. package/scripts/music-schema.mjs +56 -0
  81. package/scripts/publish-soundcloud-json.mjs +32 -0
  82. package/scripts/sync-music-links-from-worktree.mjs +32 -0
  83. package/src/App.tsx +1500 -0
  84. package/src/addons.json +76 -0
  85. package/src/components/Addon.tsx +223 -0
  86. package/src/components/BlogContent.tsx +103 -0
  87. package/src/components/CopyToClipboardButton.tsx +21 -0
  88. package/src/components/MenuBar.tsx +151 -0
  89. package/src/components/MenuBarWithContext.tsx +6 -0
  90. package/src/components/Modal.tsx +17 -0
  91. package/src/components/MusicContent.tsx +309 -0
  92. package/src/components/NavBarController.tsx +55 -0
  93. package/src/components/NavBarControllerWrapper.tsx +13 -0
  94. package/src/components/Page.tsx +56 -0
  95. package/src/components/SitemapContent.tsx +125 -0
  96. package/src/contacts.json +32 -0
  97. package/src/env.d.ts +13 -0
  98. package/src/lib/assistantStateMachine.ts +80 -0
  99. package/src/lib/audioOverlap.ts +99 -0
  100. package/src/lib/keyboardInputUtils.ts +182 -0
  101. package/src/lib/musicSchema.ts +85 -0
  102. package/src/lib/naggingAssistantClient.ts +241 -0
  103. package/src/lib/resumeAnalytics.ts +163 -0
  104. package/src/main.tsx +35 -0
  105. package/src/pages.json +50 -0
  106. package/src/sections.json +243 -0
  107. package/src/src+addons.zip +3 -0
  108. package/src/styles/main.css +465 -0
  109. package/src/utils/blogSecurity.ts +87 -0
  110. package/src/utils/menuItems.ts +33 -0
  111. package/src/windowing/MinimizedSections.tsx +86 -0
  112. package/src/windowing/Section.tsx +586 -0
  113. package/src/windowing/context.tsx +13 -0
  114. package/src/windowing/hooks.ts +10 -0
  115. package/src/windowing/index.ts +7 -0
  116. package/src/windowing/provider.tsx +74 -0
  117. package/src/windowing/server.ts +3 -0
  118. package/src/windowing/types.ts +33 -0
  119. package/src/windowing/utils.ts +135 -0
  120. package/tests/generate-soundcloud-json.test.mjs +63 -0
  121. package/tests/music-schema.test.mjs +53 -0
  122. package/tsconfig.json +26 -0
  123. package/vite.config.ts +304 -0
@@ -0,0 +1,309 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ import { PageContent } from "./Page";
4
+ import { processContent } from "../windowing/utils";
5
+ import { type PageMetadata, type SectionProps } from "../windowing";
6
+
7
+ const SOUNDCLOUD_JSON_URL =
8
+ "https://raw.githubusercontent.com/akinevz2/frontend/refs/heads/blogging/blog/soundcloud.json";
9
+ const MUSIC_LINKS_URL =
10
+ "https://raw.githubusercontent.com/akinevz2/frontend/refs/heads/blogging/blog/music-links.json";
11
+
12
+ type MusicTrack = {
13
+ path: string;
14
+ title: string;
15
+ url: string;
16
+ };
17
+
18
+ type MusicPayload = {
19
+ source: string;
20
+ generatedAt: string;
21
+ trackCount: number;
22
+ tracks: MusicTrack[];
23
+ };
24
+
25
+ type FavouriteLink = {
26
+ title: string;
27
+ url: string;
28
+ };
29
+
30
+ type FavouriteLinkContent = FavouriteLink | SectionProps;
31
+
32
+ type MusicState = {
33
+ sections: SectionProps | SectionProps[] | undefined;
34
+ metadata: PageMetadata;
35
+ error?: string;
36
+ };
37
+
38
+ const LOADING_SECTION: SectionProps = {
39
+ className: "music-loading",
40
+ children: [],
41
+ heading: "Loading...",
42
+ content: [
43
+ "![Loading spinner](/spinner.svg)",
44
+ "Fetching tracks from /soundcloud.json",
45
+ ],
46
+ };
47
+
48
+ const buildState = (content: SectionProps | SectionProps[]): MusicState => {
49
+ const { processed, metadata } = processContent(content);
50
+ return {
51
+ sections: processed as SectionProps | SectionProps[],
52
+ metadata: { sections: metadata },
53
+ };
54
+ };
55
+ const isSectionProps = (value: unknown): value is SectionProps => {
56
+ const candidate = value as Partial<SectionProps>;
57
+ return typeof candidate.heading === "string" && Array.isArray(candidate.content);
58
+ }
59
+ const isFavouriteLink = (value: unknown): value is FavouriteLink => {
60
+ const candidate = value as Partial<FavouriteLink>;
61
+ return typeof candidate.title === "string" && typeof candidate.url === "string";
62
+ }
63
+
64
+ const isValidContentFavLink = (value: unknown): value is FavouriteLink | SectionProps => {
65
+ if (!value || typeof value !== "object") return false;
66
+ console.dir("Validating favourite link content:", value);
67
+ return isSectionProps(value) || isFavouriteLink(value);
68
+ };
69
+
70
+ const fetchFavouriteLinks = async (): Promise<FavouriteLinkContent[]> => {
71
+ const response = await fetch(MUSIC_LINKS_URL, {
72
+ method: "GET",
73
+ cache: "no-store",
74
+ headers: { Accept: "application/json" },
75
+ });
76
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
77
+ const payload: unknown = await response.json();
78
+ if (!Array.isArray(payload)) throw new Error("Invalid music-links schema");
79
+ return payload.filter(isValidContentFavLink);
80
+ };
81
+
82
+ const buildSoundCloudEmbedUrl = (trackUrl: string) => {
83
+ const embedUrl = new URL("https://w.soundcloud.com/player/");
84
+ embedUrl.searchParams.set("url", trackUrl);
85
+ embedUrl.searchParams.set("color", "ff5500");
86
+ embedUrl.searchParams.set("auto_play", "false");
87
+ embedUrl.searchParams.set("hide_related", "false");
88
+ embedUrl.searchParams.set("show_comments", "true");
89
+ embedUrl.searchParams.set("show_user", "true");
90
+ embedUrl.searchParams.set("show_reposts", "false");
91
+ embedUrl.searchParams.set("visual", "false");
92
+ return embedUrl.toString();
93
+ };
94
+
95
+ const renderTrackEmbed = (track: MusicTrack) => {
96
+ const embedUrl = buildSoundCloudEmbedUrl(track.url);
97
+
98
+ return [
99
+ `<p class="music-track-title"><a href="${track.url}">${track.title}</a></p>`,
100
+ `<iframe
101
+ title="SoundCloud track: ${track.title}"
102
+ width="100%"
103
+ height="166"
104
+ scrolling="no"
105
+ frameBorder="no"
106
+ allow="autoplay"
107
+ loading="lazy"
108
+ referrerPolicy="strict-origin-when-cross-origin"
109
+ src="${embedUrl}"
110
+ ></iframe>`,
111
+ `[Open on SoundCloud](${track.url})`,
112
+ ].join("\n\n");
113
+ };
114
+
115
+ const getSpotifyEmbedUrl = (urlValue: string): string | null => {
116
+ try {
117
+ const parsed = new URL(urlValue);
118
+ if (parsed.hostname !== "open.spotify.com") {
119
+ return null;
120
+ }
121
+
122
+ const segments = parsed.pathname.split("/").filter(Boolean);
123
+ if (segments.length < 2) {
124
+ return null;
125
+ }
126
+
127
+ const resourceType = segments[0];
128
+ const resourceId = segments[1];
129
+ if (!resourceType || !resourceId) {
130
+ return null;
131
+ }
132
+
133
+ const allowedTypes = new Set(["track", "playlist", "album", "artist", "episode", "show"]);
134
+ if (!allowedTypes.has(resourceType)) {
135
+ return null;
136
+ }
137
+
138
+ return `https://open.spotify.com/embed/${resourceType}/${resourceId}`;
139
+ } catch {
140
+ return null;
141
+ }
142
+ };
143
+
144
+ const renderFavouriteLink = (link: FavouriteLink) => {
145
+ // If it's already a SectionProps, return it as-is
146
+ if ("content" in link) {
147
+ // Convert SectionProps to string representation
148
+ const section = link as SectionProps;
149
+ return section;
150
+ }
151
+
152
+ // Otherwise, it's a simple link
153
+ const favouriteLink = link as { title: string; url: string };
154
+
155
+ const embedUrl = getSpotifyEmbedUrl(favouriteLink.url);
156
+ if (!embedUrl) {
157
+ return `- [${favouriteLink.title}](${favouriteLink.url})`;
158
+ }
159
+
160
+ return [
161
+ `<p class="music-track-title"><a href="${favouriteLink.url}">${favouriteLink.title}</a></p>`,
162
+ `<iframe
163
+ title="Spotify item: ${favouriteLink.title}"
164
+ style="border-radius:12px"
165
+ src="${embedUrl}"
166
+ width="100%"
167
+ height="152"
168
+ frameborder="0"
169
+ allowfullscreen=""
170
+ allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
171
+ loading="lazy"
172
+ referrerpolicy="strict-origin-when-cross-origin"
173
+ ></iframe>`,
174
+ `[Open on Spotify](${favouriteLink.url})`,
175
+ ].join("\n\n");
176
+ };
177
+
178
+ const toMusicSections = (payload: MusicPayload, favouriteLinks: FavouriteLinkContent[]): SectionProps[] => {
179
+ const trackEmbeds = payload.tracks.map((track) => renderTrackEmbed(track));
180
+
181
+ const favouriteLinkList = favouriteLinks.map((link) => (isFavouriteLink(link)) ? renderFavouriteLink(link) : link);
182
+
183
+ // print all non-string to log
184
+ for (const link of favouriteLinkList) {
185
+ if (typeof link !== "string") {
186
+ console.dir("Non-string favourite link content:", link);
187
+ }
188
+ }
189
+
190
+ return [
191
+ {
192
+ className: "music-favorite-uploads",
193
+ children: [],
194
+ heading: "Favorite Uploads",
195
+ content:
196
+ trackEmbeds.length > 0
197
+ ? [
198
+ `Generated: ${new Date(payload.generatedAt).toLocaleString()}`,
199
+ `Track count: ${payload.trackCount}`,
200
+ ...trackEmbeds,
201
+ ]
202
+ : ["No tracks found in this snapshot."],
203
+ },
204
+ {
205
+ className: "music-favourite-links",
206
+ children: [],
207
+ heading: "Favourite Links",
208
+ content:
209
+ favouriteLinkList.length > 0
210
+ ? favouriteLinkList
211
+ : ["No favourite links configured yet."],
212
+ },
213
+ {
214
+ className: "music-source",
215
+ children: [],
216
+ heading: "Source",
217
+ content: [
218
+ `Artist page: [${payload.source}](${payload.source})`,
219
+ `<iframe data-testid="embed-iframe" style="border-radius:12px" src="https://open.spotify.com/embed/artist/2vy7FXU6dP4OEBiJVjsw7r?utm_source=generator&theme=0&si=0c8005cead7543c5" width="100%" height="352" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>`,
220
+ ],
221
+ },
222
+ ];
223
+ };
224
+
225
+ const isMusicPayload = (value: unknown): value is MusicPayload => {
226
+ if (!value || typeof value !== "object") return false;
227
+
228
+ const candidate = value as Partial<MusicPayload>;
229
+ return (
230
+ typeof candidate.source === "string" &&
231
+ typeof candidate.generatedAt === "string" &&
232
+ typeof candidate.trackCount === "number" &&
233
+ Array.isArray(candidate.tracks)
234
+ );
235
+ };
236
+
237
+ export default function MusicContent() {
238
+ const [musicState, setMusicState] = useState<MusicState>(() =>
239
+ buildState(LOADING_SECTION),
240
+ );
241
+
242
+ useEffect(() => {
243
+ let cancelled = false;
244
+
245
+ const load = async () => {
246
+ try {
247
+ const [scResponse, favouriteLinks] = await Promise.all([
248
+ fetch(SOUNDCLOUD_JSON_URL, {
249
+ method: "GET",
250
+ cache: "no-store",
251
+ headers: { Accept: "application/json" },
252
+ }).catch(() => {
253
+ return fetch("/soundcloud.json", {
254
+ method: "GET",
255
+ cache: "no-store",
256
+ headers: { Accept: "application/json" },
257
+ })
258
+ }),
259
+ fetchFavouriteLinks().catch(() => [] as FavouriteLink[]),
260
+ ]);
261
+
262
+ if (!scResponse.ok) {
263
+ throw new Error(`HTTP ${scResponse.status}`);
264
+ }
265
+
266
+ const payload: unknown = await scResponse.json();
267
+ if (!isMusicPayload(payload)) {
268
+ throw new Error("Invalid music payload schema");
269
+ }
270
+
271
+ if (!cancelled) {
272
+ setMusicState(buildState(toMusicSections(payload, favouriteLinks)));
273
+ }
274
+ } catch (error) {
275
+ if (!cancelled) {
276
+ const message =
277
+ error instanceof Error ? error.message : "Unknown error";
278
+ setMusicState({
279
+ sections: undefined,
280
+ metadata: { sections: [] },
281
+ error: `Failed to load music data (${message}).`,
282
+ });
283
+ }
284
+ }
285
+ };
286
+
287
+ void load();
288
+
289
+ return () => {
290
+ cancelled = true;
291
+ };
292
+ }, []);
293
+
294
+ return (
295
+ <>
296
+ {musicState.error ? (
297
+ <p className="status-bar">{musicState.error}</p>
298
+ ) : null}
299
+ {musicState.sections ? (
300
+ <PageContent
301
+ sections={musicState.sections}
302
+ pageMetadata={musicState.metadata}
303
+ />
304
+ ) : (
305
+ <PageContent pageMetadata={musicState.metadata} />
306
+ )}
307
+ </>
308
+ );
309
+ }
@@ -0,0 +1,55 @@
1
+ import { useEffect } from "react";
2
+
3
+ // Temporary stub - navbar visibility logic
4
+ const useIsNavBarVisible = () => true;
5
+
6
+ export const NavBarController = () => {
7
+ const isVisible = useIsNavBarVisible();
8
+ const isHomePage =
9
+ window.location.pathname === "/" ||
10
+ window.location.pathname === "/index.html";
11
+ const shouldShow = !isHomePage || isVisible;
12
+
13
+ useEffect(() => {
14
+ const navbar = document.querySelector("nav");
15
+ const body = document.body;
16
+
17
+ if (navbar && body) {
18
+ // Add transition for smooth animation
19
+ navbar.style.transition = "bottom 0.4s ease-out, opacity 0.4s ease-out";
20
+
21
+ if (shouldShow) {
22
+ navbar.style.opacity = "1";
23
+ body.classList.add("nav-visible");
24
+
25
+ // Position at bottom on non-home pages
26
+ if (!isHomePage) {
27
+ navbar.style.position = "absolute";
28
+ navbar.style.top = "auto";
29
+ navbar.style.left = "50%";
30
+ navbar.style.transform = "translateX(-50%)";
31
+ navbar.style.bottom = "10px";
32
+ navbar.style.marginTop = "0";
33
+ } else {
34
+ navbar.style.transform = "translateY(0)";
35
+ }
36
+ } else {
37
+ navbar.style.opacity = "0";
38
+ // Position below viewport on non-home pages, above viewport on home page
39
+ if (!isHomePage) {
40
+ navbar.style.position = "absolute";
41
+ navbar.style.top = "auto";
42
+ navbar.style.left = "50%";
43
+ navbar.style.transform = "translateX(-50%)";
44
+ // Push completely off page
45
+ navbar.style.bottom = "-200px";
46
+ } else {
47
+ navbar.style.transform = "translateY(-100%)";
48
+ }
49
+ body.classList.remove("nav-visible");
50
+ }
51
+ }
52
+ }, [shouldShow, isHomePage]);
53
+
54
+ return null;
55
+ };
@@ -0,0 +1,13 @@
1
+ import { SectionProvider } from "../windowing";
2
+ import { NavBarController } from "./NavBarController";
3
+
4
+ export const NavBarControllerWrapper = () => {
5
+ // Create empty metadata for navbar context
6
+ const pageMetadata = { sections: [] };
7
+
8
+ return (
9
+ <SectionProvider pageMetadata={pageMetadata}>
10
+ <NavBarController />
11
+ </SectionProvider>
12
+ );
13
+ };
@@ -0,0 +1,56 @@
1
+ import { Addon, AddonList, type AddonProps } from "./Addon";
2
+ import {
3
+ Section,
4
+ SectionProvider,
5
+ type SectionProps,
6
+ type PageMetadata,
7
+ } from "../windowing";
8
+ import { MenuBarWithContext } from "./MenuBarWithContext";
9
+
10
+ export type PageProps = {
11
+ sections?: SectionProps | SectionProps[];
12
+ addons?: AddonProps | AddonProps[];
13
+ pageMetadata?: PageMetadata;
14
+ };
15
+
16
+ export const PageContent = ({ sections, pageMetadata }: PageProps) => {
17
+ const metadata = pageMetadata || { sections: [] };
18
+
19
+ if (!sections) {
20
+ return null;
21
+ }
22
+
23
+ return (
24
+ <SectionProvider pageMetadata={metadata}>
25
+ <section className="page">
26
+ {Array.isArray(sections)
27
+ ? sections.map((item, index) => (
28
+ <Section key={item.uuid || index} {...item} />
29
+ ))
30
+ : <Section {...sections} />}
31
+ </section>
32
+ <MenuBarWithContext />
33
+ </SectionProvider>
34
+ );
35
+ };
36
+
37
+ export const PageWithAddons = ({ addons, pageMetadata }: PageProps) => {
38
+ const metadata = pageMetadata || { sections: [] };
39
+
40
+ if (!addons) {
41
+ return null;
42
+ }
43
+
44
+ return (
45
+ <SectionProvider pageMetadata={metadata}>
46
+ <section className="page">
47
+ {Array.isArray(addons)
48
+ ? addons.map((item, index) => (
49
+ <Addon key={item.uuid || index} {...item} />
50
+ ))
51
+ : <AddonList {...addons} />}
52
+ </section>
53
+ <MenuBarWithContext />
54
+ </SectionProvider>
55
+ );
56
+ };
@@ -0,0 +1,125 @@
1
+ import { useMemo } from "react";
2
+
3
+ import pages from "../pages.json";
4
+ import { PageContent } from "./Page";
5
+ import { processContent } from "../windowing/utils";
6
+ import type { PageMetadata, SectionProps } from "../windowing";
7
+
8
+ type RouteDefinition = {
9
+ path: string;
10
+ title: string;
11
+ description: string;
12
+ menuLabel?: string;
13
+ };
14
+
15
+ type SitemapSection = {
16
+ heading: string;
17
+ summary: string;
18
+ routes: RouteDefinition[];
19
+ };
20
+
21
+ const SITE_ROUTE_DEFINITIONS = pages as RouteDefinition[];
22
+
23
+ const SITE_ASSETS: SectionProps[] = [
24
+ {
25
+ heading: "Crawler assets",
26
+ link: "/robots.txt",
27
+ content: [
28
+ "[robots.txt](/robots.txt)",
29
+ "[XML sitemap](/sitemap.xml)",
30
+ "[SoundCloud snapshot](/soundcloud.json)",
31
+ ],
32
+ },
33
+ {
34
+ heading: "Common public files",
35
+ content: [
36
+ "[Resume PDF](/resume.pdf)",
37
+ "[Resume HTML](/documents/resume.html)",
38
+ "[Avatar](/avatar.png)",
39
+ "[Favicon](/favicon.ico)",
40
+ "[Loading stylesheet](/loading.css)",
41
+ ],
42
+ },
43
+ ];
44
+
45
+ const SITE_MAP_GROUPS: SitemapSection[] = [
46
+ {
47
+ heading: "Core navigation",
48
+ summary: "The pages people are most likely to start with.",
49
+ routes: SITE_ROUTE_DEFINITIONS.filter(({ path }) =>
50
+ ["/", "/blog", "/music"].includes(path),
51
+ ),
52
+ },
53
+ {
54
+ heading: "Interactive surfaces",
55
+ summary: "The utility pages that support downloads, contact, and tooling.",
56
+ routes: SITE_ROUTE_DEFINITIONS.filter(({ path }) =>
57
+ ["/addons", "/wow", "/contact", "/resume"].includes(path),
58
+ ),
59
+ },
60
+ {
61
+ heading: "Index and diagnostics",
62
+ summary: "Files and pages that help crawlers understand the site.",
63
+ routes: SITE_ROUTE_DEFINITIONS.filter(({ path }) => ["/sitemap"].includes(path)),
64
+ },
65
+ ];
66
+
67
+ const buildRouteSections = (group: SitemapSection): SectionProps => ({
68
+ heading: group.heading,
69
+ content: [
70
+ group.summary,
71
+ ...group.routes.map((route) => ({
72
+ heading: route.title,
73
+ link: route.path,
74
+ content: [
75
+ `Path: [${route.path}](${route.path})`,
76
+ route.description,
77
+ route.menuLabel
78
+ ? `Menu label: ${route.menuLabel}`
79
+ : "Menu label: hidden from navigation",
80
+ ],
81
+ })),
82
+ ],
83
+ });
84
+
85
+ const buildSitemapSections = (): SectionProps[] => [
86
+ {
87
+ className: "sitemap-intro",
88
+ heading: "Sitemap",
89
+ content: [
90
+ "This page is a human-readable map of the website. It mirrors the crawlable route list and points at the files search engines should use first.",
91
+ "Start with [robots.txt](/robots.txt) and [the XML sitemap](/sitemap.xml), then use the route groups below to jump directly to the part of the site you want.",
92
+ ],
93
+ },
94
+ ...SITE_MAP_GROUPS.map(buildRouteSections),
95
+ {
96
+ className: "sitemap-assets",
97
+ heading: "Assets and references",
98
+ content: [
99
+ "These are the supporting files that help the site load, render, or surface richer snippets.",
100
+ ...SITE_ASSETS,
101
+ ],
102
+ },
103
+ {
104
+ className: "sitemap-notes",
105
+ heading: "Indexing notes",
106
+ content: [
107
+ "The XML sitemap is generated at build time from the same route registry that drives the navigation menu, so the crawl surface stays aligned with the site structure.",
108
+ "The homepage also publishes MusicGroup structured data so the music profile, social links, and latest SoundCloud releases stay discoverable.",
109
+ ],
110
+ },
111
+ ];
112
+
113
+ export default function SitemapContent() {
114
+ const { processed, metadata } = useMemo(() => {
115
+ const sections = buildSitemapSections();
116
+ return processContent(sections);
117
+ }, []);
118
+
119
+ return (
120
+ <PageContent
121
+ sections={processed as SectionProps | SectionProps[]}
122
+ pageMetadata={{ sections: metadata } satisfies PageMetadata}
123
+ />
124
+ );
125
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "heading": "Socials",
3
+ "content": [
4
+ "I hope that I have added enough!",
5
+ {
6
+ "heading": "Email",
7
+ "content": [
8
+ "[akinevz@gmail.com](mailto:akinevz@gmail.com)",
9
+ "[akinevz@outlook.com](mailto:akinevz@outlook.com)"
10
+ ]
11
+ },
12
+ {
13
+ "heading": "GitHub",
14
+ "content": [
15
+ "[akinevz(archive)](https://github.com/akinevz)",
16
+ "[akinevz2](https://github.com/akinevz2)"
17
+ ]
18
+ },
19
+ {
20
+ "heading": "Social",
21
+ "content": [
22
+ "[LinkedIn](https://www.linkedin.com/in/akinevz/)",
23
+ "[Instagram](https://www.instagram.com/akinevz2/)",
24
+ "[Twitter/X](https://x.com/akinevz)"
25
+ ]
26
+ },
27
+ {
28
+ "heading": "Discord",
29
+ "content": "[kine_](https://discord.com/channels/@me/)"
30
+ }
31
+ ]
32
+ }
package/src/env.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ interface ImportMetaEnv {
2
+ readonly VITE_FIREBASE_API_KEY?: string;
3
+ readonly VITE_FIREBASE_AUTH_DOMAIN?: string;
4
+ readonly VITE_FIREBASE_PROJECT_ID?: string;
5
+ readonly VITE_FIREBASE_STORAGE_BUCKET?: string;
6
+ readonly VITE_FIREBASE_MESSAGING_SENDER_ID?: string;
7
+ readonly VITE_FIREBASE_APP_ID?: string;
8
+ readonly VITE_FIREBASE_MEASUREMENT_ID?: string;
9
+ }
10
+
11
+ interface ImportMeta {
12
+ readonly env: ImportMetaEnv;
13
+ }
@@ -0,0 +1,80 @@
1
+ import type { AssistantConfig } from "./naggingAssistantClient";
2
+
3
+ export type AssistantPromptOptions = {
4
+ closeModalOnSubmit?: boolean;
5
+ // wisdomRequest?: boolean;
6
+ };
7
+
8
+ export type ClippyShadowState = {
9
+ isSubmitPulseActive: boolean;
10
+ isConnectionFlashActive: boolean;
11
+ showConversationModal: boolean;
12
+ // isWisdomRequestPending: boolean;
13
+ // wisdomPulsePhase: number;
14
+ isAssistantRequestPending: boolean;
15
+ isClippyHovered: boolean;
16
+ };
17
+
18
+ // Wisdom feature is disabled in production due to unresolved CORS constraints.
19
+ // const CLIPPY_WISDOM_PROMPTS = [
20
+ // "Give me one oddly practical life tip.",
21
+ // "Share one short piece of weird-but-useful wisdom.",
22
+ // "Offer one concise line of advice for focus.",
23
+ // ];
24
+
25
+ export const hasConfiguredAssistant = (config: AssistantConfig) =>
26
+ !!config.endpoint.trim() && !!config.model.trim();
27
+
28
+ export const shouldShowInTransitPulse = (options?: AssistantPromptOptions) =>
29
+ !!options?.closeModalOnSubmit;
30
+
31
+ // export const pickRandomWisdomPrompt = () => {
32
+ // const fallbackPrompt = "Share one concise piece of practical advice.";
33
+ // return (
34
+ // CLIPPY_WISDOM_PROMPTS[
35
+ // Math.floor(Math.random() * CLIPPY_WISDOM_PROMPTS.length)
36
+ // ] ?? fallbackPrompt
37
+ // );
38
+ // };
39
+
40
+ export const buildClippyShadowFilter = (state: ClippyShadowState) => {
41
+ if (state.isSubmitPulseActive) {
42
+ return "drop-shadow(0 0 8px rgba(255, 255, 255, 0.95)) drop-shadow(0 0 18px rgba(255, 255, 255, 0.8))";
43
+ }
44
+
45
+ if (state.isConnectionFlashActive) {
46
+ return "drop-shadow(0 0 8px rgba(220, 30, 30, 0.95)) drop-shadow(0 0 16px rgba(220, 30, 30, 0.8))";
47
+ }
48
+
49
+ if (state.showConversationModal) {
50
+ return "drop-shadow(0 0 8px rgba(0, 190, 70, 0.95)) drop-shadow(0 0 16px rgba(0, 190, 70, 0.8))";
51
+ }
52
+
53
+ // Wisdom pulse shadow intentionally disabled.
54
+ // if (state.isWisdomRequestPending) {
55
+ // const intensity = 0.28 + ((Math.sin(state.wisdomPulsePhase) + 1) / 2) * 0.28;
56
+ // return `drop-shadow(0 0 7px rgba(255, 255, 255, ${intensity.toFixed(3)})) drop-shadow(0 0 14px rgba(255, 255, 255, ${(intensity * 0.8).toFixed(3)}))`;
57
+ // }
58
+
59
+ if (state.isAssistantRequestPending && state.isClippyHovered) {
60
+ return "drop-shadow(0 0 8px rgba(255, 255, 255, 0.9)) drop-shadow(0 0 14px rgba(190, 220, 255, 0.8))";
61
+ }
62
+
63
+ return "drop-shadow(0 6px 12px rgba(0, 0, 0, 0.4))";
64
+ };
65
+
66
+ // export class WisdomPulseClock {
67
+ // private intervalId: number | null = null;
68
+ //
69
+ // start(onTick: () => void, intervalMs: number) {
70
+ // this.stop();
71
+ // this.intervalId = window.setInterval(onTick, intervalMs);
72
+ // }
73
+ //
74
+ // stop() {
75
+ // if (this.intervalId !== null) {
76
+ // window.clearInterval(this.intervalId);
77
+ // this.intervalId = null;
78
+ // }
79
+ // }
80
+ // }