Add Docker support and implement Bun for dependency management
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 1m25s

- Introduced .dockerignore to exclude unnecessary files from Docker context.
- Updated Dockerfile to use Bun for building and running the application.
- Removed entrypoint.sh as it is no longer needed.
- Modified build-and-dockerise.yml to set up Bun and install dependencies.
- Enhanced index.html with improved structure and hover effects.
- Updated client.ts to include tooltips for music display.
- Implemented caching in server.ts for Last.fm data.
- Added custom styles for hover effects and tooltips in style.css.
This commit is contained in:
Cameron Redmore 2025-08-08 11:18:39 +01:00
parent be5a61185b
commit 214982649f
No known key found for this signature in database
9 changed files with 272 additions and 107 deletions

47
.dockerignore Normal file
View file

@ -0,0 +1,47 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Development files
src/
index.html
vite.config.js
*.md
.git/
.gitignore
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Editor files
.vscode/
.idea/
*.swp
*.swo
# Logs
logs
*.log
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# CI/CD
.forgejo/
.github/
# Other
coverage/
.nyc_output/

View file

@ -26,12 +26,17 @@ jobs:
with: with:
fetch-depth: 0 # Recommended for metadata action fetch-depth: 0 # Recommended for metadata action
- name: Install pnpm dependencies - name: Setup Bun
run: pnpm i uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
shell: sh shell: sh
- name: Build project - name: Build project
run: pnpm run build run: bun run build
shell: sh shell: sh
- name: Set up Docker Buildx - name: Set up Docker Buildx

View file

@ -1,11 +1,37 @@
FROM nginx:latest # Build stage
FROM oven/bun:latest as builder
COPY dist /usr/share/nginx/html WORKDIR /app
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh # Copy package files
COPY package.json bun.lock* ./
ENTRYPOINT ["/entrypoint.sh"] # Install all dependencies (including devDependencies for building)
RUN bun install --frozen-lockfile
STOPSIGNAL SIGQUIT # Copy source code
CMD ["nginx", "-g", "daemon off;"] COPY . .
# Build the application
RUN bun run build
# Production stage
FROM oven/bun:latest
WORKDIR /app
# Copy package files
COPY package.json bun.lock* ./
# Install only production dependencies
RUN bun install --frozen-lockfile --production
# Copy built application and server
COPY --from=builder /app/dist ./dist
COPY src/server.ts ./src/server.ts
# Expose the port
EXPOSE 3000
# Start the application
CMD ["bun", "run", "prod"]

View file

