add client side shuffel so every page refresh it stays fresh

This commit is contained in:
2026-03-18 15:54:29 +01:00
parent f1fbdbafdc
commit d10521f04b
11 changed files with 366 additions and 254 deletions

5
.prettierrc Normal file
View File

@@ -0,0 +1,5 @@
{
"plugins": ["prettier-plugin-astro"],
"overrides":
[{ "files": "*.astro", "*.md", "*.mdx", "options": { "parser": "astro" } }],
}

47
package-lock.json generated
View File

@@ -13,9 +13,12 @@
"@astrojs/rss": "^4.0.15", "@astrojs/rss": "^4.0.15",
"@astrojs/sitemap": "^3.7.0", "@astrojs/sitemap": "^3.7.0",
"astro": "^5.17.1", "astro": "^5.17.1",
"prettier": "^3.8.1",
"sharp": "^0.34.3", "sharp": "^0.34.3",
"typescript": "^5.9.3" "typescript": "^5.9.3"
},
"devDependencies": {
"prettier": "3.8.1",
"prettier-plugin-astro": "0.14.1"
} }
}, },
"node_modules/@astrojs/check": { "node_modules/@astrojs/check": {
@@ -5005,6 +5008,21 @@
"url": "https://github.com/prettier/prettier?sponsor=1" "url": "https://github.com/prettier/prettier?sponsor=1"
} }
}, },
"node_modules/prettier-plugin-astro": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/prettier-plugin-astro/-/prettier-plugin-astro-0.14.1.tgz",
"integrity": "sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@astrojs/compiler": "^2.9.1",
"prettier": "^3.0.0",
"sass-formatter": "^0.7.6"
},
"engines": {
"node": "^14.15.0 || >=16.0.0"
}
},
"node_modules/prismjs": { "node_modules/prismjs": {
"version": "1.30.0", "version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
@@ -5447,6 +5465,23 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/s.color": {
"version": "0.0.15",
"resolved": "https://registry.npmjs.org/s.color/-/s.color-0.0.15.tgz",
"integrity": "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/sass-formatter": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/sass-formatter/-/sass-formatter-0.7.9.tgz",
"integrity": "sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"suf-log": "^2.5.3"
}
},
"node_modules/sax": { "node_modules/sax": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz",
@@ -5681,6 +5716,16 @@
"inline-style-parser": "0.2.7" "inline-style-parser": "0.2.7"
} }
}, },
"node_modules/suf-log": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz",
"integrity": "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"s.color": "0.0.15"
}
},
"node_modules/svgo": { "node_modules/svgo": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz",

View File

@@ -17,8 +17,11 @@
"@astrojs/rss": "^4.0.15", "@astrojs/rss": "^4.0.15",
"@astrojs/sitemap": "^3.7.0", "@astrojs/sitemap": "^3.7.0",
"astro": "^5.17.1", "astro": "^5.17.1",
"prettier": "^3.8.1",
"sharp": "^0.34.3", "sharp": "^0.34.3",
"typescript": "^5.9.3" "typescript": "^5.9.3"
},
"devDependencies": {
"prettier": "3.8.1",
"prettier-plugin-astro": "0.14.1"
} }
} }

View File

@@ -1,9 +1,9 @@
--- ---
// Import the global.css file here so that it is included on // Import the global.css file here so that it is included on
// all pages through the use of the <BaseHead /> component. // all pages through the use of the <BaseHead /> component.
import '@styles/global.css'; import "@styles/global.css";
import type { ImageMetadata } from 'astro'; import type { ImageMetadata } from "astro";
import { SITE_TITLE } from '@ptypes/consts.ts'; import { SITE_TITLE } from "@ptypes/consts.ts";
interface Props { interface Props {
title: string; title: string;
@@ -26,13 +26,25 @@ const { title, description } = Astro.props;
rel="alternate" rel="alternate"
type="application/rss+xml" type="application/rss+xml"
title={SITE_TITLE} title={SITE_TITLE}
href={new URL('rss.xml', Astro.site)} href={new URL("rss.xml", Astro.site)}
/> />
<meta name="generator" content={Astro.generator} /> <meta name="generator" content={Astro.generator} />
<!-- Font preloads --> <!-- Font preloads -->
<link rel="preload" href="/fonts/atkinson-regular.woff" as="font" type="font/woff" crossorigin /> <link
<link rel="preload" href="/fonts/atkinson-bold.woff" as="font" type="font/woff" crossorigin /> rel="preload"
href="/fonts/atkinson-regular.woff"
as="font"
type="font/woff"
crossorigin
/>
<link
rel="preload"
href="/fonts/atkinson-bold.woff"
as="font"
type="font/woff"
crossorigin
/>
<!-- Canonical URL --> <!-- Canonical URL -->
<link rel="canonical" href={canonicalURL} /> <link rel="canonical" href={canonicalURL} />

View File

@@ -8,10 +8,10 @@ const { date } = Astro.props;
<time datetime={date.toISOString()}> <time datetime={date.toISOString()}>
{ {
date.toLocaleDateString('en-us', { date.toLocaleDateString("en-us", {
year: 'numeric', year: "numeric",
month: 'short', month: "short",
day: 'numeric', day: "numeric",
}) })
} }
</time> </time>

View File

@@ -1,21 +1,21 @@
--- ---
import { SITE_TITLE } from '@ptypes/consts.ts'; import { SITE_TITLE } from "@ptypes/consts.ts";
import HeaderLink from '@components/HeaderLink.astro'; import HeaderLink from "@components/HeaderLink.astro";
import '@styles/indexHoverCss.css'; import "@styles/indexHoverCss.css";
import { getCollection } from 'astro:content'; import { getCollection } from "astro:content";
const personalPosts = (await getCollection('personal')).sort( const personalPosts = (await getCollection("personal")).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(), (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
); );
const uniPosts = (await getCollection('uni')).sort( const uniPosts = (await getCollection("uni")).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(), (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
); );
--- ---
<!DOCTYPE html>
<!doctype html>
<html> <html>
<head> <head>
<style> <style></style>
</style>
</head> </head>
<body> <body>
<nav> <nav>
@@ -25,22 +25,25 @@ const uniPosts = (await getCollection('uni')).sort(
{ {
personalPosts.map((post) => ( personalPosts.map((post) => (
<li> <li>
<HeaderLink href={`/blog/personal/${post.id}/`}>{post.data.title}</HeaderLink> <HeaderLink href={`/blog/personal/${post.id}/`}>
{post.data.title}
</HeaderLink>
</li> </li>
)) ))
} }
<hr> <hr />
<h4>University</h4> <h4>University</h4>
<h6>(Group Projects)</h6> <h6>(Group Projects)</h6>
{ {
uniPosts.map((post) => ( uniPosts.map((post) => (
<li> <li>
<HeaderLink href={`/blog/university/${post.id}/`}>{post.data.title}</HeaderLink> <HeaderLink href={`/blog/university/${post.id}/`}>
{post.data.title}
</HeaderLink>
</li> </li>
)) ))
} }
</ul> </ul>
</div>
</nav> </nav>
</body> </body>
</html> </html>

View File

@@ -1,18 +1,17 @@
--- ---
import type { HTMLAttributes } from 'astro/types'; import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<'a'>; type Props = HTMLAttributes<"a">;
const { href, class: className, ...props } = Astro.props; const { href, class: className, ...props } = Astro.props;
const pathname = Astro.url.pathname.replace(import.meta.env.BASE_URL, ''); const pathname = Astro.url.pathname.replace(import.meta.env.BASE_URL, "");
const subpath = pathname.match(/[^\/]+/g); const subpath = pathname.match(/[^\/]+/g);
const isActive = href === pathname || href === '/' + (subpath?.[0] || ''); const isActive = href === pathname || href === "/" + (subpath?.[0] || "");
--- ---
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head> </head>
</head>
<body> <body>
<a href={href} class:list={[className, { active: isActive }]} {...props}> <a href={href} class:list={[className, { active: isActive }]} {...props}>
<slot /> <slot />

View File

@@ -1,7 +1,7 @@
--- ---
import BaseHead from '@components/BaseHead.astro'; import BaseHead from "@components/BaseHead.astro";
import Header from '@components/Header.astro'; import Header from "@components/Header.astro";
import GithubIcon from '@assets/github.svg' import GithubIcon from "@assets/github.svg";
const { title, description, gitLink } = Astro.props; const { title, description, gitLink } = Astro.props;
--- ---
@@ -12,10 +12,11 @@ const { title, description, gitLink } = Astro.props;
<!-- To add meta data here later --> <!-- To add meta data here later -->
<style> <style>
body { body {
display:flex; display: flex;
flex-direction: row; flex-direction: row;
} }
main { display: flex; main {
display: flex;
flex-direction: row; flex-direction: row;
align-items: start; align-items: start;
justify-content: space-between; justify-content: space-between;
@@ -33,7 +34,6 @@ const { title, description, gitLink } = Astro.props;
padding: 1cm; padding: 1cm;
margin: 1cm; margin: 1cm;
} }
</style> </style>
</head> </head>
@@ -41,12 +41,18 @@ const { title, description, gitLink } = Astro.props;
<main> <main>
<Header /> <Header />
<div class="wrapper"> <div class="wrapper">
<a href={gitLink ?? 'https://github.com/juli0n21'} style="position: relative;"> <a
<GithubIcon style="position: absolute; right:0;" width="50px" height="50px" /> href={gitLink ?? "https://github.com/juli0n21"}
style="position: relative;"
>
<GithubIcon
style="position: absolute; right:0;"
width="50px"
height="50px"
/>
</a> </a>
<slot /> <slot />
</div> </div>
</main> </main>
</body> </body>
</html> </html>

View File

@@ -1,6 +1,6 @@
--- ---
import { getCollection, render } from 'astro:content'; import { getCollection, render } from "astro:content";
import BlogPost from '@layouts/BlogPost.astro'; import BlogPost from "@layouts/BlogPost.astro";
import type { import type {
InferGetStaticParamsType, InferGetStaticParamsType,
InferGetStaticPropsType, InferGetStaticPropsType,
@@ -8,8 +8,8 @@ import type {
} from "astro"; } from "astro";
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await getCollection('personal'); const posts = await getCollection("personal");
return posts.map(post => ({ return posts.map((post) => ({
params: { id: post.id }, params: { id: post.id },
props: { post }, props: { post },
})); }));

View File

@@ -1,6 +1,6 @@
--- ---
import { getCollection, render } from 'astro:content'; import { getCollection, render } from "astro:content";
import BlogPost from '@layouts/BlogPost.astro'; import BlogPost from "@layouts/BlogPost.astro";
import type { import type {
InferGetStaticParamsType, InferGetStaticParamsType,
InferGetStaticPropsType, InferGetStaticPropsType,
@@ -8,8 +8,8 @@ import type {
} from "astro"; } from "astro";
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await getCollection('uni'); const posts = await getCollection("uni");
return posts.map(post => ({ return posts.map((post) => ({
params: { id: post.id }, params: { id: post.id },
props: { post }, props: { post },
})); }));

View File

@@ -1,27 +1,36 @@
--- ---
import BaseHead from '@components/BaseHead.astro'; import BaseHead from "@components/BaseHead.astro";
import Header from '@components/Header.astro'; import Header from "@components/Header.astro";
import { SITE_DESCRIPTION, SITE_TITLE } from '@ptypes/consts.ts'; import { SITE_DESCRIPTION, SITE_TITLE } from "@ptypes/consts.ts";
let top: string[] = [];
let bottom: string[] = [];
const api: string =
"https://fshare.kami.boo/api/server/folder/cmm9jbqvt00025omwu5cnpw9i";
const response = await fetch(api);
let shuffled: string[] = [];
let images: string[] = [];
const api: string = 'https://fshare.kami.boo/api/server/folder/cmm9jbqvt00025omwu5cnpw9i';
const response = await fetch(api)
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
const files = data.files.map((f: Response) => `https://fshare.kami.boo/raw${f.url.substring(2)}`).sort(() => Math.random() - .5) as string[]; const files = data.files.map(
const middle = Math.round(files.length / 2); (f: Response) => `https://fshare.kami.boo/raw${f.url.substring(2)}`,
shuffled = files.slice(0,middle); ) as string[];
images = files.slice(middle);
} else {
const errorBody = await response.text();
console.error("FAILED TO FETCH", response.status, response.statusText, errorBody);
process.exit(1);
}
const middle = Math.round(files.length / 2);
top = files.slice(0, middle);
bottom = files.slice(middle);
} else {
const errorBody = await response.text();
console.error(
"FAILED TO FETCH",
response.status,
response.statusText,
errorBody,
);
process.exit(1);
}
--- ---
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} /> <BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
@@ -84,11 +93,11 @@ if (response.ok) {
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
transition: transform 0.3s ease; transition: transform 0.3s ease;
box-shadow: 0 10px 20px rgba(0,0,0,0.1); box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
} }
.tilted-container:hover { .tilted-container:hover {
transform: scale(1.10); transform: scale(1.1);
z-index: 1; z-index: 1;
} }
@@ -100,13 +109,21 @@ if (response.ok) {
} }
@keyframes infiniteScroll { @keyframes infiniteScroll {
0% { transform: translateX(0%); } 0% {
100% { transform: translateX(-50%); } transform: translateX(0%);
}
100% {
transform: translateX(-50%);
}
} }
@keyframes infiniteScrollReverse { @keyframes infiniteScrollReverse {
0% { transform: translateX(0%); } 0% {
100% { transform: translateX(50%) } transform: translateX(0%);
}
100% {
transform: translateX(50%);
}
} }
</style> </style>
</head> </head>
@@ -118,18 +135,19 @@ if (response.ok) {
<main class="scrolling-gallery"> <main class="scrolling-gallery">
<div class="gallery-wrapper"> <div class="gallery-wrapper">
<div class="scroll-track"> <div id="top-images" class="scroll-track">
{[...images, ...images].map((src) => ( {
[...top, ...top].map((src) => (
<a class="tilted-container" href={src} target="_blank"> <a class="tilted-container" href={src} target="_blank">
<img src={src} loading="lazy" /> <img src={src} loading="lazy" />
</a> </a>
))} ))
</div>
{
} }
<div class="scroll-track track-reverse"> </div>
{[...shuffled,...shuffled].map((src) => ( {}
<div id="bottom-images" class="scroll-track track-reverse">
{
[...bottom, ...bottom].map((src) => (
<a class="tilted-container" href={src} target="_blank"> <a class="tilted-container" href={src} target="_blank">
<img src={src} loading="lazy" /> <img src={src} loading="lazy" />
</a> </a>
@@ -139,5 +157,26 @@ if (response.ok) {
</div> </div>
</main> </main>
</div> </div>
<script>
function shuffleImages(selector: string) {
const track = document.querySelector(selector);
if (!track) return;
const items = Array.from(track.children);
const shuffled = items.sort(() => Math.random() - 0.5);
track.innerHTML = "";
const fragment = document.createDocumentFragment();
[...shuffled, ...shuffled].forEach((item) => {
fragment.appendChild(item.cloneNode(true));
});
track.appendChild(fragment);
}
shuffleImages("#top-images");
shuffleImages("#bottom-images");
</script>
</body> </body>
</html> </html>