Add Docker support and implement Bun for dependency management
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 1m25s
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:
parent
be5a61185b
commit
214982649f
9 changed files with 272 additions and 107 deletions
47
.dockerignore
Normal file
47
.dockerignore
Normal 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/
|
|
@ -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
|
||||||
|
|
40
Dockerfile
40
Dockerfile
|
@ -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"]
|
|
@ -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 "$@"
|
|
74
index.html
74
index.html
|
@ -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>
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -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}`
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue