did all this shit without even commiting once ffs
This commit is contained in:
commit
26e489f0bf
12 changed files with 2978 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
public
|
||||
resources
|
||||
.hugo_build.lock
|
||||
data
|
||||
5
archetypes/default.md
Normal file
5
archetypes/default.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
+++
|
||||
date = '{{ .Date }}'
|
||||
draft = true
|
||||
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
|
||||
+++
|
||||
41
fetch-and-convert.sh
Executable file
41
fetch-and-convert.sh
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Configuration
|
||||
PUBKEY="setto@basspistol.org"
|
||||
RELAY="wss://basspistol.org"
|
||||
DATA_DIR="data"
|
||||
|
||||
# Clean up old JSON files (optional)
|
||||
# rm -f data/*/*.json
|
||||
|
||||
# Define kind mappings
|
||||
declare -A KINDS
|
||||
KINDS[0]="profile/profile"
|
||||
KINDS[30023]="longform/blog"
|
||||
KINDS[36787]="music/tracks"
|
||||
KINDS[20]="pictures/photos"
|
||||
KINDS[22]="videos/videos"
|
||||
|
||||
# Fetch each kind
|
||||
for kind in "${!KINDS[@]}"; do
|
||||
output="${KINDS[$kind]}"
|
||||
echo "Fetching kind $kind..."
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
mkdir -p "$DATA_DIR/$(dirname "$output")"
|
||||
|
||||
# Fetch and convert to JSON
|
||||
nak req -k "$kind" -a "$PUBKEY" "$RELAY" | jq -s '.' > "$DATA_DIR/$output.json"
|
||||
|
||||
# Remove any existing .jsonl file in the same directory
|
||||
rm -f "$DATA_DIR/$(dirname "$output")"/*.jsonl
|
||||
|
||||
echo "Saved to $DATA_DIR/$output.json"
|
||||
done
|
||||
|
||||
echo "All data fetched and converted!"
|
||||
echo "JSONL files have been removed."
|
||||
|
||||
echo "Building website"
|
||||
|
||||
hugo build
|
||||
15
hugo.toml
Normal file
15
hugo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
baseURL = "https://setto.basspistol.com"
|
||||
locale = "en-us"
|
||||
title = "Settolino Malandrino"
|
||||
theme = "one-pager"
|
||||
disableKinds = ["sitemap", "taxonomy", "term"]
|
||||
|
||||
[params]
|
||||
sitename = "My Site"
|
||||
description = "Set, Party, Gang 徒 setto セット"
|
||||
headerImage = "/images/header.jpg"
|
||||
|
||||
[permalinks]
|
||||
page = "/:contentbasename/"
|
||||
|
||||
|
||||
1519
themes/one-pager/assets/css/style.css
Normal file
1519
themes/one-pager/assets/css/style.css
Normal file
File diff suppressed because it is too large
Load diff
870
themes/one-pager/assets/js/scripts.js
Normal file
870
themes/one-pager/assets/js/scripts.js
Normal file
|
|
@ -0,0 +1,870 @@
|
|||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Sticky Navigation
|
||||
const stickyNav = document.getElementById('stickyNav');
|
||||
const mainHeader = document.querySelector('.main-header');
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
const themeToggleSmall = document.getElementById('themeToggleSmall');
|
||||
let lastScrollY = window.scrollY;
|
||||
let ticking = false;
|
||||
|
||||
// Function to update sticky nav visibility
|
||||
function updateStickyNav() {
|
||||
const scrollY = window.scrollY;
|
||||
const headerHeight = mainHeader ? mainHeader.offsetHeight : 400;
|
||||
|
||||
if (scrollY > headerHeight - 100) {
|
||||
stickyNav.classList.add('visible');
|
||||
if (themeToggle) {
|
||||
themeToggle.classList.add('hidden');
|
||||
}
|
||||
} else {
|
||||
stickyNav.classList.remove('visible');
|
||||
if (themeToggle) {
|
||||
themeToggle.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
lastScrollY = scrollY;
|
||||
}
|
||||
|
||||
// Throttled scroll listener
|
||||
window.addEventListener('scroll', function() {
|
||||
if (!ticking) {
|
||||
window.requestAnimationFrame(function() {
|
||||
updateStickyNav();
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Initial check
|
||||
setTimeout(updateStickyNav, 100);
|
||||
|
||||
|
||||
|
||||
|
||||
// Read More toggle for blog posts
|
||||
const readMoreButtons = document.querySelectorAll('.read-more-toggle');
|
||||
|
||||
readMoreButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const content = this.nextElementSibling;
|
||||
if (content.style.display === 'none') {
|
||||
content.style.display = 'block';
|
||||
this.innerHTML = '<span>Read Less</span>';
|
||||
} else {
|
||||
content.style.display = 'none';
|
||||
this.innerHTML = '<span>Read More</span>';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Smooth scrolling for navigation links (both main nav and sticky nav)
|
||||
document.querySelectorAll('nav a').forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const targetId = this.getAttribute('href');
|
||||
const targetElement = document.querySelector(targetId);
|
||||
if (targetElement) {
|
||||
// Calculate offset for sticky nav
|
||||
const navHeight = stickyNav.offsetHeight;
|
||||
const targetPosition = targetElement.getBoundingClientRect().top + window.pageYOffset - navHeight - 20;
|
||||
|
||||
window.scrollTo({
|
||||
top: targetPosition,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ============ CUSTOM LIGHTBOX ============
|
||||
const lightbox = document.getElementById('customLightbox');
|
||||
const lightboxImage = document.getElementById('lightboxImage');
|
||||
const lightboxCaption = document.getElementById('lightboxCaption');
|
||||
const lightboxMeta = document.getElementById('lightboxMeta');
|
||||
const lightboxCounter = document.getElementById('lightboxCounter');
|
||||
const lightboxClose = document.getElementById('lightboxClose');
|
||||
const lightboxPrev = document.getElementById('lightboxPrev');
|
||||
const lightboxNext = document.getElementById('lightboxNext');
|
||||
|
||||
let currentPhotos = [];
|
||||
let currentIndex = 0;
|
||||
|
||||
// Get all photo items
|
||||
const photoItems = document.querySelectorAll('.photo-item');
|
||||
|
||||
// Build photo array
|
||||
photoItems.forEach(item => {
|
||||
const image = item.querySelector('img');
|
||||
const date = item.dataset.date || '';
|
||||
const pubkey = item.dataset.pubkey || '';
|
||||
const alt = item.dataset.alt || '';
|
||||
|
||||
if (image) {
|
||||
currentPhotos.push({
|
||||
url: image.src,
|
||||
alt: alt,
|
||||
date: date,
|
||||
pubkey: pubkey
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Open lightbox
|
||||
function openLightbox(index) {
|
||||
if (index < 0) index = currentPhotos.length - 1;
|
||||
if (index >= currentPhotos.length) index = 0;
|
||||
currentIndex = index;
|
||||
|
||||
const photo = currentPhotos[currentIndex];
|
||||
lightboxImage.src = photo.url;
|
||||
lightboxImage.alt = photo.alt;
|
||||
lightboxCaption.textContent = photo.alt;
|
||||
lightboxMeta.textContent = `📅 ${photo.date} | 🖊️ ${photo.pubkey}`;
|
||||
lightboxCounter.textContent = `${currentIndex + 1} / ${currentPhotos.length}`;
|
||||
|
||||
lightbox.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
// Close lightbox
|
||||
function closeLightbox() {
|
||||
lightbox.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// Navigate
|
||||
function prevPhoto() {
|
||||
openLightbox(currentIndex - 1);
|
||||
}
|
||||
|
||||
function nextPhoto() {
|
||||
openLightbox(currentIndex + 1);
|
||||
}
|
||||
|
||||
// Event listeners for photo clicks
|
||||
photoItems.forEach((item, index) => {
|
||||
const img = item.querySelector('img');
|
||||
if (img) {
|
||||
img.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
openLightbox(index);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Event listeners for lightbox controls
|
||||
lightboxClose.addEventListener('click', closeLightbox);
|
||||
lightboxPrev.addEventListener('click', prevPhoto);
|
||||
lightboxNext.addEventListener('click', nextPhoto);
|
||||
|
||||
// Keyboard controls
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (!lightbox.classList.contains('active')) return;
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
closeLightbox();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
prevPhoto();
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
nextPhoto();
|
||||
}
|
||||
});
|
||||
|
||||
// Click outside image to close
|
||||
lightbox.addEventListener('click', function(e) {
|
||||
if (e.target === lightbox) {
|
||||
closeLightbox();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// No external lightbox library needed anymore!
|
||||
console.log('Custom lightbox initialized successfully!');
|
||||
|
||||
// Artist filter list
|
||||
const allowedArtists = [
|
||||
'徒 Setto セット',
|
||||
'Settoshi Tonami',
|
||||
'Sethy Bowoy'
|
||||
];
|
||||
|
||||
// Function to check if a track is by an allowed artist
|
||||
function isAllowedArtist(artist) {
|
||||
if (!artist) return false;
|
||||
// Normalize the artist name for comparison (remove extra spaces, lowercase)
|
||||
const normalizedArtist = artist.trim().toLowerCase();
|
||||
|
||||
return allowedArtists.some(allowed => {
|
||||
// Normalize the allowed artist name too
|
||||
const normalizedAllowed = allowed.trim().toLowerCase();
|
||||
// Check if the artist name contains the allowed artist name or vice versa
|
||||
return normalizedArtist.includes(normalizedAllowed) ||
|
||||
normalizedAllowed.includes(normalizedArtist);
|
||||
});
|
||||
}
|
||||
|
||||
// Render music tracks (randomized, 6 at a time, filtered by artist)
|
||||
function renderMusicTracks(tracks, containerId) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
// Filter tracks by artist
|
||||
const filteredTracks = tracks.filter(track => {
|
||||
// Extract artist from tags
|
||||
let artist = '';
|
||||
if (track.tags) {
|
||||
track.tags.forEach(tag => {
|
||||
if (tag[0] === 'artist') {
|
||||
artist = tag[1];
|
||||
}
|
||||
});
|
||||
}
|
||||
return isAllowedArtist(artist);
|
||||
});
|
||||
|
||||
// Check if we have any filtered tracks
|
||||
if (filteredTracks.length === 0) {
|
||||
container.innerHTML = '<p style="text-align:center;padding:2rem;color:var(--text-secondary);font-family:\'Poppins\',sans-serif;">No tracks found for the selected artists.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Shuffle and get first 6
|
||||
const shuffled = filteredTracks.sort(() => 0.5 - Math.random());
|
||||
const selected = shuffled.slice(0, 6);
|
||||
|
||||
// Clear container
|
||||
container.innerHTML = '';
|
||||
|
||||
// Render each track (same as before)
|
||||
selected.forEach(track => {
|
||||
// Extract data from tags
|
||||
let title = '', url = '', image = '', artist = '', album = '',
|
||||
trackNumber = '', duration = '', released = '', language = '',
|
||||
tags = [], alt = '', content = '';
|
||||
|
||||
// Parse tags
|
||||
if (track.tags) {
|
||||
track.tags.forEach(tag => {
|
||||
if (tag[0] === 'title') title = tag[1];
|
||||
else if (tag[0] === 'url') url = tag[1];
|
||||
else if (tag[0] === 'image') image = tag[1];
|
||||
else if (tag[0] === 'artist') artist = tag[1];
|
||||
else if (tag[0] === 'album') album = tag[1];
|
||||
else if (tag[0] === 'track_number') trackNumber = tag[1];
|
||||
else if (tag[0] === 'duration') duration = tag[1];
|
||||
else if (tag[0] === 'released') released = tag[1];
|
||||
else if (tag[0] === 'language') language = tag[1];
|
||||
else if (tag[0] === 't') tags.push(tag[1]);
|
||||
else if (tag[0] === 'alt') alt = tag[1];
|
||||
});
|
||||
}
|
||||
|
||||
content = track.content || '';
|
||||
|
||||
// Create music card HTML
|
||||
const card = document.createElement('div');
|
||||
card.className = 'music-card';
|
||||
|
||||
let cardHTML = `
|
||||
${image ? `<img src="${image}" alt="${title}" class="music-image" loading="lazy">` : ''}
|
||||
<div class="music-info-header">
|
||||
<h4>${title || 'Untitled'}</h4>
|
||||
${artist ? `<p class="music-artist">${artist}</p>` : ''}
|
||||
${album ? `<p class="music-album">${album}</p>` : ''}
|
||||
</div>
|
||||
${url ? `<audio controls preload="none"><source src="${url}" type="audio/mpeg">Your browser does not support the audio element.</audio>` : ''}
|
||||
<div class="music-details">
|
||||
${trackNumber ? `<span class="track-number">Track ${trackNumber}</span>` : ''}
|
||||
${duration ? `<span class="duration">${duration}s</span>` : ''}
|
||||
${released ? `<span class="released">${released}</span>` : ''}
|
||||
${language ? `<span class="language">${language}</span>` : ''}
|
||||
</div>
|
||||
<div class="music-tags">
|
||||
${tags.map(t => `<span class="tag"><span>${t}</span></span>`).join('')}
|
||||
</div>
|
||||
${content ? `
|
||||
<button class="lyrics-toggle"><span>Show Info</span></button>
|
||||
<div class="lyrics" style="display: none;">${content.replace(/\n/g, '<br>')}</div>
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
card.innerHTML = cardHTML;
|
||||
container.appendChild(card);
|
||||
});
|
||||
|
||||
// Re-initialize event listeners for new buttons
|
||||
initMusicEventListeners();
|
||||
}
|
||||
|
||||
// Initialize music event listeners
|
||||
function initMusicEventListeners() {
|
||||
// Lyrics toggle
|
||||
document.querySelectorAll('.lyrics-toggle').forEach(button => {
|
||||
button.removeEventListener('click', handleLyricsToggle);
|
||||
button.addEventListener('click', handleLyricsToggle);
|
||||
});
|
||||
}
|
||||
|
||||
function handleLyricsToggle() {
|
||||
const lyrics = this.nextElementSibling;
|
||||
if (lyrics && lyrics.classList.contains('lyrics')) {
|
||||
if (lyrics.style.display === 'none') {
|
||||
lyrics.style.display = 'block';
|
||||
this.innerHTML = '<span>Hide Info</span>';
|
||||
} else {
|
||||
lyrics.style.display = 'none';
|
||||
this.innerHTML = '<span>Show Info</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh music (randomize)
|
||||
function refreshMusic() {
|
||||
if (window.allMusicData) {
|
||||
renderMusicTracks(window.allMusicData, 'musicGrid');
|
||||
}
|
||||
}
|
||||
|
||||
// Call this when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (window.allMusicData && window.allMusicData.length > 0) {
|
||||
renderMusicTracks(window.allMusicData, 'musicGrid');
|
||||
}
|
||||
});
|
||||
|
||||
// ============ CUSTOM BLURHASH DECODER ============
|
||||
// Based on the official blurhash implementation, but compact
|
||||
|
||||
const Blurhash = {
|
||||
// Base83 character set
|
||||
chars: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+,;<=>?@[]^_`{|}~',
|
||||
|
||||
// Decode base83 to integer
|
||||
decode83: function(str) {
|
||||
let value = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const c = str[i];
|
||||
const digit = this.chars.indexOf(c);
|
||||
if (digit === -1) {
|
||||
throw new Error('Invalid base83 character: ' + c);
|
||||
}
|
||||
value = value * 83 + digit;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
||||
// Decode blurhash to pixel data
|
||||
decode: function(blurhash, width, height, punch = 1) {
|
||||
// Parse metadata
|
||||
const sizeFlag = this.decode83(blurhash[0]);
|
||||
const numX = (sizeFlag % 9) + 1;
|
||||
const numY = Math.floor(sizeFlag / 9) + 1;
|
||||
|
||||
// Calculate maximum AC component value
|
||||
const maxAC = this.decode83(blurhash[1]) + 1;
|
||||
|
||||
// Decode DC component
|
||||
const dcValue = this.decode83(blurhash.slice(2, 6));
|
||||
const dc = this.decodeDC(dcValue);
|
||||
|
||||
// Decode AC components
|
||||
const ac = [];
|
||||
let acIndex = 6;
|
||||
for (let y = 0; y < numY; y++) {
|
||||
for (let x = 0; x < numX; x++) {
|
||||
if (x === 0 && y === 0) continue;
|
||||
const encoded = this.decode83(blurhash.slice(acIndex, acIndex + 2));
|
||||
acIndex += 2;
|
||||
const acValue = this.decodeAC(encoded, maxAC * punch);
|
||||
ac.push({ x, y, value: acValue });
|
||||
}
|
||||
}
|
||||
|
||||
// Render pixels
|
||||
const pixels = new Uint8ClampedArray(width * height * 4);
|
||||
const size = width * height;
|
||||
|
||||
// Pre-calculate cos values for performance
|
||||
const cosX = [];
|
||||
const cosY = [];
|
||||
for (let x = 0; x < width; x++) {
|
||||
cosX[x] = Math.cos(Math.PI * x / width);
|
||||
}
|
||||
for (let y = 0; y < height; y++) {
|
||||
cosY[y] = Math.cos(Math.PI * y / height);
|
||||
}
|
||||
|
||||
for (let i = 0; i < size; i++) {
|
||||
const x = i % width;
|
||||
const y = Math.floor(i / width);
|
||||
let r = dc[0];
|
||||
let g = dc[1];
|
||||
let b = dc[2];
|
||||
|
||||
for (const acVal of ac) {
|
||||
const basis = cosX[x] * cosY[y];
|
||||
const value = acVal.value;
|
||||
r += value[0] * basis;
|
||||
g += value[1] * basis;
|
||||
b += value[2] * basis;
|
||||
}
|
||||
|
||||
const index = i * 4;
|
||||
pixels[index] = this.clamp(r + 128);
|
||||
pixels[index + 1] = this.clamp(g + 128);
|
||||
pixels[index + 2] = this.clamp(b + 128);
|
||||
pixels[index + 3] = 255;
|
||||
}
|
||||
|
||||
return pixels;
|
||||
},
|
||||
|
||||
// Decode DC component
|
||||
decodeDC: function(value) {
|
||||
return [
|
||||
this.signPow((value >> 16) & 0x3ff, 2),
|
||||
this.signPow((value >> 8) & 0xff, 2),
|
||||
this.signPow(value & 0xff, 2)
|
||||
];
|
||||
},
|
||||
|
||||
// Decode AC component
|
||||
decodeAC: function(value, maxVal) {
|
||||
const quantR = Math.floor(value / (19 * 19));
|
||||
const quantG = Math.floor((value / 19) % 19);
|
||||
const quantB = value % 19;
|
||||
return [
|
||||
this.signPow((quantR - 9) / 9, 2) * maxVal,
|
||||
this.signPow((quantG - 9) / 9, 2) * maxVal,
|
||||
this.signPow((quantB - 9) / 9, 2) * maxVal
|
||||
];
|
||||
},
|
||||
|
||||
// Sign power function
|
||||
signPow: function(val, exp) {
|
||||
return Math.sign(val) * Math.pow(Math.abs(val), exp);
|
||||
},
|
||||
|
||||
// Clamp value to 0-255
|
||||
clamp: function(value) {
|
||||
return Math.min(255, Math.max(0, value));
|
||||
},
|
||||
|
||||
// Render blurhash to canvas
|
||||
renderToCanvas: function(blurhash, canvas, punch = 1) {
|
||||
if (!blurhash) return false;
|
||||
|
||||
try {
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
const pixels = this.decode(blurhash, width, height, punch);
|
||||
const ctx = canvas.getContext('2d');
|
||||
const imageData = ctx.createImageData(width, height);
|
||||
imageData.data.set(pixels);
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.warn('Failed to decode blurhash:', e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Get blurhash as data URL
|
||||
toDataURL: function(blurhash, width = 64, height = 64, punch = 1) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const success = this.renderToCanvas(blurhash, canvas, punch);
|
||||
return success ? canvas.toDataURL('image/png') : null;
|
||||
}
|
||||
};
|
||||
|
||||
// ============ RANDOMIZED GALLERIES ============
|
||||
|
||||
// Function to shuffle array (Fisher-Yates)
|
||||
function shuffleArray(array) {
|
||||
const shuffled = [...array];
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||
}
|
||||
return shuffled;
|
||||
}
|
||||
|
||||
// Render random pictures (6 max)
|
||||
|
||||
function renderRandomPictures(picturesData, containerId) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
// Shuffle and get first 6
|
||||
const shuffled = shuffleArray(picturesData);
|
||||
const selected = shuffled.slice(0, 6);
|
||||
|
||||
// Clear container
|
||||
container.innerHTML = '';
|
||||
|
||||
// Render each picture
|
||||
selected.forEach(picture => {
|
||||
// Parse imeta tag to get image URL
|
||||
let imageUrl = '';
|
||||
let alt = picture.content || '';
|
||||
|
||||
if (picture.tags) {
|
||||
picture.tags.forEach(tag => {
|
||||
if (tag[0] === 'imeta') {
|
||||
const imetaStr = tag[1];
|
||||
// Extract URL
|
||||
const urlMatch = imetaStr.match(/url ([^\s]+)/);
|
||||
if (urlMatch) {
|
||||
imageUrl = urlMatch[1];
|
||||
}
|
||||
// Extract alt
|
||||
const altMatch = imetaStr.match(/alt ([^\s]+)/);
|
||||
if (altMatch) {
|
||||
alt = altMatch[1];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create picture item
|
||||
const item = document.createElement('div');
|
||||
item.className = 'photo-item';
|
||||
item.dataset.image = imageUrl;
|
||||
item.dataset.alt = alt;
|
||||
|
||||
const date = new Date(picture.created_at * 1000);
|
||||
const dateStr = date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
|
||||
item.innerHTML = `
|
||||
${imageUrl ? `
|
||||
<img src="${imageUrl}" alt="${alt}" loading="lazy">
|
||||
` : ''}
|
||||
<div class="photo-date">
|
||||
<span>📅 ${dateStr}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(item);
|
||||
});
|
||||
|
||||
// Re-initialize the custom lightbox for new images
|
||||
initCustomLightbox();
|
||||
}
|
||||
|
||||
// Initialize custom lightbox (call this after any DOM updates)
|
||||
function initCustomLightbox() {
|
||||
const lightbox = document.getElementById('customLightbox');
|
||||
if (!lightbox) return;
|
||||
|
||||
const lightboxImage = document.getElementById('lightboxImage');
|
||||
const lightboxCaption = document.getElementById('lightboxCaption');
|
||||
const lightboxMeta = document.getElementById('lightboxMeta');
|
||||
const lightboxCounter = document.getElementById('lightboxCounter');
|
||||
const lightboxClose = document.getElementById('lightboxClose');
|
||||
const lightboxPrev = document.getElementById('lightboxPrev');
|
||||
const lightboxNext = document.getElementById('lightboxNext');
|
||||
|
||||
// Get all photo items
|
||||
const photoItems = document.querySelectorAll('.photo-item');
|
||||
|
||||
// Build photo array
|
||||
let currentPhotos = [];
|
||||
photoItems.forEach(item => {
|
||||
const image = item.querySelector('img');
|
||||
const date = item.dataset.date || '';
|
||||
const pubkey = item.dataset.pubkey || '';
|
||||
const alt = item.dataset.alt || '';
|
||||
|
||||
if (image) {
|
||||
currentPhotos.push({
|
||||
url: image.src,
|
||||
alt: alt,
|
||||
date: date,
|
||||
pubkey: pubkey
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let currentIndex = 0;
|
||||
|
||||
// Remove old event listeners by cloning and replacing
|
||||
const newLightboxImage = lightboxImage.cloneNode(true);
|
||||
const newLightboxCaption = lightboxCaption.cloneNode(true);
|
||||
const newLightboxMeta = lightboxMeta.cloneNode(true);
|
||||
const newLightboxCounter = lightboxCounter.cloneNode(true);
|
||||
const newLightboxClose = lightboxClose.cloneNode(true);
|
||||
const newLightboxPrev = lightboxPrev.cloneNode(true);
|
||||
const newLightboxNext = lightboxNext.cloneNode(true);
|
||||
|
||||
lightboxImage.parentNode.replaceChild(newLightboxImage, lightboxImage);
|
||||
lightboxCaption.parentNode.replaceChild(newLightboxCaption, lightboxCaption);
|
||||
lightboxMeta.parentNode.replaceChild(newLightboxMeta, lightboxMeta);
|
||||
lightboxCounter.parentNode.replaceChild(newLightboxCounter, lightboxCounter);
|
||||
lightboxClose.parentNode.replaceChild(newLightboxClose, lightboxClose);
|
||||
lightboxPrev.parentNode.replaceChild(newLightboxPrev, lightboxPrev);
|
||||
lightboxNext.parentNode.replaceChild(newLightboxNext, lightboxNext);
|
||||
|
||||
// Update references
|
||||
const newImage = document.getElementById('lightboxImage');
|
||||
const newCaption = document.getElementById('lightboxCaption');
|
||||
const newMeta = document.getElementById('lightboxMeta');
|
||||
const newCounter = document.getElementById('lightboxCounter');
|
||||
const newClose = document.getElementById('lightboxClose');
|
||||
const newPrev = document.getElementById('lightboxPrev');
|
||||
const newNext = document.getElementById('lightboxNext');
|
||||
|
||||
// Open lightbox
|
||||
function openLightbox(index) {
|
||||
if (index < 0) index = currentPhotos.length - 1;
|
||||
if (index >= currentPhotos.length) index = 0;
|
||||
currentIndex = index;
|
||||
|
||||
const photo = currentPhotos[currentIndex];
|
||||
newImage.src = photo.url;
|
||||
newImage.alt = photo.alt;
|
||||
newCaption.textContent = photo.alt;
|
||||
newMeta.textContent = `📅 ${photo.date} | 🖊️ ${photo.pubkey}`;
|
||||
newCounter.textContent = `${currentIndex + 1} / ${currentPhotos.length}`;
|
||||
|
||||
lightbox.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
// Close lightbox
|
||||
function closeLightbox() {
|
||||
lightbox.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// Navigate
|
||||
function prevPhoto() {
|
||||
openLightbox(currentIndex - 1);
|
||||
}
|
||||
|
||||
function nextPhoto() {
|
||||
openLightbox(currentIndex + 1);
|
||||
}
|
||||
|
||||
// Event listeners for photo clicks
|
||||
photoItems.forEach((item, index) => {
|
||||
const img = item.querySelector('img');
|
||||
if (img) {
|
||||
// Remove old listener by cloning
|
||||
const newImg = img.cloneNode(true);
|
||||
img.parentNode.replaceChild(newImg, img);
|
||||
newImg.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
openLightbox(index);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Event listeners for lightbox controls
|
||||
newClose.addEventListener('click', closeLightbox);
|
||||
newPrev.addEventListener('click', prevPhoto);
|
||||
newNext.addEventListener('click', nextPhoto);
|
||||
|
||||
// Keyboard controls
|
||||
document.removeEventListener('keydown', handleLightboxKeydown);
|
||||
document.addEventListener('keydown', handleLightboxKeydown);
|
||||
|
||||
function handleLightboxKeydown(e) {
|
||||
if (!lightbox.classList.contains('active')) return;
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
closeLightbox();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
prevPhoto();
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
nextPhoto();
|
||||
}
|
||||
}
|
||||
|
||||
// Click outside image to close
|
||||
lightbox.removeEventListener('click', handleLightboxClick);
|
||||
lightbox.addEventListener('click', handleLightboxClick);
|
||||
|
||||
function handleLightboxClick(e) {
|
||||
if (e.target === lightbox) {
|
||||
closeLightbox();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the DOMContentLoaded event listener
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// ... (your existing code for theme toggle, sticky nav, etc.) ...
|
||||
|
||||
// Initialize Music (if you already have this)
|
||||
if (window.allMusicData && window.allMusicData.length > 0) {
|
||||
renderMusicTracks(window.allMusicData, 'musicGrid');
|
||||
}
|
||||
|
||||
// Initialize Pictures
|
||||
if (window.allPicturesData && window.allPicturesData.length > 0) {
|
||||
renderRandomPictures(window.allPicturesData, 'photoGrid');
|
||||
} else {
|
||||
// If no data, still initialize lightbox for static content
|
||||
initCustomLightbox();
|
||||
}
|
||||
|
||||
// Initialize Videos
|
||||
if (window.allVideosData && window.allVideosData.length > 0) {
|
||||
renderRandomVideos(window.allVideosData, 'videoGrid');
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh functions
|
||||
function refreshMusic() {
|
||||
if (window.allMusicData) {
|
||||
renderMusicTracks(window.allMusicData, 'musicGrid');
|
||||
}
|
||||
}
|
||||
|
||||
function refreshPictures() {
|
||||
if (window.allPicturesData) {
|
||||
renderRandomPictures(window.allPicturesData, 'photoGrid');
|
||||
}
|
||||
}
|
||||
|
||||
function refreshVideos() {
|
||||
if (window.allVideosData) {
|
||||
renderRandomVideos(window.allVideosData, 'videoGrid');
|
||||
}
|
||||
}
|
||||
|
||||
// Render random videos (2 max)
|
||||
function renderRandomVideos(videosData, containerId) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
// Shuffle and get first 2
|
||||
const shuffled = shuffleArray(videosData);
|
||||
const selected = shuffled.slice(0, 2);
|
||||
|
||||
// Clear container
|
||||
container.innerHTML = '';
|
||||
|
||||
// Static poster image
|
||||
const posterImage = '/images/videoposter.png';
|
||||
|
||||
|
||||
let html = '';
|
||||
|
||||
selected.forEach(video => {
|
||||
let videoUrl = '';
|
||||
let mimeType = 'video/mp4';
|
||||
let alt = video.content || '';
|
||||
let dim = '';
|
||||
let isVertical = false;
|
||||
|
||||
if (video.tags) {
|
||||
video.tags.forEach(tag => {
|
||||
if (tag[0] === 'imeta') {
|
||||
const imetaStr = tag[1];
|
||||
const urlMatch = imetaStr.match(/url ([^\s]+)/);
|
||||
if (urlMatch) videoUrl = urlMatch[1];
|
||||
|
||||
const mimeMatch = imetaStr.match(/m ([^\s]+)/);
|
||||
if (mimeMatch) mimeType = mimeMatch[1];
|
||||
|
||||
const altMatch = imetaStr.match(/alt ([^\s]+)/);
|
||||
if (altMatch) alt = altMatch[1];
|
||||
|
||||
const dimMatch = imetaStr.match(/dim ([^\s]+)/);
|
||||
if (dimMatch) {
|
||||
dim = dimMatch[1];
|
||||
const [width, height] = dim.split('x').map(Number);
|
||||
if (height > width) isVertical = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const date = new Date(video.created_at * 1000);
|
||||
const dateStr = date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
|
||||
const wrapperStyle = isVertical
|
||||
? 'style="position:relative;padding-bottom:177.78%;height:0;overflow:hidden;background:var(--accent-black);"'
|
||||
: '';
|
||||
|
||||
const videoStyle = isVertical
|
||||
? 'style="position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover;"'
|
||||
: 'style="width:100%;"';
|
||||
|
||||
html += `
|
||||
<div class="video-item">
|
||||
${videoUrl ? `
|
||||
<div class="video-wrapper" ${wrapperStyle}>
|
||||
<video controls preload="none" poster="${posterImage}" ${videoStyle}>
|
||||
<source src="${videoUrl}" type="${mimeType}">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="video-info">
|
||||
<p class="video-caption">${alt}</p>
|
||||
<div class="video-meta">
|
||||
<span class="video-date">📅 ${dateStr}</span>
|
||||
${dim ? `<span class="video-dim">📐 ${dim}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// ============ INITIALIZATION ============
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// ... (your existing code for theme toggle, sticky nav, etc.) ...
|
||||
|
||||
// Initialize Music (if you already have this)
|
||||
if (window.allMusicData && window.allMusicData.length > 0) {
|
||||
renderMusicTracks(window.allMusicData, 'musicGrid');
|
||||
}
|
||||
|
||||
// Initialize Pictures
|
||||
if (window.allPicturesData && window.allPicturesData.length > 0) {
|
||||
renderRandomPictures(window.allPicturesData, 'photoGrid');
|
||||
}
|
||||
|
||||
// Initialize Videos
|
||||
if (window.allVideosData && window.allVideosData.length > 0) {
|
||||
renderRandomVideos(window.allVideosData, 'videoGrid');
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh functions for randomize buttons
|
||||
function refreshMusic() {
|
||||
if (window.allMusicData) {
|
||||
renderMusicTracks(window.allMusicData, 'musicGrid');
|
||||
}
|
||||
}
|
||||
|
||||
function refreshPictures() {
|
||||
if (window.allPicturesData) {
|
||||
renderRandomPictures(window.allPicturesData, 'photoGrid');
|
||||
}
|
||||
}
|
||||
|
||||
function refreshVideos() {
|
||||
if (window.allVideosData) {
|
||||
renderRandomVideos(window.allVideosData, 'videoGrid');
|
||||
}
|
||||
}
|
||||
180
themes/one-pager/layouts/index.html
Normal file
180
themes/one-pager/layouts/index.html
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- SEO -->
|
||||
{{ partial "seo.html" . }}
|
||||
|
||||
|
||||
<meta name="robots" content="index" />
|
||||
<meta name="geo.region" content="CH-GE" />
|
||||
<meta name="geo.placename" content="Geneva" />
|
||||
<meta name="geo.position" content="46.203918;6.133011" />
|
||||
<meta name="ICBM" content="46.203918, 6.133011" />
|
||||
<link rel="canonical" href="https://setto.basspistol.com" />
|
||||
<link rel="alternate" hreflang="x-default" href="https://setto.basspistol.com" />
|
||||
{{ $style := resources.Get "css/style.css" | resources.Minify | fingerprint }}
|
||||
<link rel="stylesheet" href="{{ $style.RelPermalink }}">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
{{ partial "header.html" . }}
|
||||
|
||||
<main>
|
||||
<!-- BLOG SECTION - Longform -->
|
||||
<section id="blog" class="section">
|
||||
<h2>Now Log</h2>
|
||||
<div class="blog-grid">
|
||||
{{ $blogPosts := slice }}
|
||||
{{ range $path, $entries := hugo.Data.longform }}
|
||||
{{ range $entries }}
|
||||
{{ $blogPosts = $blogPosts | append . }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $sortedPosts := sort $blogPosts "created_at" "desc" }}
|
||||
{{ range first 2 $sortedPosts }}
|
||||
{{ $title := "" }}
|
||||
{{ $summary := "" }}
|
||||
{{ $image := "" }}
|
||||
{{ $tags := slice }}
|
||||
{{ $d := "" }}
|
||||
{{ $createdAt := int .created_at }}
|
||||
|
||||
{{ range .tags }}
|
||||
{{ if eq (index . 0) "title" }}
|
||||
{{ $title = index . 1 }}
|
||||
{{ else if eq (index . 0) "summary" }}
|
||||
{{ $summary = index . 1 }}
|
||||
{{ else if eq (index . 0) "image" }}
|
||||
{{ $image = index . 1 }}
|
||||
{{ else if eq (index . 0) "t" }}
|
||||
{{ $tags = $tags | append (index . 1) }}
|
||||
{{ else if eq (index . 0) "d" }}
|
||||
{{ $d = index . 1 }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
<div class="blog-card">
|
||||
{{ if $image }}
|
||||
<img src="{{ $image }}" alt="{{ $title }}" class="blog-header-image" loading="lazy">
|
||||
{{ end }}
|
||||
<div class="blog-content">
|
||||
<h3>{{ $title }}</h3>
|
||||
<p class="blog-meta">
|
||||
{{ dateFormat "January 2, 2006" (time (int .created_at)) }}
|
||||
</p>
|
||||
{{ if $summary }}
|
||||
<p class="blog-description">{{ $summary }}</p>
|
||||
{{ end }}
|
||||
<div class="blog-tags">
|
||||
{{ range $tags }}
|
||||
<span class="tag"><span>{{ . }}</span></span>
|
||||
{{ end }}
|
||||
</div>
|
||||
<button class="read-more-toggle">Read More</button>
|
||||
<div class="blog-full-content" style="display: none;">
|
||||
{{ .content | markdownify }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- MUSIC SECTION -->
|
||||
<section id="music" class="section">
|
||||
<h2>Music Traxxx</h2>
|
||||
<div style="text-align: right; margin-bottom: 1rem;">
|
||||
<button onclick="refreshMusic()" class="refresh-button">
|
||||
<span>🎲 Randomize</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="music-grid" id="musicGrid">
|
||||
{{ $allMusic := slice }}
|
||||
{{ range $path, $entries := hugo.Data.music }}
|
||||
{{ range $entries }}
|
||||
{{ $allMusic = $allMusic | append . }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ $musicData := jsonify $allMusic }}
|
||||
<script>
|
||||
window.allMusicData = {{ $musicData | safeJS }};
|
||||
</script>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PICTURES SECTION -->
|
||||
<section id="pictures" class="section">
|
||||
<h2>Pix & Doodles</h2>
|
||||
<div style="text-align: right; margin-bottom: 1rem;">
|
||||
<button onclick="refreshPictures()" class="refresh-button">
|
||||
<span>🎲 Randomize</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="photo-grid" id="photoGrid">
|
||||
{{ $allPictures := slice }}
|
||||
{{ range $path, $entries := hugo.Data.pictures }}
|
||||
{{ range $entries }}
|
||||
{{ $allPictures = $allPictures | append . }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
<!-- Store all pictures data in a JSON array -->
|
||||
{{ $picturesData := jsonify $allPictures }}
|
||||
<script>
|
||||
window.allPicturesData = {{ $picturesData | safeJS }};
|
||||
</script>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- VIDEO SECTION -->
|
||||
<section id="videos" class="section">
|
||||
<h2>Vidz</h2>
|
||||
<div style="text-align: right; margin-bottom: 1rem;">
|
||||
<button onclick="refreshVideos()" class="refresh-button">
|
||||
<span>🎲 Randomize</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="video-grid" id="videoGrid">
|
||||
{{ $allVideos := slice }}
|
||||
{{ range $path, $entries := hugo.Data.videos }}
|
||||
{{ range $entries }}
|
||||
{{ $allVideos = $allVideos | append . }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
<!-- Store all videos data in a JSON array -->
|
||||
{{ $videosData := jsonify $allVideos }}
|
||||
<script>
|
||||
window.allVideosData = {{ $videosData | safeJS }};
|
||||
</script>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
{{ partial "footer.html" . }}
|
||||
|
||||
<!-- Custom Lightbox -->
|
||||
<div class="custom-lightbox" id="customLightbox">
|
||||
<span class="custom-lightbox-close" id="lightboxClose">✕</span>
|
||||
<button class="custom-lightbox-prev" id="lightboxPrev">‹</button>
|
||||
<button class="custom-lightbox-next" id="lightboxNext">›</button>
|
||||
<div class="custom-lightbox-content">
|
||||
<img id="lightboxImage" src="" alt="">
|
||||
<div class="custom-lightbox-caption">
|
||||
<span id="lightboxCaption"></span>
|
||||
<span class="lightbox-meta" id="lightboxMeta"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="custom-lightbox-counter" id="lightboxCounter">1 / 1</div>
|
||||
</div>
|
||||
|
||||
{{ $js := resources.Get "js/scripts.js" | resources.Minify | fingerprint }}
|
||||
<script src="{{ $js.RelPermalink }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
55
themes/one-pager/layouts/partials/footer.html
Normal file
55
themes/one-pager/layouts/partials/footer.html
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<footer class="site-footer">
|
||||
<!-- Glow effect -->
|
||||
<div class="footer-glow"></div>
|
||||
|
||||
<div class="footer-content">
|
||||
<!-- Left Column -->
|
||||
<div class="footer-left">
|
||||
<div class="footer-brand">
|
||||
<span class="copyleft">🄯</span> Basspistol
|
||||
<span class="year">{{ now.Format "2006" }}</span>
|
||||
</div>
|
||||
<div class="footer-tagline">
|
||||
<span>✦</span> No cookies, no trackers <span>✦</span>
|
||||
</div>
|
||||
<div class="footer-statement">
|
||||
{{ .Site.Params.description | default "A cyberpunk nostr experience" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="footer-right">
|
||||
<div class="footer-meta-item">
|
||||
<span class="icon">🕒</span>
|
||||
<span class="label">Updated</span>
|
||||
<span class="value">{{ now.Format "Jan 2, 2006 · 15:04" }}</span>
|
||||
</div>
|
||||
<div class="footer-meta-item">
|
||||
<span class="icon">⚡</span>
|
||||
<span class="label">Powered by</span>
|
||||
<span class="value">
|
||||
<a href="https://gohugo.io/" target="_blank" rel="noopener noreferrer">Hugo</a>
|
||||
<span style="color:var(--accent-grey);opacity:0.3;margin:0 0.3rem;">&</span>
|
||||
<a href="https://nostr.org/" target="_blank" rel="noopener noreferrer">Nostr</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="footer-meta-item">
|
||||
<span class="icon">📡</span>
|
||||
<span class="label">Relay</span>
|
||||
<span class="value" style="font-size:0.65rem;opacity:0.7;">wss://basspistol.org</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Bar -->
|
||||
<div class="footer-bottom">
|
||||
<div class="footer-bottom-text">
|
||||
<span>✦</span> Built with <span>♥</span> for the nostr community <span>✦</span>
|
||||
</div>
|
||||
<div class="footer-social">
|
||||
<a href="https://nostr.basspistol.org/users/nprofile1qqsr7ydtkt3rtk3dfhd96m0t9ufrzu68df695099dz26r58kx2jz7sqpz3mhxue69uhkyctnwdcxjum5dakzummjvuy2xely" target="_blank" rel="noopener noreferrer">Nostr</a>
|
||||
<a href="Code" target="_blank" rel="noopener noreferrer">GitHub</a>
|
||||
<a href="https://basspistol.com" target="_blank" rel="noopener noreferrer">Bpist</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
85
themes/one-pager/layouts/partials/header.html
Normal file
85
themes/one-pager/layouts/partials/header.html
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
{{ $displayName := "" }}
|
||||
{{ $name := "" }}
|
||||
{{ $picture := "" }}
|
||||
{{ $banner := "" }}
|
||||
{{ $about := "" }}
|
||||
{{ $website := "" }}
|
||||
{{ $nip05 := "" }}
|
||||
{{ $lud16 := "" }}
|
||||
|
||||
{{ range hugo.Data.profile }}
|
||||
{{ range . }}
|
||||
{{ range .tags }}
|
||||
{{ if eq (index . 0) "display_name" }}
|
||||
{{ $displayName = index . 1 }}
|
||||
{{ else if eq (index . 0) "name" }}
|
||||
{{ $name = index . 1 }}
|
||||
{{ else if eq (index . 0) "picture" }}
|
||||
{{ $picture = index . 1 }}
|
||||
{{ else if eq (index . 0) "banner" }}
|
||||
{{ $banner = index . 1 }}
|
||||
{{ else if eq (index . 0) "about" }}
|
||||
{{ $about = index . 1 }}
|
||||
{{ else if eq (index . 0) "website" }}
|
||||
{{ $website = index . 1 }}
|
||||
{{ else if eq (index . 0) "nip05" }}
|
||||
{{ $nip05 = index . 1 }}
|
||||
{{ else if eq (index . 0) "lud16" }}
|
||||
{{ $lud16 = index . 1 }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
<!-- If display_name is empty, use name -->
|
||||
{{ if not $displayName }}
|
||||
{{ $displayName = $name }}
|
||||
{{ end }}
|
||||
|
||||
<!-- Sticky Navigation -->
|
||||
<nav class="sticky-nav" id="stickyNav" role="navigation">
|
||||
<div class="nav-container">
|
||||
<a href="#" class="nav-brand">
|
||||
<span>✦</span> {{ if $displayName }}{{ $displayName | truncate 12 }}{{ else }}{{ .Site.Title }}{{ end }}
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
<a href="#blog"><span>📝 Now</span></a>
|
||||
<a href="#music"><span>🎵 Music</span></a>
|
||||
<a href="#pictures"><span>🖼️ Pix</span></a>
|
||||
<a href="#videos"><span>🎬 Vidz</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<!-- Main Header -->
|
||||
<header class="main-header" style="position:relative;overflow:hidden;">
|
||||
<!-- Background image -->
|
||||
{{ if $banner }}
|
||||
<div style="position:absolute;top:0;left:0;right:0;bottom:0;z-index:0;background-image:url('{{ $banner }}');background-size:cover;background-position:center;"></div>
|
||||
<!-- Dark overlay for readability -->
|
||||
<div style="position:absolute;top:0;left:0;right:0;bottom:0;z-index:1;background:linear-gradient(to bottom,rgba(0,0,0,0.4) 0%,rgba(0,0,0,0.2) 50%,rgba(0,0,0,0.4) 100%);pointer-events:none;"></div>
|
||||
{{ end }}
|
||||
|
||||
|
||||
|
||||
<!-- Decorative '>' -->
|
||||
<div></div>
|
||||
|
||||
<div class="header-content">
|
||||
<div class="header-profile">
|
||||
{{ if $picture }}
|
||||
<img src="{{ $picture }}" alt="{{ $displayName }}" class="profile-picture" loading="lazy">
|
||||
{{ end }}
|
||||
<div class="header-text">
|
||||
|
||||
{{ if $about }}
|
||||
<div class="profile-about tagline"><h1>{{ if $displayName }}{{ $displayName }}{{ else }}{{ .Site.Title }}{{ end }}</h1>{{ $about | markdownify }}</div>
|
||||
{{ else }}
|
||||
<div class="profile-about tagline">{{ .Site.Params.description }}</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
148
themes/one-pager/layouts/partials/seo.html
Normal file
148
themes/one-pager/layouts/partials/seo.html
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
|
||||
|
||||
|
||||
{{/* Get profile data */}}
|
||||
{{ $displayName := "" }}
|
||||
{{ $name := "" }}
|
||||
{{ $picture := "" }}
|
||||
{{ $about := "" }}
|
||||
|
||||
{{ range hugo.Data.profile }}
|
||||
{{ range . }}
|
||||
{{ range .tags }}
|
||||
{{ if eq (index . 0) "display_name" }}
|
||||
{{ $displayName = index . 1 }}
|
||||
{{ else if eq (index . 0) "name" }}
|
||||
{{ $name = index . 1 }}
|
||||
{{ else if eq (index . 0) "picture" }}
|
||||
{{ $picture = index . 1 }}
|
||||
{{ else if eq (index . 0) "about" }}
|
||||
{{ $about = index . 1 }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Fallback if display_name is empty */}}
|
||||
{{ if not $displayName }}
|
||||
{{ $displayName = $name }}
|
||||
{{ end }}
|
||||
|
||||
{{/* If still empty, use site title */}}
|
||||
{{ if not $displayName }}
|
||||
{{ $displayName = .Site.Title }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Clean about text - remove markdown and HTML */}}
|
||||
{{ $cleanAbout := $about | plainify | htmlUnescape }}
|
||||
{{ $cleanAbout = trim $cleanAbout " \n\r\t" }}
|
||||
|
||||
{{/* Truncate to 160 chars for SEO */}}
|
||||
{{ $truncatedAbout := $cleanAbout }}
|
||||
{{ if gt (len $truncatedAbout) 160 }}
|
||||
{{ $truncatedAbout = substr $truncatedAbout 0 160 | printf "%s…" }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Hugo escaping functions */}}
|
||||
{{ $escapedTitle := $displayName | safeHTMLAttr }}
|
||||
{{ $escapedDescription := $truncatedAbout | safeHTMLAttr }}
|
||||
{{ $escapedPicture := $picture | safeURL }}
|
||||
{{ $escapedSiteTitle := .Site.Title | safeHTMLAttr }}
|
||||
|
||||
{{/* Build time in ISO 8601 format */}}
|
||||
{{ $buildTime := now.Format "2006-01-02T15:04:05-07:00" }}
|
||||
{{ $buildTimeRFC3339 := now.Format "2006-01-02T15:04:05Z07:00" }}
|
||||
|
||||
{{/* Hugo version */}}
|
||||
{{ $hugoVersion := hugo.Version }}
|
||||
|
||||
{{/* Base URL */}}
|
||||
{{ $baseURL := .Site.BaseURL | safeURL }}
|
||||
{{ $permalink := .Permalink | safeURL }}
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>{{ $escapedTitle }}{{ if ne $escapedTitle $escapedSiteTitle }} | {{ $escapedSiteTitle }}{{ end }}</title>
|
||||
<meta name="title" content="{{ $escapedTitle }}" />
|
||||
<meta name="description" content="{{ $escapedDescription }}" />
|
||||
<meta name="author" content="{{ $escapedTitle }}" />
|
||||
<meta name="generator" content="Hugo {{ $hugoVersion }}" />
|
||||
|
||||
<!-- Modified Date -->
|
||||
<meta name="dc.date.modified" scheme="ISO8601" content="{{ $buildTime }}" />
|
||||
<meta name="dcterms.modified" content="{{ $buildTime }}" />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="{{ $permalink }}" />
|
||||
<meta property="og:title" content="{{ $escapedTitle }}" />
|
||||
<meta property="og:description" content="{{ $escapedDescription }}" />
|
||||
{{ if $picture }}
|
||||
<meta property="og:image" content="{{ $escapedPicture }}" />
|
||||
{{ end }}
|
||||
<meta property="og:site_name" content="{{ $escapedTitle }}" />
|
||||
<meta property="article:published_time" content="{{ $buildTimeRFC3339 }}" />
|
||||
<meta property="article:modified_time" content="{{ $buildTimeRFC3339 }}" />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:url" content="{{ $permalink }}" />
|
||||
<meta name="twitter:title" content="{{ $escapedTitle }}" />
|
||||
<meta name="twitter:description" content="{{ $escapedDescription }}" />
|
||||
{{ if $picture }}
|
||||
<meta name="twitter:image" content="{{ $escapedPicture }}" />
|
||||
<meta name="twitter:image:alt" content="{{ $escapedTitle }}" />
|
||||
{{ end }}
|
||||
|
||||
<!-- Additional SEO -->
|
||||
<link rel="canonical" href="{{ $permalink }}" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta name="language" content="English" />
|
||||
<meta name="revisit-after" content="7 days" />
|
||||
|
||||
<!-- Theme Color for mobile browsers -->
|
||||
<meta name="theme-color" content="#ffd700" />
|
||||
<meta name="msapplication-TileColor" content="#ffd700" />
|
||||
|
||||
<!-- Favicon fallback -->
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E⚡%3C/text%3E%3C/svg%3E" />
|
||||
|
||||
<!-- JSON-LD Structured Data -->
|
||||
<script type="application/ld+json">
|
||||
{{- $personSchema := dict
|
||||
"@context" "https://schema.org"
|
||||
"@type" "Person"
|
||||
"name" ($displayName | safeHTML)
|
||||
"description" ($cleanAbout | safeHTML)
|
||||
"url" $baseURL
|
||||
-}}
|
||||
{{- if $picture -}}
|
||||
{{- $personSchema = merge $personSchema (dict "image" ($picture | safeHTML)) -}}
|
||||
{{- end -}}
|
||||
{{- $personSchema | jsonify (dict "indent" " ") | safeJS -}}
|
||||
</script>
|
||||
|
||||
<script type="application/ld+json">
|
||||
{{- $websiteSchema := dict
|
||||
"@context" "https://schema.org"
|
||||
"@type" "WebSite"
|
||||
"name" ($displayName | safeHTML)
|
||||
"description" ($cleanAbout | safeHTML)
|
||||
"url" $baseURL
|
||||
"dateModified" $buildTimeRFC3339
|
||||
-}}
|
||||
{{- $websiteSchema | jsonify (dict "indent" " ") | safeJS -}}
|
||||
</script>
|
||||
|
||||
<script type="application/ld+json">
|
||||
{{- $breadcrumbSchema := dict
|
||||
"@context" "https://schema.org"
|
||||
"@type" "BreadcrumbList"
|
||||
"itemListElement" (slice (dict
|
||||
"@type" "ListItem"
|
||||
"position" 1
|
||||
"name" "Home"
|
||||
"item" $baseURL
|
||||
))
|
||||
-}}
|
||||
{{- $breadcrumbSchema | jsonify (dict "indent" " ") | safeJS -}}
|
||||
</script>
|
||||
BIN
themes/one-pager/static/images/videoposter.png
Normal file
BIN
themes/one-pager/static/images/videoposter.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
56
video-poster.svg
Normal file
56
video-poster.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 1.5 MiB |
Loading…
Add table
Add a link
Reference in a new issue