@ -1,54 +0,0 @@
#!/bin/sh
# vim:sw=4:ts=4:et
set -e
entrypoint_log() {
if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then
echo "$@"
fi
}
if [ "$1" = "nginx" ] || [ "$1" = "nginx-debug" ]; then
if /usr/bin/find "/docker-entrypoint.d/" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then
entrypoint_log "$0: /docker-entrypoint.d/ is not empty, will attempt to perform configuration"
entrypoint_log "$0: Looking for shell scripts in /docker-entrypoint.d/"
find "/docker-entrypoint.d/" -follow -type f -print | sort -V | while read -r f; do
case "$f" in
*.envsh)
if [ -x "$f" ]; then
entrypoint_log "$0: Sourcing $f";
. "$f"
else
# warn on shell scripts without exec bit
entrypoint_log "$0: Ignoring $f, not executable";
fi
;;
*.sh)
if [ -x "$f" ]; then
entrypoint_log "$0: Launching $f";
"$f"
else
# warn on shell scripts without exec bit
entrypoint_log "$0: Ignoring $f, not executable";
fi
;;
*) entrypoint_log "$0: Ignoring $f";;
esac
done
entrypoint_log "$0: Configuration complete; ready for start up"
else
entrypoint_log "$0: No files found in /docker-entrypoint.d/, skipping configuration"
fi
fi
# Check if the folder /extra exists, if so, copy all files from /extra to /usr/share/nginx/html
if [ -d "/extra" ]; then
echo "Copying files from /extra to /usr/share/nginx/html"
cp -r /extra/* /usr/share/nginx/html/
fi
echo "Executing command: $@"
exec "$@"

View file

@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -7,13 +8,14 @@
<link rel="stylesheet" href="/src/style.css"> <link rel="stylesheet" href="/src/style.css">
</head> </head>
<body class="text-gray-200"> <body class="text-gray-200">
<script type="module"> <script type="module">
import '~icons/mdi/github' import '~icons/mdi/github'
import '~icons/mdi/git' import '~icons/mdi/git'
import '/src/client.ts' import '/src/client.ts'
</script> </script>
<!-- Decorative background shapes --> <!-- Decorative background shapes -->
<div class="shape shape-1"></div> <div class="shape shape-1"></div>
@ -29,19 +31,22 @@
<!-- Left Column: Profile Picture and Social Links --> <!-- Left Column: Profile Picture and Social Links -->
<div class="flex-shrink-0 text-center mb-6 md:mb-0"> <div class="flex-shrink-0 text-center mb-6 md:mb-0">
<img src="https://avatars.githubusercontent.com/u/5846077?v=4" alt="Cameron B. R. Redmore" class="rounded-full w-32 h-32 md:w-40 md:h-40 mx-auto border-4 border-white/20 shadow-lg"> <img src="https://avatars.githubusercontent.com/u/5846077?v=4" alt="Cameron B. R. Redmore"
class="rounded-full w-32 h-32 md:w-40 md:h-40 mx-auto border-4 border-white/20 shadow-lg hover-glass">
<div class="mt-6 flex justify-center space-x-4"> <div class="mt-6 flex justify-center space-x-4">
<a href="https://github.com/CameronRedmore" target="_blank" class="skill-tag p-3 rounded-full" aria-label="GitHub Profile"> <a href="https://github.com/CameronRedmore" target="_blank"
<icon-mdi-github class="w-8 h-8 text-2xl" /> class="hover-glass hover:text-cyan-200 p-2 rounded-full" aria-label="GitHub Profile">
<icon-mdi-github class="text-3xl" />
</a> </a>
<a href="https://git.cmzi.uk/" target="_blank" class="skill-tag p-3 rounded-full" aria-label="Personal Git Hosting"> <a href="https://git.cmzi.uk/" target="_blank"
<icon-mdi-git class="w-8 h-8 text-2xl" /> class="hover-glass hover:text-cyan-200 p-2 rounded-full" aria-label="Personal Git Hosting">
<icon-mdi-git class="text-3xl" />
</a> </a>
</div> </div>
<!-- Last.fm Now Playing Section --> <!-- Last.fm Now Playing Section -->
<div class="mt-6 md:mt-8 hidden md:block text-center"> <div class="mt-6 md:mt-8 hidden md:block text-center">
<div class="p-4 rounded-lg skill-tag"> <div class="p-4 rounded-lg hover-glass">
<div id="last-song-container"> <div id="last-song-container">
<div class="text-white-400 text-sm animate-pulse"> <div class="text-white-400 text-sm animate-pulse">
Loading music data... Loading music data...
@ -55,44 +60,56 @@
<div class="w-full text-center md:text-left"> <div class="w-full text-center md:text-left">
<h1 class="text-4xl md:text-5xl font-bold text-white animated-gradient">Cameron B. R. Redmore</h1> <h1 class="text-4xl md:text-5xl font-bold text-white animated-gradient">Cameron B. R. Redmore</h1>
<p class="mt-2 text-lg text-cyan-200">Software Developer from Kingston-Upon-Hull, UK</p> <p class="mt-2 text-lg text-cyan-200">Software Developer from Kingston-Upon-Hull, UK</p>
<p class="mt-6 text-gray-200 leading-relaxed"> <p class="mt-6 text-gray-200 leading-relaxed">
With 8 years of professional experience and over 15 years of passion-driven coding, I build robust and efficient software solutions. My journey has taken me from recreational projects to developing complex professional applications. With 8 years of professional experience and over 15 years of passion-driven coding, I build
robust and efficient software solutions. My journey has taken me from recreational projects to
developing complex professional applications.
</p> </p>
<!-- Interests & Hobbies Section --> <!-- Interests & Hobbies Section -->
<div class="mt-8"> <div class="mt-8">
<h2 class="text-xl font-semibold text-white mb-3">Interests & Hobbies</h2> <h2 class="text-xl font-semibold text-white mb-3">Interests & Hobbies</h2>
<p class="text-gray-200 leading-relaxed"> <p class="text-gray-200 leading-relaxed">
Beyond my core work, I have a deep interest in the broader field of computing and programming. I'm particularly passionate about the AI and Machine Learning space, focusing on integrating Large Language Models (LLMs) in novel ways. In my spare time, I also enjoy the logical challenge of puzzles like Sudoku and Nonograms. Beyond my core work, I have a deep interest in the broader field of computing and
programming. I'm particularly passionate about the AI and Machine Learning space, focusing
on integrating Large Language Models (LLMs) in novel ways. In my spare time, I also enjoy
the logical challenge of puzzles like Sudoku and Nonograms.
</p> </p>
</div> </div>
<div class="mt-8"> <div class="mt-8">
<h2 class="text-xl font-semibold text-white mb-3">Programming Skills</h2> <h2 class="text-xl font-semibold text-white mb-3">Programming Skills</h2>
<div class="flex flex-wrap gap-2 items-center justify-center"> <div class="flex flex-wrap gap-2 items-center justify-center">
<span class="skill-tag text-sm font-medium px-4 py-1 rounded-full">HTML</span> <span class="hover-glass text-sm font-medium px-4 py-1 rounded-full">HTML</span>
<span class="skill-tag text-sm font-medium px-4 py-1 rounded-full">CSS</span> <span class="hover-glass text-sm font-medium px-4 py-1 rounded-full">CSS</span>
<span class="skill-tag text-sm font-medium px-4 py-1 rounded-full">JavaScript</span> <span class="hover-glass text-sm font-medium px-4 py-1 rounded-full">JavaScript</span>
<span class="skill-tag text-sm font-medium px-4 py-1 rounded-full">TypeScript</span> <span class="hover-glass text-sm font-medium px-4 py-1 rounded-full">TypeScript</span>
<span class="skill-tag text-sm font-medium px-4 py-1 rounded-full">C#</span> <span class="hover-glass text-sm font-medium px-4 py-1 rounded-full">C#</span>
<span class="skill-tag text-sm font-medium px-4 py-1 rounded-full">C++</span> <span class="hover-glass text-sm font-medium px-4 py-1 rounded-full">C++</span>
<span class="skill-tag text-sm font-medium px-4 py-1 rounded-full">C</span> <span class="hover-glass text-sm font-medium px-4 py-1 rounded-full">C</span>
<span class="skill-tag text-sm font-medium px-4 py-1 rounded-full">Java</span> <span class="hover-glass text-sm font-medium px-4 py-1 rounded-full">Java</span>
</div> </div>
</div> </div>
<!-- Current Project Section --> <!-- Current Project Section -->
<div class="mt-8"> <div class="mt-8">
<h2 class="text-xl font-semibold text-white mb-3">Current Hobby Project</h2> <h2 class="text-xl font-semibold text-white mb-3">Current Hobby Project</h2>
<a href="https://weaver.cmzi.uk/" target="_blank" class="block p-4 rounded-lg skill-tag transition-all duration-300 ease-in-out hover:bg-white/30"> <a href="https://weaver.cmzi.uk/" target="_blank"
class="block p-4 rounded-lg hover-glass transition-all duration-300 ease-in-out hover:bg-white/30">
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="w-8 h-8 text-cyan-300" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path></svg> <svg class="w-8 h-8 text-cyan-300" fill="none" stroke="currentColor"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1">
</path>
</svg>
</div> </div>
<div> <div>
<p class="font-bold text-white">Word Weaver</p> <p class="font-bold text-white">Word Weaver</p>
<p class="text-sm text-cyan-200">An AI-powered web game of "Alchemy" with infinite, wacky combinations.</p> <p class="text-sm text-cyan-200">An AI-powered web game of "Alchemy" with infinite,
wacky combinations.</p>
</div> </div>
</div> </div>
</a> </a>
@ -101,7 +118,7 @@
<!-- Last.fm Now Playing Section - Mobile Only --> <!-- Last.fm Now Playing Section - Mobile Only -->
<div class="mt-8 md:hidden"> <div class="mt-8 md:hidden">
<h2 class="text-xl font-semibold text-white mb-3">Music</h2> <h2 class="text-xl font-semibold text-white mb-3">Music</h2>
<div class="p-4 rounded-lg skill-tag"> <div class="p-4 rounded-lg hover-glass">
<div id="last-song-container-mobile"> <div id="last-song-container-mobile">
<div class="text-white-400 text-sm animate-pulse"> <div class="text-white-400 text-sm animate-pulse">
Loading music data... Loading music data...
@ -114,4 +131,5 @@
</div> </div>
</main> </main>
</body> </body>
</html>
</html>

View file

@ -7,7 +7,8 @@
"start": "bun src/server.ts", "start": "bun src/server.ts",
"dev": "concurrently \"bun run --hot src/server.ts\" \"vite build --watch\"", "dev": "concurrently \"bun run --hot src/server.ts\" \"vite build --watch\"",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview",
"prod": "NODE_ENV=production bun src/server.ts"
}, },
"devDependencies": { "devDependencies": {
"@iconify/json": "^2.2.367", "@iconify/json": "^2.2.367",

View file

@ -92,9 +92,16 @@ function updateLastSongDisplay(song: LastSong | null, errorMessage?: string): vo
return; return;
} }
const statusText = song.nowPlaying ? 'Currently listening to' : 'Last listened to'; const statusText = song.nowPlaying ? 'Currently Listening To' : 'Last Played';
const imageUrl = getOptimalImageUrl(song.image); const imageUrl = getOptimalImageUrl(song.image);
// Create tooltip content for music
const tooltipContent = `
<div class="song-name">${song.name}</div>
<div class="artist-name">by ${song.artist}</div>
${song.album ? `<div class="album-name">from "${song.album}"</div>` : ''}
`;
const imageElement = imageUrl const imageElement = imageUrl
? `<img src="${imageUrl}" alt="${song.name}" class="w-24 h-24 rounded-lg object-cover mx-auto border border-white/20" loading="lazy">` ? `<img src="${imageUrl}" alt="${song.name}" class="w-24 h-24 rounded-lg object-cover mx-auto border border-white/20" loading="lazy">`
: `<div class="w-16 h-16 rounded-lg bg-gradient-to-br from-cyan-500/20 to-purple-500/20 flex items-center justify-center mx-auto border border-white/20"> : `<div class="w-16 h-16 rounded-lg bg-gradient-to-br from-cyan-500/20 to-purple-500/20 flex items-center justify-center mx-auto border border-white/20">
@ -105,14 +112,17 @@ function updateLastSongDisplay(song: LastSong | null, errorMessage?: string): vo
container.innerHTML = ` container.innerHTML = `
<div class="flex flex-col items-center space-y-3"> <div class="flex flex-col items-center space-y-3">
${imageElement} <div class="tooltip music-tooltip">
<div class="text-center w-full"> <a href="${song.url}" target="_blank" class="block hover:text-cyan-200 transition-colors duration-200 w-36">
<p class="text-sm text-cyan-300 font-medium">${statusText}</p> ${imageElement}
<a href="${song.url}" target="_blank" class="block hover:text-cyan-200 transition-colors duration-200"> <div class="text-center w-full">
<p class="font-semibold text-white">${song.name}</p> <p class="text-sm text-cyan-300 font-medium w-36">${statusText}</p>
<p class="text-sm text-gray-300">by ${song.artist}</p> <div class="font-semibold text-white-900 text-ellipsis overflow-hidden whitespace-nowrap max-w-full">${song.name}</div>
${song.album ? `<p class="text-xs text-white-400">from ${song.album}</p>` : ''} <div class="text-sm text-white-200 text-ellipsis overflow-hidden whitespace-nowrap max-w-full">${song.artist}</div>
${song.album ? `<div class="text-xs text-white-600 text-ellipsis overflow-hidden whitespace-nowrap max-w-full">${song.album}</div>` : ''}
</div>
</a> </a>
<span class="tooltip-text">${tooltipContent}</span>
</div> </div>
</div> </div>
`; `;

View file

@ -1,6 +1,35 @@
import { Elysia, t } from 'elysia'; import { Elysia, t } from 'elysia';
import { staticPlugin } from '@elysiajs/static'; import { staticPlugin } from '@elysiajs/static';
// Simple cache implementation
interface CacheEntry {
data: any;
timestamp: number;
}
const cache = new Map<string, CacheEntry>();
const CACHE_TTL = 30 * 1000; // 30 seconds in milliseconds
function getCachedData(key: string): any | null {
const entry = cache.get(key);
if (!entry) return null;
const now = Date.now();
if (now - entry.timestamp > CACHE_TTL) {
cache.delete(key);
return null;
}
return entry.data;
}
function setCachedData(key: string, data: any): void {
cache.set(key, {
data,
timestamp: Date.now()
});
}
const app = new Elysia() const app = new Elysia()
.use(staticPlugin({ .use(staticPlugin({
assets: 'dist', assets: 'dist',
@ -17,13 +46,22 @@ const app = new Elysia()
); );
} }
// Check cache first
const cacheKey = `lastfm:${username}`;
const cachedData = getCachedData(cacheKey);
if (cachedData) {
console.log('Returning cached Last.fm data');
return cachedData;
}
const url = `https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${username}&api_key=${apiKey}&format=json&limit=1`; const url = `https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${username}&api_key=${apiKey}&format=json&limit=1`;
try { try {
const response = await fetch(url); const response = await fetch(url);
const data = await response.json(); const data = await response.json();
const track = data.recenttracks.track[0]; const track = data.recenttracks.track[0];
return {
const result = {
name: track.name, name: track.name,
artist: track.artist['#text'], artist: track.artist['#text'],
album: track.album['#text'], album: track.album['#text'],
@ -31,6 +69,12 @@ const app = new Elysia()
nowPlaying: track['@attr']?.nowplaying === 'true', nowPlaying: track['@attr']?.nowplaying === 'true',
image: track.image || [], image: track.image || [],
}; };
// Cache the result
setCachedData(cacheKey, result);
console.log('Fetched and cached new Last.fm data');
return result;
} catch (error) { } catch (error) {
console.error('Error fetching from Last.fm:', error); console.error('Error fetching from Last.fm:', error);
return new Response(JSON.stringify({ error: 'Failed to fetch last song' }), { return new Response(JSON.stringify({ error: 'Failed to fetch last song' }), {
@ -39,7 +83,10 @@ const app = new Elysia()
}); });
} }
}) })
.listen(3000); .listen({
port: process.env.PORT || 3000,
hostname: process.env.NODE_ENV === 'production' ? '0.0.0.0' : 'localhost'
});
console.log( console.log(
`🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}` `🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`

View file

@ -78,14 +78,16 @@ body {
} }
} }
/* Custom styling for skill tags and buttons */ /* Custom styling for hoverable elements */
.skill-tag { .hover-glass {
background: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.15);
transition: background 0.3s ease, transform 0.3s ease; box-shadow: 0 2px 10px rgba(0, 0, 0, 0);
transition: background 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease, color 0.3s ease;
} }
.skill-tag:hover { .hover-glass:hover {
background: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.3);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
transform: translateY(-2px); transform: translateY(-2px);
} }
@ -109,4 +111,67 @@ body {
100% { 100% {
background-position: 0% 50%; background-position: 0% 50%;
} }
}
/* Tooltip styles */
.tooltip {
position: relative;
display: inline-block;
}
.tooltip .tooltip-text {
visibility: hidden;
opacity: 0;
width: max-content;
max-width: 200px;
background: rgba(43, 85, 131, 0.9);
color: #fff;
text-align: center;
border-radius: 8px;
padding: 8px 12px;
position: absolute;
z-index: 1000;
bottom: -25%;
left: 50%;
transform: translateX(-50%);
font-size: 14px;
font-weight: 500;
white-space: nowrap;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
transition: opacity 0.3s ease, visibility 0.3s ease, transform 0.3s ease;
}
.tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
transform: translateX(-50%) translateY(-5px);
}
/* Music tooltip specific styling */
.music-tooltip .tooltip-text {
white-space: normal;
text-align: left;
max-width: 250px;
line-height: 1.4;
}
.music-tooltip .tooltip-text .song-name {
font-weight: 600;
color: #4dd0e1;
margin-bottom: 2px;
}
.music-tooltip .tooltip-text .artist-name {
font-weight: 500;
color: #e2e8f0;
margin-bottom: 1px;
}
.music-tooltip .tooltip-text .album-name {
font-weight: 400;
color: #cbd5e0;
font-size: 12px;
} }