diff --git a/assets/js/code-copy.js b/assets/js/code-copy.js new file mode 100644 index 0000000..f55f123 --- /dev/null +++ b/assets/js/code-copy.js @@ -0,0 +1,70 @@ +// Add copy buttons to all code blocks +document.addEventListener("DOMContentLoaded", function () { + // Find all
 elements that contain 
+  const codeBlocks = document.querySelectorAll("pre code");
+
+  codeBlocks.forEach((codeBlock) => {
+    const pre = codeBlock.parentElement;
+
+    // Create wrapper for positioning
+    const wrapper = document.createElement("div");
+    wrapper.style.position = "relative";
+
+    // Wrap the pre element
+    pre.parentNode.insertBefore(wrapper, pre);
+    wrapper.appendChild(pre);
+
+    // Create copy button
+    const copyButton = document.createElement("button");
+    copyButton.className = "code-copy-btn";
+    copyButton.innerHTML = `
+      
+        
+        
+      
+    `;
+    copyButton.setAttribute("aria-label", "Copy code to clipboard");
+
+    // Add click handler
+    copyButton.addEventListener("click", async () => {
+      const code = codeBlock.textContent;
+
+      try {
+        await navigator.clipboard.writeText(code);
+
+        // Show success feedback
+        copyButton.classList.add("copied");
+
+        // Reset after 2 seconds
+        setTimeout(() => {
+          copyButton.classList.remove("copied");
+        }, 2000);
+      } catch (err) {
+        console.error("Failed to copy code:", err);
+
+        // Fallback for older browsers
+        const textArea = document.createElement("textarea");
+        textArea.value = code;
+        textArea.style.position = "fixed";
+        textArea.style.opacity = "0";
+        document.body.appendChild(textArea);
+        textArea.select();
+
+        try {
+          document.execCommand("copy");
+          copyButton.classList.add("copied");
+
+          setTimeout(() => {
+            copyButton.classList.remove("copied");
+          }, 2000);
+        } catch (err2) {
+          console.error("Fallback copy failed:", err2);
+        }
+
+        document.body.removeChild(textArea);
+      }
+    });
+
+    wrapper.appendChild(copyButton);
+  });
+});
diff --git a/assets/js/lastfm-stats.js b/assets/js/lastfm-stats.js
new file mode 100644
index 0000000..92b9192
--- /dev/null
+++ b/assets/js/lastfm-stats.js
@@ -0,0 +1,260 @@
+// Last.fm Stats Interactive Module
+(function () {
+  "use strict";
+
+  const LASTFM_API_URL = "https://ws.audioscrobbler.com/2.0/";
+  const LASTFM_API_KEY = "3a4fef48fecc593d25e0f9a40df1fefe";
+
+  // Store current stats for export
+  let currentStats = {
+    artists: [],
+    totalTracks: 0,
+    period: "",
+    username: "",
+  };
+
+  // Calculate timestamps based on period
+  function getTimestamps(period) {
+    const now = Math.floor(Date.now() / 1000);
+    let from;
+
+    if (period === "7day") {
+      from = now - 7 * 24 * 60 * 60; // 7 days
+    } else if (period === "1month") {
+      from = now - 30 * 24 * 60 * 60; // 30 days
+    }
+
+    return { from, to: now };
+  }
+
+  // Fetch top artists for the specified period
+  async function fetchTopArtists(username, period) {
+    const url = `${LASTFM_API_URL}?method=user.gettopartists&user=${username}&api_key=${LASTFM_API_KEY}&format=json&period=${period}&limit=5`;
+
+    const response = await fetch(url);
+    if (!response.ok) {
+      throw new Error(`Failed to fetch top artists: ${response.statusText}`);
+    }
+
+    const data = await response.json();
+
+    // Check for Last.fm API errors
+    if (data.error) {
+      throw new Error(data.message || "Last.fm API error");
+    }
+
+    return data.topartists?.artist || [];
+  }
+
+  // Fetch recent tracks to count total scrobbles in period
+  async function fetchTrackCount(username, period) {
+    const { from, to } = getTimestamps(period);
+    const url = `${LASTFM_API_URL}?method=user.getrecenttracks&user=${username}&api_key=${LASTFM_API_KEY}&format=json&from=${from}&to=${to}&limit=1`;
+
+    const response = await fetch(url);
+    if (!response.ok) {
+      throw new Error(`Failed to fetch track count: ${response.statusText}`);
+    }
+
+    const data = await response.json();
+
+    // Check for Last.fm API errors
+    if (data.error) {
+      throw new Error(data.message || "Last.fm API error");
+    }
+
+    return data.recenttracks?.["@attr"]?.total || 0;
+  }
+
+  // Generate markdown format
+  function generateMarkdown() {
+    const periodText =
+      currentStats.period === "7day" ? "Past Week" : "Past Month";
+    let markdown = `## Last.fm Stats - ${periodText}\n\n`;
+    markdown += `**Total Tracks:** ${currentStats.totalTracks}\n\n`;
+    markdown += `**Top 5 Artists:**\n\n`;
+
+    currentStats.artists.forEach((artist) => {
+      markdown += `- [${artist.name}](${artist.url}) - ${artist.playcount} plays\n`;
+    });
+
+    return markdown;
+  }
+
+  // Generate plain text format
+  function generatePlainText() {
+    const periodText =
+      currentStats.period === "7day" ? "Past Week" : "Past Month";
+    let text = `Last.fm Stats - ${periodText}\n\n`;
+    text += `Total Tracks: ${currentStats.totalTracks}\n\n`;
+    text += `Top 5 Artists:\n\n`;
+
+    currentStats.artists.forEach((artist) => {
+      text += `- ${artist.name} - ${artist.playcount} plays\n`;
+    });
+
+    return text;
+  }
+
+  // Copy to clipboard
+  async function copyToClipboard(text, button) {
+    try {
+      await navigator.clipboard.writeText(text);
+      const originalText = button.textContent;
+      button.textContent = "Copied!";
+      button.classList.add("copied");
+
+      setTimeout(() => {
+        button.textContent = originalText;
+        button.classList.remove("copied");
+      }, 2000);
+    } catch (err) {
+      console.error("Failed to copy:", err);
+      alert("Failed to copy to clipboard");
+    }
+  }
+
+  // Display the stats
+  function displayStats(artists, totalTracks, period, username) {
+    const artistsList = document.getElementById("top-artists");
+    const totalTracksEl = document.getElementById("total-tracks");
+
+    // Store current stats for export
+    currentStats = { artists, totalTracks, period, username };
+
+    // Update total tracks
+    totalTracksEl.textContent = totalTracks;
+
+    // Clear and populate artists list
+    artistsList.innerHTML = "";
+
+    if (artists.length === 0) {
+      artistsList.innerHTML = "
  • No artists found for this period
  • "; + return; + } + + artists.forEach((artist) => { + const li = document.createElement("li"); + li.innerHTML = `${artist.name} - ${artist.playcount} plays`; + artistsList.appendChild(li); + }); + + // Show export buttons + const exportButtons = document.getElementById("export-buttons"); + if (exportButtons) { + exportButtons.style.display = "flex"; + } + } + + // Show/hide UI elements + function setLoadingState(isLoading) { + const loading = document.getElementById("stats-loading"); + const content = document.getElementById("stats-content"); + const error = document.getElementById("stats-error"); + const results = document.getElementById("stats-results"); + + results.style.display = "block"; + + if (isLoading) { + loading.style.display = "block"; + content.style.display = "none"; + error.style.display = "none"; + } else { + loading.style.display = "none"; + } + } + + function showError(message) { + const error = document.getElementById("stats-error"); + const errorMessage = document.getElementById("error-message"); + const content = document.getElementById("stats-content"); + + error.style.display = "block"; + content.style.display = "none"; + errorMessage.textContent = message; + } + + function showContent() { + const error = document.getElementById("stats-error"); + const content = document.getElementById("stats-content"); + + error.style.display = "none"; + content.style.display = "block"; + } + + // Main fetch function + async function fetchStats() { + const username = document.getElementById("lastfm-username").value.trim(); + const period = document.getElementById("time-period").value; + + if (!username) { + showError("Please enter a Last.fm username"); + return; + } + + setLoadingState(true); + + try { + // Fetch both stats in parallel + const [artists, totalTracks] = await Promise.all([ + fetchTopArtists(username, period), + fetchTrackCount(username, period), + ]); + + displayStats(artists, totalTracks, period, username); + showContent(); + } catch (error) { + console.error("Error fetching Last.fm stats:", error); + showError( + error.message || + "Failed to fetch stats. Please check the username and try again.", + ); + } finally { + setLoadingState(false); + } + } + + // Initialize when DOM is ready + function init() { + const fetchButton = document.getElementById("fetch-stats"); + const usernameInput = document.getElementById("lastfm-username"); + const copyMarkdownBtn = document.getElementById("copy-markdown"); + const copyPlainTextBtn = document.getElementById("copy-plaintext"); + + if (!fetchButton || !usernameInput) { + return; // Not on the stats page + } + + // Fetch stats on button click + fetchButton.addEventListener("click", fetchStats); + + // Also fetch on Enter key in username input + usernameInput.addEventListener("keypress", (e) => { + if (e.key === "Enter") { + fetchStats(); + } + }); + + // Copy buttons + if (copyMarkdownBtn) { + copyMarkdownBtn.addEventListener("click", () => { + const markdown = generateMarkdown(); + copyToClipboard(markdown, copyMarkdownBtn); + }); + } + + if (copyPlainTextBtn) { + copyPlainTextBtn.addEventListener("click", () => { + const plainText = generatePlainText(); + copyToClipboard(plainText, copyPlainTextBtn); + }); + } + } + + // Run init when DOM is loaded + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})(); diff --git a/assets/js/pgp-copy.js b/assets/js/pgp-copy.js new file mode 100644 index 0000000..843662f --- /dev/null +++ b/assets/js/pgp-copy.js @@ -0,0 +1,33 @@ +// PGP Key copy functionality +document.addEventListener('DOMContentLoaded', function() { + const copyButtons = document.querySelectorAll('.pgp-copy-trigger'); + + copyButtons.forEach(button => { + button.addEventListener('click', async function() { + const feedback = button.closest('.contact-pgp').querySelector('.pgp-copy-feedback'); + + try { + const response = await fetch('/publickey.asc'); + const pgpKey = await response.text(); + + await navigator.clipboard.writeText(pgpKey); + + feedback.textContent = 'PGP key copied to clipboard!'; + feedback.classList.add('show', 'success'); + + setTimeout(() => { + feedback.classList.remove('show'); + }, 3000); + } catch (err) { + console.error('Failed to copy PGP key:', err); + + feedback.textContent = 'Failed to copy key'; + feedback.classList.add('show', 'error'); + + setTimeout(() => { + feedback.classList.remove('show'); + }, 3000); + } + }); + }); +}); diff --git a/assets/sass/pages/blog.scss b/assets/sass/pages/blog.scss index 4ed1a18..0d2d9a2 100644 --- a/assets/sass/pages/blog.scss +++ b/assets/sass/pages/blog.scss @@ -559,7 +559,7 @@ } // Contact section styling - .blog-contact-section { + .contact-section { margin-top: 40px; padding: 20px; background: rgba(0, 255, 0, 0.05); diff --git a/assets/sass/pages/resources.scss b/assets/sass/pages/resources.scss index d615573..06ec85d 100644 --- a/assets/sass/pages/resources.scss +++ b/assets/sass/pages/resources.scss @@ -1,8 +1,80 @@ -// Resources Whiteboard Page -.resources-page { +.test-chamber-grid { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + + background-image: + linear-gradient(rgba(58, 58, 58, 0.6) 2px, transparent 2px), + linear-gradient(90deg, rgba(58, 58, 58, 0.6) 2px, transparent 2px), + radial-gradient( + ellipse 80% 20% at 50% 100%, + rgba(200, 220, 255, 0.25) 0%, + transparent 50% + ), + linear-gradient(180deg, #6a6d75 0%, #7a7d85 50%, #3f4146 100%); + background-size: + 150px 200px, + 150px 200px, + 100% 100%, + 100% 100%; + background-position: + 0 0, + 0 0, + 0 0, + 0 0; + + &::before { + content: "TEST 06"; + position: absolute; + top: 55%; + right: 8%; + transform: translateY(-50%); + font-family: "Barlow Condensed", sans-serif; + font-size: 6rem; + font-weight: 700; + color: rgba(255, 255, 255, 0.3); + letter-spacing: 0.2em; + text-transform: uppercase; + pointer-events: none; + z-index: 1; + writing-mode: vertical-rl; + text-orientation: mixed; + + @include media-up(md) { + font-size: 7rem; + } + } + + &::after { + position: absolute; + z-index: 12; + top: -180px; + left: -200px; + width: 600px; + height: 600px; + background: radial-gradient( + ellipse 60% 70% at 40% 45%, + rgba(0, 0, 0, 0.65) 0%, + rgba(0, 0, 0, 0.4) 30%, + rgba(0, 0, 0, 0.3) 50%, + rgba(0, 0, 0, 0.15) 70%, + transparent 100% + ); + content: " "; + filter: blur(8px); + } +} + +.resources-page, +.resouce-single { min-height: 100vh; padding: 2rem 1rem; +} +// Resources Whiteboard Page +.resources-page { @include media-up(md) { padding: 3rem 2rem; } @@ -11,46 +83,342 @@ .resources-container { max-width: 1400px; margin: 0 auto; + position: relative; + z-index: 1; } -.whiteboard { - background: linear-gradient(135deg, #f5f5f0 0%, #e8e8dd 100%); - border-radius: 8px; - - box-shadow: - 0 10px 40px rgba(0, 0, 0, 0.1), - inset 0 0 100px rgba(0, 0, 0, 0.02); +// Portal Header with portals on either side +.portal-header { + display: flex; + align-items: center; + justify-content: center; + gap: 3rem; + margin-bottom: 3rem; position: relative; - // Subtle texture overlay - &::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-image: - repeating-linear-gradient( - 0deg, - transparent, - transparent 2px, - rgba(0, 0, 0, 0.01) 2px, - rgba(0, 0, 0, 0.01) 4px - ), - repeating-linear-gradient( - 90deg, - transparent, - transparent 2px, - rgba(0, 0, 0, 0.01) 2px, - rgba(0, 0, 0, 0.01) 4px + .portal-title { + font-size: 3.5rem; + font-family: Verdana, "Trebuchet MS", Tahoma, sans-serif; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.15em; + color: #ffffff; + text-shadow: + 0 0 20px rgba(255, 255, 255, 0.8), + 0 0 40px rgba(255, 255, 255, 0.4), + 0 2px 4px rgba(0, 0, 0, 0.3); + margin: 0; + position: relative; + z-index: 2; + + @include media-up(md) { + font-size: 4rem; + } + } + + .portal-icon { + width: 100px; + height: 160px; + position: relative; + overflow: visible; + z-index: 1; + + // Portal ring (the colored edge) + &::before { + content: ""; + position: absolute; + inset: 0; + border-radius: 50%; + border: 8px solid; + z-index: -1; + animation: portal-pulse 2s ease-in-out infinite; + } + + // Black center void + &::after { + content: ""; + position: absolute; + inset: 16px; + border-radius: 50%; + background: radial-gradient( + ellipse at center, + rgba(0, 0, 0, 0.95) 0%, + rgba(0, 0, 0, 0.85) 60%, + rgba(0, 0, 0, 0.6) 100% ); - pointer-events: none; - border-radius: 8px; + z-index: 1; + } + + &.blue-portal { + left: 100px; + + &::before { + border-color: #0096ff; + box-shadow: + 0 0 30px 8px rgba(0, 150, 255, 0.8), + 0 0 60px 12px rgba(0, 150, 255, 0.5), + inset 0 0 30px rgba(0, 150, 255, 0.6); + } + + &::after { + box-shadow: + inset 0 0 40px rgba(0, 150, 255, 0.3), + inset 0 0 20px rgba(0, 150, 255, 0.5); + } + } + + &.orange-portal { + left: -100px; + &::before { + border-color: #ff7800; + box-shadow: + 0 0 30px 8px rgba(255, 120, 0, 0.8), + 0 0 60px 12px rgba(255, 120, 0, 0.5), + inset 0 0 30px rgba(255, 120, 0, 0.6); + } + + &::after { + box-shadow: + inset 0 0 40px rgba(255, 120, 0, 0.3), + inset 0 0 20px rgba(255, 120, 0, 0.5); + } + } } } -.whiteboard-header { +// Companion Cube +.companion-cube { + position: fixed; + bottom: 40px; + right: 40px; + width: 80px; + height: 80px; + background: linear-gradient( + 135deg, + rgba(200, 200, 200, 0.2) 0%, + rgba(150, 150, 150, 0.3) 100% + ); + border: 2px solid rgba(200, 200, 200, 0.4); + border-radius: 8px; + box-shadow: + 0 10px 30px rgba(0, 0, 0, 0.5), + inset 0 0 20px rgba(255, 255, 255, 0.1); + transform: rotateX(15deg) rotateY(-15deg); + transform-style: preserve-3d; + animation: cube-float 4s ease-in-out infinite; + position: relative; + + // Heart symbol in center + &::before { + content: "♥"; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 32px; + color: rgba(255, 100, 150, 0.6); + text-shadow: 0 0 10px rgba(255, 100, 150, 0.8); + } + + // Corner circles + &::after { + content: ""; + position: absolute; + width: 12px; + height: 12px; + border-radius: 50%; + background: rgba(200, 200, 200, 0.4); + top: 8px; + left: 8px; + box-shadow: + 60px 0 0 rgba(200, 200, 200, 0.4), + 0 60px 0 rgba(200, 200, 200, 0.4), + 60px 60px 0 rgba(200, 200, 200, 0.4); + } + + @include media-up(md) { + width: 100px; + height: 100px; + + &::before { + font-size: 40px; + } + } +} + +@keyframes portal-pulse { + 0%, + 100% { + transform: scaleX(0.75) scaleY(0.85); + opacity: 1; + } + 50% { + transform: scaleX(0.8) scaleY(0.9); + opacity: 0.9; + } +} + +@keyframes portal-ring-pulse { + 0%, + 100% { + transform: scale(1); + opacity: 0.3; + } + 50% { + transform: scale(1.2); + opacity: 0.1; + } +} + +@keyframes cube-float { + 0%, + 100% { + transform: rotateX(15deg) rotateY(-15deg) translateY(0); + } + 50% { + transform: rotateX(15deg) rotateY(-15deg) translateY(-10px); + } +} + +@keyframes portal-spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +@keyframes portal-vortex { + 0% { + transform: rotate(0deg) scale(1); + } + 50% { + transform: rotate(180deg) scale(1.1); + } + 100% { + transform: rotate(360deg) scale(1); + } +} + +.portal-sign { + position: relative; + padding: 3rem 2rem; + + border: 10px solid black; + background: white; + + max-width: 900px; + margin: auto; + + box-shadow: + 0 8px 24px rgba(0, 0, 0, 0.6), + 0 0 40px rgba(0, 0, 0, 0.6); + + @include media-up(md) { + padding: 4rem 3rem; + } + + .portal-sign-decor { + border-top: 10px solid black; + border-bottom: 10px solid black; + margin-left: 6rem; + position: relative; + + @include media-down(lg) { + margin-left: auto; + } + + .portal-sign-text { + p { + margin-bottom: 1rem; + } + + font-family: "caveat", cursive; + font-size: 1.5rem; + position: absolute; + text-align: right; + right: 0; + top: 50px; + width: 50%; + transform: rotate(10deg); + + @include media-down(md) { + position: relative; + transform: none; + top: auto; + left: auto; + width: 100%; + text-align: left; + margin-top: 1em; + } + } + + .portal-sign-number { + position: relative; + font-family: "Barlow Condensed", sans-serif; + font-size: 30rem; + + @include media-down(md) { + display: none; + } + } + + .portal-sign-sub { + font-family: "Barlow Condensed", sans-serif; + font-size: 3rem; + font-weight: bold; + + @include media-down(md) { + display: none; + } + } + } + + .portal-sign-lines { + margin-left: 6rem; + margin-top: 1rem; + padding-bottom: 5rem; + display: block; + position: relative; + border-bottom: 10px solid black; + + @include media-down(md) { + display: none; + } + + @include media-down(lg) { + margin-left: auto; + } + + &::before { + content: ""; + height: 50px; + width: 10px; + background-color: black; + display: block; + + /* repeat the line 5 more times */ + box-shadow: + 20px 0 0 black, + 40px 0 0 black, + 60px 0 0 black, + 80px 0 0 black, + 100px 0 0 black; + } + } + + .portal-sign-content { + margin-left: 6rem; + margin-top: 1rem; + + @include media-down(lg) { + margin-left: auto; + } + } +} + +.portal-header { text-align: center; margin-bottom: 4rem; position: relative; @@ -58,129 +426,63 @@ h1 { font-size: 3rem; - color: #2c3e50; + color: #0096ff; margin-bottom: 1rem; font-weight: 700; - } - - .whiteboard-description { - font-family: "Caveat", cursive; - font-size: 28px; - font-weight: bold; - color: #2c3e50; - max-width: 600px; - margin: 0 auto; + text-shadow: 0 0 20px rgba(0, 150, 255, 0.6); } } -.whiteboard-pins { +.portal-sign-items { display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 3rem; - position: relative; - z-index: 1; - - @include media-up(lg) { - grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); - } + grid-template-columns: 1fr 1fr 1fr; + gap: 1em; } -// Resource Pin (Post-it note style) -.resource-pin { +.resource-pin a { + color: #000; +} + +.resource-card { + padding: 2rem 1.5rem 1.5rem; + aspect-ratio: 1/1; + display: flex; + flex-direction: column; + gap: 1rem; + border: 3px solid; position: relative; - transform: rotate(0deg); - transition: - transform 0.3s ease, - box-shadow 0.3s ease; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; - // Random slight rotations for pins - &:nth-child(3n + 1) { - transform: rotate(-1deg); - } + .resource-info { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; - &:nth-child(3n + 2) { - transform: rotate(1deg); - } + background: rgba(255, 255, 255, 0.6); + font-size: 2rem; + text-align: center; + font-family: "caveat", cursive; - &:nth-child(3n + 3) { - transform: rotate(-0.5deg); - } + opacity: 0; - &:hover { - transform: rotate(0deg) translateY(-5px) !important; - box-shadow: 0 15px 30px rgba(0, 0, 0, 0.2); - z-index: 10; - - .pin-tack { - transform: translateY(-2px); + &:hover { + opacity: 1; } } } -// Pin tack at the top -.pin-tack { - position: absolute; - top: -15px; - left: 50%; - transform: translateX(-50%); - width: 20px; - height: 20px; - background: radial-gradient(circle at 30% 30%, #ff6b6b, #c92a2a); - border-radius: 50%; - box-shadow: - 0 2px 5px rgba(0, 0, 0, 0.3), - inset -2px -2px 4px rgba(0, 0, 0, 0.2); - z-index: 2; - transition: transform 0.3s ease; - - &::after { - content: ""; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 6px; - height: 6px; - background: #fff; - border-radius: 50%; - opacity: 0.5; - } -} - -// Resource card (the note itself) -.resource-card { - background: linear-gradient(135deg, #fef9c3 0%, #fde68a 100%); - padding: 2rem 1.5rem 1.5rem; - border-radius: 4px; - box-shadow: - 0 4px 8px rgba(0, 0, 0, 0.1), - inset 0 -40px 40px -20px rgba(0, 0, 0, 0.05); - min-height: 280px; - display: flex; - flex-direction: column; - gap: 1rem; - - // Vary note colors - .resource-pin:nth-child(4n + 1) & { - background: linear-gradient(135deg, #fef9c3 0%, #fde68a 100%); // Yellow - } - - .resource-pin:nth-child(4n + 2) & { - background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%); // Blue - } - - .resource-pin:nth-child(4n + 3) & { - background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%); // Green - } - - .resource-pin:nth-child(4n + 4) & { - background: linear-gradient(135deg, #fce7f3 0%, #fbcfe8 100%); // Pink - } -} - .resource-icon { - width: 80px; - height: 80px; + width: 100px; + height: 100px; margin: 0 auto 1rem; position: relative; @@ -190,13 +492,12 @@ 180deg, transparent 0%, transparent 20%, - #9333ea 20%, - #7c3aed 100% + #000 20%, + #000 100% ); border-radius: 10px 10px 40px 40px; - border: 3px solid #6b21a8; + border: 3px solid #000; position: relative; - box-shadow: inset 0 0 20px rgba(147, 51, 234, 0.3); // Lamp base &::before { @@ -205,7 +506,7 @@ bottom: -8px; left: 50%; transform: translateX(-50%); - width: 90%; + width: 70%; height: 12px; background: #1f2937; border-radius: 4px; @@ -219,16 +520,15 @@ left: 30%; width: 25px; height: 30px; - background: radial-gradient(circle, #ec4899 0%, #db2777 100%); + background: #fff; border-radius: 50% 50% 40% 60%; - opacity: 0.9; animation: float-blob 4s ease-in-out infinite; } } // Last.fm stats icon &.lastfm-stats { - background: linear-gradient(135deg, #d32323 0%, #b71c1c 100%); + background: black; border-radius: 8px; display: flex; align-items: center; @@ -261,44 +561,6 @@ } } -// Additional lava blob for lavalamp -.resource-icon.lavalamp { - & > span { - position: absolute; - top: 50%; - right: 25%; - width: 20px; - height: 25px; - background: radial-gradient(circle, #f97316 0%, #ea580c 100%); - border-radius: 60% 40% 50% 50%; - opacity: 0.85; - animation: float-blob-2 3.5s ease-in-out infinite; - } - - // Create the blob with a pseudo-element instead - &:not(:empty)::before { - // Override for when we add content - animation: float-blob 4s ease-in-out infinite; - } -} - -// Last.fm additional bars -.resource-icon.lastfm-stats { - & > span { - position: absolute; - bottom: 15px; - width: 8px; - background: #fff; - border-radius: 2px; - - &:nth-child(1) { - left: 50px; - height: 30px; - animation: equalizer-3 0.8s ease-in-out infinite 0.4s; - } - } -} - @keyframes float-blob { 0%, 100% { @@ -309,16 +571,6 @@ } } -@keyframes float-blob-2 { - 0%, - 100% { - transform: translateY(0) scale(1); - } - 50% { - transform: translateY(-20px) scale(0.9); - } -} - @keyframes equalizer-1 { 0%, 100% { @@ -339,112 +591,9 @@ } } -@keyframes equalizer-3 { - 0%, - 100% { - height: 30px; - } - 50% { - height: 15px; - } -} - -.resource-info { - flex: 1; - display: flex; - flex-direction: column; - - h2 { - font-size: 1.5rem; - margin: 0 0 0.5rem 0; - color: #1f2937; - font-weight: 600; - - a { - color: inherit; - text-decoration: none; - transition: color 0.2s ease; - - &:hover { - color: #3b82f6; - } - } - } -} - -.resource-description { - color: #4b5563; - font-size: 0.95rem; - line-height: 1.5; - margin: 0 0 1rem 0; - flex: 1; -} - -.resource-tags { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - margin-bottom: 1rem; - - .tag { - background: rgba(0, 0, 0, 0.1); - padding: 0.25rem 0.75rem; - border-radius: 12px; - font-size: 0.8rem; - color: #374151; - text-decoration: none; - transition: background 0.2s ease; - - &:hover { - background: rgba(0, 0, 0, 0.2); - } - } -} - -.resource-links { - display: flex; - gap: 0.75rem; - margin-top: auto; - - .resource-link { - flex: 1; - padding: 0.5rem 1rem; - text-align: center; - text-decoration: none; - border-radius: 4px; - font-weight: 500; - font-size: 0.9rem; - transition: all 0.2s ease; - - &.demo { - background: #3b82f6; - color: #fff; - - &:hover { - background: #2563eb; - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(59, 130, 246, 0.4); - } - } - - &.source { - background: #1f2937; - color: #fff; - - &:hover { - background: #111827; - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(31, 41, 55, 0.4); - } - } - } -} - // Single Resource Page .resource-single { - max-width: 900px; margin: 0 auto; - padding: 2rem 1rem; @include media-up(md) { padding: 3rem 2rem; @@ -452,10 +601,113 @@ } .resource-content { - background: #fff; + max-width: 900px; + margin: auto; + background: + linear-gradient( + 145deg, + rgba(10, 20, 40, 0.95) 0%, + rgba(15, 25, 45, 0.98) 100% + ) + padding-box, + linear-gradient( + 135deg, + rgba(0, 150, 255, 0.5), + rgba(255, 120, 0, 0.3), + rgba(0, 150, 255, 0.5) + ) + border-box; border-radius: 8px; + border: 2px solid transparent; padding: 3rem 2rem; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + box-shadow: + 0 0 40px rgba(0, 150, 255, 0.2), + 0 0 80px rgba(255, 120, 0, 0.1), + inset 0 0 100px rgba(0, 100, 200, 0.05), + 0 8px 32px rgba(0, 0, 0, 0.4); + position: relative; + overflow: hidden; + + // Subtle tech panel grid pattern + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: + linear-gradient(rgba(0, 150, 255, 0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 150, 255, 0.03) 1px, transparent 1px); + background-size: 50px 50px; + pointer-events: none; + opacity: 0.3; + border-radius: 8px; + } + + // Corner accents (Aperture Science style) + &::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + radial-gradient( + circle at top left, + rgba(0, 150, 255, 0.15) 0%, + transparent 30% + ), + radial-gradient( + circle at bottom right, + rgba(255, 120, 0, 0.15) 0%, + transparent 30% + ); + pointer-events: none; + } + + // Animated energy particles on border + .energy-particles { + position: absolute; + inset: -2px; + border-radius: 8px; + pointer-events: none; + z-index: 1; + overflow: hidden; + + &::before, + &::after { + content: ""; + position: absolute; + width: 20px; + height: 20px; + border-radius: 50%; + filter: blur(4px); + } + + // Blue particle + &::before { + background: radial-gradient( + circle, + rgba(0, 150, 255, 0.8) 0%, + transparent 70% + ); + box-shadow: 0 0 15px rgba(0, 150, 255, 0.8); + animation: particle-travel-blue 8s linear infinite; + } + + // Orange particle + &::after { + background: radial-gradient( + circle, + rgba(255, 120, 0, 0.8) 0%, + transparent 70% + ); + box-shadow: 0 0 15px rgba(255, 120, 0, 0.8); + animation: particle-travel-orange 8s linear infinite 4s; + } + } @include media-up(md) { padding: 4rem 3rem; @@ -466,70 +718,294 @@ text-align: center; margin-bottom: 3rem; padding-bottom: 2rem; - border-bottom: 2px solid #e5e7eb; + border-bottom: 2px solid; + border-image: linear-gradient( + 90deg, + transparent, + rgba(0, 150, 255, 0.6), + rgba(255, 120, 0, 0.6), + transparent + ) + 1; + position: relative; + z-index: 1; h1 { font-size: 2.5rem; - color: #1f2937; + color: rgba(240, 245, 250, 0.95); margin: 1rem 0; + font-family: Verdana, "Trebuchet MS", Tahoma, sans-serif; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.15em; + text-shadow: 0 0 10px rgba(255, 255, 255, 0.3); + position: relative; + display: inline-block; + + // Blue line on the left + &::before { + content: ""; + position: absolute; + left: -4rem; + top: 50%; + transform: translateY(-50%); + width: 3rem; + height: 3px; + background: linear-gradient(90deg, transparent, #0096ff); + box-shadow: 0 0 10px rgba(0, 150, 255, 0.8); + } + + // Orange line on the right + &::after { + content: ""; + position: absolute; + right: -4rem; + top: 50%; + transform: translateY(-50%); + width: 3rem; + height: 3px; + background: linear-gradient(90deg, #ff7800, transparent); + box-shadow: 0 0 10px rgba(255, 120, 0, 0.8); + } @include media-up(md) { font-size: 3rem; + + &::before, + &::after { + width: 5rem; + } + + &::before { + left: -6rem; + } + + &::after { + right: -6rem; + } } } .lead { font-size: 1.25rem; - color: #6b7280; + color: rgba(150, 200, 255, 0.9); margin: 1rem 0; - } -} - -.resource-icon-large { - width: 120px; - height: 120px; - margin: 0 auto; - - &.lavalamp, - &.lastfm-stats { - // Inherit styles from smaller icons + font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; + font-weight: 300; + text-shadow: 0 0 10px rgba(0, 150, 255, 0.3); } } .resource-body { - color: #374151; + color: rgba(200, 220, 255, 0.95); line-height: 1.8; font-size: 1.1rem; + font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; + position: relative; + z-index: 1; h2 { - color: #1f2937; + color: #0096ff; margin-top: 2rem; margin-bottom: 1rem; + text-transform: uppercase; + letter-spacing: 0.08em; + font-weight: 600; + text-shadow: 0 0 15px rgba(0, 150, 255, 0.5); + border-bottom: 2px solid; + border-image: linear-gradient( + 90deg, + transparent, + rgba(0, 150, 255, 0.5), + transparent + ) + 1; + padding-bottom: 0.5rem; + padding-left: 1rem; + position: relative; + + &::before { + content: ""; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 4px; + height: 70%; + background: linear-gradient( + 180deg, + rgba(0, 150, 255, 0.8), + rgba(255, 120, 0, 0.6) + ); + box-shadow: 0 0 10px rgba(0, 150, 255, 0.6); + } + } + + h3 { + color: rgba(255, 140, 50, 0.9); + margin-top: 1.5rem; + margin-bottom: 0.75rem; + text-transform: uppercase; + font-size: 1rem; + font-weight: 600; + letter-spacing: 0.06em; + text-shadow: 0 0 10px rgba(255, 120, 0, 0.4); } p { margin-bottom: 1rem; } + a { + color: #0096ff; + text-decoration: none; + border-bottom: 1px solid rgba(0, 150, 255, 0.3); + transition: all 0.3s ease; + position: relative; + + &:hover { + color: #ff7800; + border-bottom-color: rgba(255, 120, 0, 0.6); + text-shadow: 0 0 8px rgba(0, 150, 255, 0.5); + } + } + + ul, + ol { + margin-bottom: 1rem; + padding-left: 2rem; + + li { + margin-bottom: 0.5rem; + position: relative; + + &::marker { + color: rgba(0, 150, 255, 0.6); + } + } + } + + strong { + color: #fff; + font-weight: 600; + text-shadow: 0 0 5px rgba(0, 150, 255, 0.3); + } + code { - background: #f3f4f6; - padding: 0.2rem 0.4rem; + background: rgba(0, 100, 180, 0.15); + padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.9em; + color: #66ccff; + border: 1px solid rgba(0, 150, 255, 0.3); + font-family: "Consolas", "Monaco", monospace; + box-shadow: 0 0 10px rgba(0, 150, 255, 0.1); } pre { - background: #1f2937; - color: #e5e7eb; + background: linear-gradient( + 145deg, + rgba(5, 15, 30, 0.9) 0%, + rgba(10, 20, 40, 0.95) 100% + ); + color: #66ccff; padding: 1.5rem; - border-radius: 8px; + border-radius: 6px; overflow-x: auto; margin: 1.5rem 0; + border: 1px solid rgba(0, 150, 255, 0.3); + box-shadow: + inset 0 0 20px rgba(0, 100, 200, 0.1), + 0 4px 20px rgba(0, 0, 0, 0.3); + position: relative; + + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + background: linear-gradient( + 180deg, + rgba(0, 150, 255, 0.6), + rgba(255, 120, 0, 0.4) + ); + } code { background: none; padding: 0; color: inherit; + border: none; + box-shadow: none; + } + } + + // Code copy button + .code-copy-btn { + position: absolute; + top: 0.75rem; + right: 0.75rem; + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + background: linear-gradient( + 135deg, + rgba(0, 100, 180, 0.3) 0%, + rgba(0, 80, 150, 0.2) 100% + ); + color: #66ccff; + border: 1px solid rgba(0, 150, 255, 0.4); + border-radius: 4px; + font-size: 0.85rem; + font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + text-transform: uppercase; + letter-spacing: 0.05em; + z-index: 10; + + svg { + width: 14px; + height: 14px; + transition: transform 0.2s ease; + } + + &:hover { + background: linear-gradient( + 135deg, + rgba(0, 120, 200, 0.4) 0%, + rgba(255, 100, 0, 0.25) 100% + ); + border-color: rgba(0, 150, 255, 0.7); + box-shadow: 0 0 15px rgba(0, 150, 255, 0.3); + color: #fff; + transform: translateY(-1px); + + svg { + transform: scale(1.1); + } + } + + &:active { + transform: translateY(0); + } + + &.copied { + background: linear-gradient( + 135deg, + rgba(0, 200, 150, 0.4) 0%, + rgba(0, 150, 100, 0.3) 100% + ); + border-color: rgba(0, 255, 180, 0.6); + color: #66ffcc; + + svg { + transform: scale(0.9); + } } } } @@ -540,30 +1016,89 @@ align-items: center; margin-top: 3rem; padding-top: 2rem; - border-top: 2px solid #e5e7eb; + border-top: 2px solid; + border-image: linear-gradient( + 90deg, + transparent, + rgba(0, 150, 255, 0.4), + rgba(255, 120, 0, 0.4), + transparent + ) + 1; gap: 1rem; flex-wrap: wrap; + position: relative; + z-index: 1; .nav-link { padding: 0.75rem 1.5rem; - background: #f3f4f6; - color: #374151; + background: linear-gradient( + 135deg, + rgba(0, 100, 180, 0.2) 0%, + rgba(0, 80, 150, 0.15) 100% + ); + color: #66ccff; text-decoration: none; - border-radius: 6px; + border-radius: 4px; font-weight: 500; - transition: all 0.2s ease; + font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; + border: 1px solid rgba(0, 150, 255, 0.4); + transition: all 0.3s ease; + text-transform: uppercase; + font-size: 0.85rem; + letter-spacing: 0.08em; + position: relative; + overflow: hidden; + + &::before { + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient( + 90deg, + transparent, + rgba(0, 150, 255, 0.2), + transparent + ); + transition: left 0.5s ease; + } &:hover { - background: #e5e7eb; + background: linear-gradient( + 135deg, + rgba(0, 120, 200, 0.3) 0%, + rgba(255, 100, 0, 0.2) 100% + ); + border-color: rgba(0, 150, 255, 0.7); + box-shadow: + 0 0 20px rgba(0, 150, 255, 0.4), + 0 0 30px rgba(255, 120, 0, 0.2); + color: #fff; + text-shadow: 0 0 10px rgba(0, 150, 255, 0.6); transform: translateY(-2px); + + &::before { + left: 100%; + } } &.back { - background: #3b82f6; - color: #fff; + background: linear-gradient( + 135deg, + rgba(255, 100, 0, 0.2) 0%, + rgba(200, 80, 0, 0.15) 100% + ); + border-color: rgba(255, 120, 0, 0.5); + color: #ffaa66; &:hover { - background: #2563eb; + border-color: rgba(255, 120, 0, 0.8); + box-shadow: + 0 0 20px rgba(255, 120, 0, 0.4), + 0 0 30px rgba(0, 150, 255, 0.2); } } @@ -574,3 +1109,756 @@ } } } + +.resource-tags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + justify-content: center; + margin: 1rem 0; + position: relative; + z-index: 1; + + .tag { + background: linear-gradient( + 135deg, + rgba(0, 100, 180, 0.2) 0%, + rgba(0, 80, 150, 0.1) 100% + ); + padding: 0.35rem 0.85rem; + border-radius: 4px; + font-size: 0.75rem; + color: rgba(100, 200, 255, 0.9); + text-decoration: none; + transition: all 0.3s ease; + font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; + border: 1px solid rgba(0, 150, 255, 0.3); + text-transform: uppercase; + letter-spacing: 0.06em; + font-weight: 500; + position: relative; + overflow: hidden; + + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 2px; + height: 100%; + background: linear-gradient( + 180deg, + rgba(0, 150, 255, 0.6), + rgba(255, 120, 0, 0.4) + ); + opacity: 0; + transition: opacity 0.3s ease; + } + + &:hover { + background: linear-gradient( + 135deg, + rgba(0, 120, 200, 0.3) 0%, + rgba(255, 100, 0, 0.2) 100% + ); + border-color: rgba(0, 150, 255, 0.6); + color: #fff; + box-shadow: 0 0 15px rgba(0, 150, 255, 0.3); + transform: translateY(-1px); + + &::before { + opacity: 1; + } + } + } +} + +// Contact section styling for resources +.contact-section { + margin: 3rem 0; + padding: 2rem; + background: linear-gradient( + 135deg, + rgba(0, 100, 180, 0.15) 0%, + rgba(0, 80, 150, 0.1) 100% + ); + border-radius: 6px; + border: 1px solid rgba(0, 150, 255, 0.3); + box-shadow: + 0 0 20px rgba(0, 150, 255, 0.1), + inset 0 0 40px rgba(0, 100, 200, 0.05); + position: relative; + z-index: 1; + + &::before { + content: ""; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 4px; + background: linear-gradient( + 180deg, + rgba(0, 150, 255, 0.7), + rgba(255, 120, 0, 0.5) + ); + border-radius: 6px 0 0 6px; + } + + .contact-title { + color: #0096ff; + font-size: 1.8rem; + font-family: Verdana, "Trebuchet MS", Tahoma, sans-serif; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.1em; + margin-bottom: 1.5rem; + text-shadow: 0 0 15px rgba(0, 150, 255, 0.5); + } + + .contact-content { + color: rgba(200, 220, 255, 0.95); + + p { + margin-bottom: 1.5rem; + line-height: 1.6; + } + } + + .contact-email, + .contact-pgp { + margin-bottom: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + + @include media-up(md) { + flex-direction: row; + align-items: center; + gap: 1rem; + } + } + + .contact-label { + font-weight: 600; + color: rgba(100, 180, 255, 0.9); + text-transform: uppercase; + font-size: 0.85rem; + letter-spacing: 0.08em; + } + + .contact-email a { + color: #66ccff; + text-decoration: none; + border-bottom: 1px solid rgba(0, 150, 255, 0.3); + transition: all 0.3s ease; + font-family: "Consolas", "Monaco", monospace; + + &:hover { + color: #ff7800; + border-bottom-color: rgba(255, 120, 0, 0.6); + text-shadow: 0 0 8px rgba(0, 150, 255, 0.5); + } + } + + .pgp-actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; + } + + .pgp-button { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: linear-gradient( + 135deg, + rgba(0, 100, 180, 0.3) 0%, + rgba(0, 80, 150, 0.2) 100% + ); + color: #66ccff; + border: 1px solid rgba(0, 150, 255, 0.4); + border-radius: 4px; + font-size: 0.85rem; + font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + text-transform: uppercase; + letter-spacing: 0.05em; + text-decoration: none; + + .button-icon { + font-size: 1.1rem; + } + + &:hover { + background: linear-gradient( + 135deg, + rgba(0, 120, 200, 0.4) 0%, + rgba(255, 100, 0, 0.25) 100% + ); + border-color: rgba(0, 150, 255, 0.7); + box-shadow: 0 0 15px rgba(0, 150, 255, 0.3); + color: #fff; + transform: translateY(-1px); + } + + &:active { + transform: translateY(0); + } + } + + .copy-feedback { + margin-top: 0.5rem; + font-size: 0.85rem; + font-weight: 500; + opacity: 0; + transition: opacity 0.3s ease; + + &.show { + opacity: 1; + } + + &.success { + color: #66ffcc; + text-shadow: 0 0 10px rgba(0, 255, 180, 0.6); + } + + &.error { + color: #ff9966; + text-shadow: 0 0 10px rgba(255, 120, 0, 0.6); + } + } +} + +// Last.fm Stats Interactive Form +#lastfm-stats-app { + margin: 2rem 0; + padding: 2rem; + background: linear-gradient( + 145deg, + rgba(5, 15, 30, 0.9) 0%, + rgba(10, 20, 40, 0.95) 100% + ); + border-radius: 6px; + border: 1px solid rgba(0, 150, 255, 0.4); + box-shadow: + 0 0 30px rgba(0, 150, 255, 0.15), + 0 0 50px rgba(255, 120, 0, 0.08), + inset 0 0 60px rgba(0, 100, 200, 0.05); + position: relative; + z-index: 1; + overflow: hidden; + + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient( + 90deg, + transparent, + rgba(0, 150, 255, 0.6), + rgba(255, 120, 0, 0.6), + transparent + ); + } +} + +.stats-form { + display: flex; + flex-direction: column; + gap: 1.5rem; + margin-bottom: 2rem; + position: relative; + z-index: 1; + + @include media-up(md) { + flex-direction: row; + align-items: flex-end; + } + + .form-group { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.5rem; + + label { + font-weight: 600; + color: #66ccff; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.08em; + font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; + text-shadow: 0 0 8px rgba(0, 150, 255, 0.4); + } + + input, + select { + padding: 0.75rem 1rem; + border: 1px solid rgba(0, 150, 255, 0.4); + border-radius: 4px; + font-size: 1rem; + font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; + background: rgba(5, 15, 30, 0.7); + color: rgba(200, 220, 255, 0.95); + transition: all 0.3s ease; + box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3); + + &:focus { + outline: none; + border-color: rgba(0, 150, 255, 0.7); + box-shadow: + 0 0 15px rgba(0, 150, 255, 0.3), + inset 0 2px 8px rgba(0, 0, 0, 0.4); + background: rgba(5, 15, 30, 0.9); + } + + &::placeholder { + color: rgba(100, 150, 200, 0.5); + } + } + + select { + cursor: pointer; + + option { + background: rgba(5, 15, 30, 0.95); + color: rgba(200, 220, 255, 0.95); + } + } + } + + .btn-primary { + padding: 0.75rem 2rem; + background: linear-gradient( + 135deg, + rgba(0, 120, 200, 0.3) 0%, + rgba(0, 100, 180, 0.2) 100% + ); + color: #66ccff; + border: 1px solid rgba(0, 150, 255, 0.5); + border-radius: 4px; + font-size: 0.95rem; + font-weight: 600; + font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; + cursor: pointer; + transition: all 0.3s ease; + align-self: flex-end; + text-transform: uppercase; + letter-spacing: 0.08em; + position: relative; + overflow: hidden; + + &::before { + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient( + 90deg, + transparent, + rgba(0, 150, 255, 0.3), + transparent + ); + transition: left 0.5s ease; + } + + &:hover { + background: linear-gradient( + 135deg, + rgba(0, 150, 255, 0.4) 0%, + rgba(255, 100, 0, 0.3) 100% + ); + border-color: rgba(0, 150, 255, 0.8); + box-shadow: + 0 0 20px rgba(0, 150, 255, 0.4), + 0 0 30px rgba(255, 120, 0, 0.2); + color: #fff; + text-shadow: 0 0 10px rgba(0, 150, 255, 0.6); + transform: translateY(-2px); + + &::before { + left: 100%; + } + } + + &:active { + transform: translateY(0); + box-shadow: 0 0 15px rgba(0, 150, 255, 0.3); + } + } +} + +.stats-results { + position: relative; + z-index: 1; + + .loading { + text-align: center; + padding: 2rem; + color: #66ccff; + font-size: 1.1rem; + font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; + + p { + animation: pulse 1.5s ease-in-out infinite; + text-shadow: 0 0 15px rgba(0, 150, 255, 0.6); + } + } + + #stats-content { + h2 { + color: #0096ff; + margin-bottom: 1.5rem; + font-size: 1.8rem; + font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.1em; + text-shadow: 0 0 15px rgba(0, 150, 255, 0.5); + border-bottom: 2px solid; + border-image: linear-gradient( + 90deg, + transparent, + rgba(0, 150, 255, 0.6), + transparent + ) + 1; + padding-bottom: 0.75rem; + } + } + + .stat-box { + background: linear-gradient( + 135deg, + rgba(0, 100, 180, 0.15) 0%, + rgba(0, 80, 150, 0.1) 100% + ); + padding: 1.5rem; + border-radius: 6px; + margin-bottom: 1.5rem; + border: 1px solid rgba(0, 150, 255, 0.3); + box-shadow: + 0 0 20px rgba(0, 150, 255, 0.1), + inset 0 0 40px rgba(0, 100, 200, 0.05); + transition: all 0.3s ease; + position: relative; + overflow: hidden; + + &::before { + content: ""; + position: absolute; + left: 0; + top: 0; + width: 3px; + height: 100%; + background: linear-gradient( + 180deg, + rgba(0, 150, 255, 0.7), + rgba(255, 120, 0, 0.5) + ); + opacity: 0; + transition: opacity 0.3s ease; + } + + &:hover { + background: linear-gradient( + 135deg, + rgba(0, 120, 200, 0.25) 0%, + rgba(255, 100, 0, 0.15) 100% + ); + border-color: rgba(0, 150, 255, 0.5); + box-shadow: + 0 0 30px rgba(0, 150, 255, 0.2), + 0 0 40px rgba(255, 120, 0, 0.1); + + &::before { + opacity: 1; + } + } + + h3 { + color: rgba(100, 180, 255, 0.8); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.12em; + margin-bottom: 0.75rem; + font-weight: 600; + font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; + text-shadow: 0 0 8px rgba(0, 150, 255, 0.4); + } + + .stat-number { + font-size: 3rem; + font-weight: 700; + color: #66ccff; + margin: 0; + font-family: "DSEG7-Classic", "Segoe UI", monospace; + text-shadow: + 0 0 20px rgba(0, 150, 255, 0.8), + 0 0 40px rgba(0, 150, 255, 0.4); + letter-spacing: 0.08em; + background: linear-gradient(135deg, #66ccff 0%, #0096ff 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + filter: drop-shadow(0 0 15px rgba(0, 150, 255, 0.5)); + } + } + + .artist-list { + list-style: none; + padding: 0; + margin: 0; + + li { + padding: 0.75rem 0; + padding-left: 1rem; + border-bottom: 1px solid rgba(0, 150, 255, 0.2); + font-size: 1.05rem; + color: rgba(200, 220, 255, 0.9); + font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; + position: relative; + transition: all 0.3s ease; + + &::before { + content: ""; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 60%; + background: linear-gradient( + 180deg, + rgba(0, 150, 255, 0.6), + rgba(255, 120, 0, 0.4) + ); + opacity: 0; + transition: opacity 0.3s ease; + } + + &:hover { + padding-left: 1.2rem; + + &::before { + opacity: 1; + } + } + + &:last-child { + border-bottom: none; + } + + a { + color: #66ccff; + text-decoration: none; + font-weight: 500; + transition: all 0.3s ease; + border-bottom: 1px solid rgba(0, 150, 255, 0.2); + + &:hover { + color: #ff7800; + border-bottom-color: rgba(255, 120, 0, 0.5); + text-shadow: 0 0 10px rgba(0, 150, 255, 0.5); + } + } + } + } + + .export-buttons { + display: flex; + gap: 1rem; + margin-top: 2rem; + padding-top: 1.5rem; + border-top: 2px solid; + border-image: linear-gradient( + 90deg, + transparent, + rgba(0, 150, 255, 0.3), + transparent + ) + 1; + flex-wrap: wrap; + + @include media-up(md) { + flex-wrap: nowrap; + } + + .btn-export { + flex: 1; + padding: 0.75rem 1.5rem; + background: linear-gradient( + 135deg, + rgba(0, 100, 180, 0.2) 0%, + rgba(0, 80, 150, 0.15) 100% + ); + color: #66ccff; + border: 1px solid rgba(0, 150, 255, 0.4); + border-radius: 4px; + font-size: 0.85rem; + font-weight: 600; + font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; + cursor: pointer; + transition: all 0.3s ease; + min-width: 150px; + text-transform: uppercase; + letter-spacing: 0.06em; + position: relative; + overflow: hidden; + + &::before { + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient( + 90deg, + transparent, + rgba(0, 150, 255, 0.2), + transparent + ); + transition: left 0.5s ease; + } + + &:hover { + background: linear-gradient( + 135deg, + rgba(0, 120, 200, 0.3) 0%, + rgba(255, 100, 0, 0.2) 100% + ); + border-color: rgba(0, 150, 255, 0.6); + transform: translateY(-2px); + box-shadow: 0 0 20px rgba(0, 150, 255, 0.3); + color: #fff; + text-shadow: 0 0 8px rgba(0, 150, 255, 0.5); + + &::before { + left: 100%; + } + } + + &:active { + transform: translateY(0); + box-shadow: 0 0 15px rgba(0, 150, 255, 0.2); + } + + &.copied { + background: linear-gradient( + 135deg, + rgba(0, 200, 150, 0.3) 0%, + rgba(0, 150, 100, 0.2) 100% + ); + border-color: rgba(0, 255, 180, 0.6); + color: #66ffcc; + text-shadow: 0 0 10px rgba(0, 255, 180, 0.6); + box-shadow: 0 0 20px rgba(0, 255, 180, 0.3); + } + } + } + + .error { + background: linear-gradient( + 135deg, + rgba(180, 50, 0, 0.2) 0%, + rgba(150, 30, 0, 0.15) 100% + ); + border: 1px solid rgba(255, 80, 0, 0.5); + border-radius: 6px; + padding: 1.5rem; + color: #ff9966; + text-align: center; + font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; + box-shadow: + 0 0 20px rgba(255, 80, 0, 0.2), + inset 0 0 40px rgba(255, 50, 0, 0.1); + position: relative; + + &::before { + content: ""; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 4px; + background: linear-gradient( + 180deg, + rgba(255, 120, 0, 0.8), + rgba(255, 80, 0, 0.6) + ); + } + + p { + margin: 0; + font-weight: 500; + text-shadow: 0 0 10px rgba(255, 120, 0, 0.5); + } + } +} + +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +// Energy particle animations - travel around the border +@keyframes particle-travel-blue { + 0% { + top: -10px; + left: 0; + } + 25% { + top: -10px; + left: 100%; + } + 50% { + top: 100%; + left: 100%; + } + 75% { + top: 100%; + left: -10px; + } + 100% { + top: -10px; + left: -10px; + } +} + +@keyframes particle-travel-orange { + 0% { + top: -10px; + left: 100%; + } + 25% { + top: 100%; + left: 100%; + } + 50% { + top: 100%; + left: -10px; + } + 75% { + top: -10px; + left: -10px; + } + 100% { + top: -10px; + left: 100%; + } +} diff --git a/assets/sass/style.scss b/assets/sass/style.scss index bdde189..b5a53f8 100644 --- a/assets/sass/style.scss +++ b/assets/sass/style.scss @@ -26,7 +26,7 @@ @import "pages/media"; @import "pages/resources"; -@import url("https://fonts.bunny.net/css2?family=Caveat:wght@400..700&family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&family=Neonderthaw&display=swap"); +@import url(https://fonts.bunny.net/css?family=abel:400|barlow-condensed:400,500|caveat:400|lato:300,300i,400,400i|neonderthaw:400); @font-face { font-family: "DSEG7-Classic"; diff --git a/content/resources/_index.md b/content/resources/_index.md index 8a2654c..28c733f 100644 --- a/content/resources/_index.md +++ b/content/resources/_index.md @@ -3,6 +3,6 @@ title: "Resources" draft: false --- -# Resources +Welcome to my test chamber, here you'll find various experiements, tests and resources. Take a look around. -Welcome to my whiteboard of resources. Here you'll find various tools, scripts, and experiments I've built and wanted to share. +The cake isn't a lie. diff --git a/content/resources/lastfm-stats/index.md b/content/resources/lastfm-stats/index.md index b33e843..d4b199b 100644 --- a/content/resources/lastfm-stats/index.md +++ b/content/resources/lastfm-stats/index.md @@ -1,7 +1,6 @@ --- -title: "Last.fm Weekly Stats Script" +title: "Last.fm Weekly Stats" date: 2026-01-04 -tags: ["javascript", "api", "last.fm"] description: "Fetch and display your weekly listening stats from Last.fm" icon: "lastfm-stats" demo_url: "" @@ -9,26 +8,115 @@ source_url: "" draft: false --- -A handy script for pulling your weekly listening statistics from Last.fm's API. Perfect for tracking your music habits and creating weekly listening reports. +Get your weekly listening statistics from Last.fm's API. Enter your username below to see your top artists and track counts for different time periods. -## Features +I made this so I could easily add in listening stats to my [weekly posts](/tags/weekly-update/), if you find it useful please let me know. -- Fetches weekly top tracks from Last.fm -- Displays track count and listening time -- Formats data in a clean, readable format -- Easy to integrate into blogs or dashboards +{{< lastfm-stats-form >}} -## Setup +## Download the Shell Script -1. Get your Last.fm API key from [Last.fm API](https://www.last.fm/api) -2. Configure your username in the script -3. Run the script to fetch your weekly stats +Want to run this locally or integrate it into your own workflows? Here is a bash script I was using to generate this before I decided to make a web version. -## Output +``` +#!/bin/bash -The script returns your top tracks for the week, including: -- Track name and artist -- Play count -- Listening duration +# Last.fm Weekly Stats Script +# Fetches your Last.fm listening statistics for the past week +# +# Requirements: +# - curl (for API requests) +# - jq (for JSON parsing) +# +# Usage: ./lastfm-week.sh +# +# Setup: +# Create a .env file with: +# LASTFM_API_KEY=your_api_key_here +# LASTFM_USERNAME=your_username_here +# +# Output: Markdown-formatted stats with top artists and track counts +# +# Download from: https://ritual.sh/resources/lastfm-stats/ -Great for weekly blog posts or personal music tracking! +# Load environment variables from .env file +if [ -f .env ]; then + export $(cat .env | grep -v '^#' | xargs) +else + echo "Error: .env file not found" + exit 1 +fi + +# Check required variables +if [ -z "$LASTFM_API_KEY" ] || [ -z "$LASTFM_USERNAME" ]; then + echo "Error: LASTFM_API_KEY and LASTFM_USERNAME must be set in .env file" + exit 1 +fi + +API_BASE="http://ws.audioscrobbler.com/2.0/" + +# Get current timestamp +NOW=$(date +%s) +# Get timestamp from 7 days ago +WEEK_AGO=$((NOW - 604800)) + +# Fetch top artists for the week +TOP_ARTISTS=$(curl -s "${API_BASE}?method=user.gettopartists&user=${LASTFM_USERNAME}&api_key=${LASTFM_API_KEY}&format=json&period=7day&limit=5") + +# Fetch recent tracks to count this week's scrobbles +RECENT_TRACKS=$(curl -s "${API_BASE}?method=user.getrecenttracks&user=${LASTFM_USERNAME}&api_key=${LASTFM_API_KEY}&format=json&from=${WEEK_AGO}&to=${NOW}&limit=1") + +# Get total track count +TOTAL_TRACKS=$(echo "$RECENT_TRACKS" | jq -r '.recenttracks["@attr"].total') + +# Output in markdown format +echo "## Last.fm Weekly Stats" +echo "" +echo "**Total Tracks:** ${TOTAL_TRACKS}" +echo "" +echo "**Top 5 Artists:**" +echo "" + +# Parse and display top 5 artists as markdown links +echo "$TOP_ARTISTS" | jq -r '.topartists.artist[] | "- [\(.name)](\(.url)) - \(.playcount) plays"' +``` + +### Shell Script Usage + +The script fetches your Last.fm stats and outputs them in markdown format. + +**Requirements:** + +- `curl` for API requests +- `jq` for JSON parsing + +**Setup:** + +1. Create a `.env` file with your credentials: + +```bash +LASTFM_API_KEY=your_api_key_here +LASTFM_USERNAME=your_username_here +``` + +2. Make the script executable: + +```bash +chmod +x lastfm-week.sh +``` + +3. Run it: + +```bash +./lastfm-week.sh +``` + +**Output:** + +The script prints markdown-formatted stats including: + +- Total tracks scrobbled this week +- Top 5 artists with play counts +- Direct links to artist pages + +Enjoy! diff --git a/content/resources/lavalamp/index.md b/content/resources/lavalamp/index.md index 1660430..e18dc73 100644 --- a/content/resources/lavalamp/index.md +++ b/content/resources/lavalamp/index.md @@ -1,12 +1,11 @@ --- title: "HTML/CSS Lavalamp" date: 2026-01-04 -tags: ["css", "html", "animation"] description: "A pure CSS lavalamp animation with floating blobs" icon: "lavalamp" demo_url: "" source_url: "" -draft: false +draft: true --- A mesmerizing lavalamp effect created entirely with HTML and CSS. Features smooth floating animations and gradient blobs that rise and fall like a classic lava lamp. diff --git a/lastfm-week.sh b/lastfm-week.sh index c208320..1d64688 100755 --- a/lastfm-week.sh +++ b/lastfm-week.sh @@ -1,7 +1,22 @@ #!/bin/bash # Last.fm Weekly Stats Script -# Usage: ./lastfm-weekly.sh +# Fetches your Last.fm listening statistics for the past week +# +# Requirements: +# - curl (for API requests) +# - jq (for JSON parsing) +# +# Usage: ./lastfm-week.sh +# +# Setup: +# Create a .env file with: +# LASTFM_API_KEY=your_api_key_here +# LASTFM_USERNAME=your_username_here +# +# Output: Markdown-formatted stats with top artists and track counts +# +# Download from: https://ritual.sh/resources/lastfm-stats/ # Load environment variables from .env file if [ -f .env ]; then diff --git a/layouts/blog/single.html b/layouts/blog/single.html index 1a9cec6..b8ea142 100644 --- a/layouts/blog/single.html +++ b/layouts/blog/single.html @@ -23,36 +23,7 @@
    {{ .Content }}
    -
    -

    Contact

    -
    -

    - If you found this interesting, have any comments, questions, - corrections, or just fancy saying "hello" please feel free to get - in touch. -

    -
    - Email: - dan@ritual.sh -
    -
    - PGP Public Key: -
    - - Download - - -
    -
    -
    -
    -
    + {{ partial "contact-section.html" . }}