// Services Page JavaScript
(function() {
'use strict';
// Discord webhook for form submissions (public - submissions go to private channel for review)
const WEBHOOK_URL = 'https://freelance.vdo.workers.dev';
// Fetch approved services from GitHub Gist (updated by Discord bot on approval)
const GIST_ID = '3642a19e9ed4b16571906cdb2e216a45';
const GIST_URL = GIST_ID
? `https://gist.githubusercontent.com/steveseguin/${GIST_ID}/raw/services.json`
: 'data/services.json'; // Fallback to local file
// DOM Elements
let servicesGrid;
let platformFilters;
let submitToggleBtn;
let submissionForm;
let formMessage;
let adminPanel;
// State
let services = [];
let currentFilter = 'ssn'; // Default to SSN filter
// Initialize
document.addEventListener('DOMContentLoaded', function() {
servicesGrid = document.getElementById('services-grid');
platformFilters = document.querySelectorAll('.platform-filter');
submitToggleBtn = document.getElementById('submit-toggle-btn');
submissionForm = document.getElementById('submission-form-wrapper');
formMessage = document.getElementById('form-message');
adminPanel = document.getElementById('admin-panel');
loadServices();
initFilters();
initFormToggle();
initForm();
initAdmin();
initMultiInputs();
initDiscordAuth();
initDiscordHelp();
});
// Load services from Gist (or fallback to local JSON)
async function loadServices() {
try {
// Add cache-busting for gist URL to get latest data
const url = GIST_ID
? `${GIST_URL}?t=${Date.now()}`
: GIST_URL;
const response = await fetch(url);
const data = await response.json();
services = data.services || [];
renderServices();
} catch (error) {
console.error('Failed to load services:', error);
renderEmptyState();
}
}
// Render services grid
function renderServices() {
if (!servicesGrid) return;
const filtered = currentFilter === 'all'
? services
: services.filter(s => s.platforms && s.platforms.includes(currentFilter));
if (filtered.length === 0) {
renderEmptyState();
return;
}
servicesGrid.innerHTML = filtered.map(service => createServiceCard(service)).join('');
// Add click handlers for portfolio images
document.querySelectorAll('.portfolio-thumb').forEach(img => {
img.addEventListener('click', (e) => {
e.stopPropagation();
openPortfolioModal(img.dataset.fullSrc || img.src);
});
});
// Add click handlers for reveal links (SEO protection)
document.querySelectorAll('.reveal-link').forEach(span => {
span.addEventListener('click', function() {
if (this.classList.contains('revealed')) return;
const encodedUrl = this.dataset.url;
const url = atob(encodedUrl); // Decode Base64
// Extract domain name from URL
let domain;
try {
domain = new URL(url).hostname.replace('www.', '');
} catch {
domain = url;
}
this.classList.add('revealed');
this.innerHTML = `${escapeHtml(domain)}`;
});
});
}
// Create service card HTML
function createServiceCard(service) {
const initials = getInitials(service.name);
const platformBadges = (service.platforms || []).map(p =>
`${p === 'ssn' ? 'SSN' : 'VDO.Ninja'}`
).join('');
const typeTags = (service.serviceTypes || []).map(t =>
`${t}`
).join('');
const portfolio = (service.portfolio || []).slice(0, 5).map(url =>
``
).join('');
const socials = createSocialLinks(service.socials || {});
const contactLink = service.socials?.discord || service.socials?.website ||
(service.paymentLinks && service.paymentLinks[0]) || '#';
// Use real avatar if available, otherwise show initials
const avatarHtml = service.avatarUrl
? `
`
: `
${escapeHtml(service.description || '')}
${portfolio ? `Be the first to offer your freelance services to the community!
Scroll down to submit your listing.