art-gallery.js

  1import { readySkills, skillFocusAreas } from "../data.js";
  2import { renderSkillDemo, setupDemoTabs } from "../demo-renderer.js";
  3import { setupDemoToggles } from "../demo-toggles.js";
  4
  5export function initArtGallery() {
  6	// Initial setup if needed
  7}
  8
  9export function renderGallery(skills) {
 10	const container = document.querySelector(".skills-gallery");
 11	if (!container) return;
 12
 13	// Filter skills (hide impeccable as per original)
 14	const filteredSkills = skills.filter((s) => s.id !== "impeccable");
 15
 16	container.innerHTML = `
 17        <div class="gallery-track">
 18            ${filteredSkills.map((skill, index) => renderFrame(skill, index)).join("")}
 19        </div>
 20        <div class="gallery-map" role="tablist" aria-label="Skill gallery navigation">
 21            ${filteredSkills.map((skill, index) => `<button class="gallery-dot ${index === 0 ? "active" : ""}" data-index="${index}" role="tab" aria-selected="${index === 0 ? "true" : "false"}" aria-label="View ${formatName(skill.id)} skill"></button>`).join("")}
 22        </div>
 23    `;
 24
 25	setupInteractions();
 26	setupDemoTabs();
 27	setupDemoToggles();
 28}
 29
 30function renderFrame(skill, index) {
 31	const isReady = readySkills.includes(skill.id);
 32	const focusAreas = skillFocusAreas[skill.id] || [];
 33	const displayName = formatName(skill.id);
 34
 35	return `
 36        <article class="gallery-frame ${index === 0 ? "active" : ""}" data-index="${index}" id="skill-${skill.id}">
 37            <div class="gallery-content">
 38                <div class="gallery-visual">
 39                    ${isReady ? renderSkillDemo(skill.id) : renderComingSoonVisual(skill.id)}
 40                </div>
 41                <div class="gallery-info">
 42                    <div class="gallery-header">
 43                        <h3 class="gallery-title">${displayName}</h3>
 44                        <div class="gallery-meta">
 45                            Skill · ${isReady ? "Available" : "Coming Soon"}
 46                        </div>
 47                    </div>
 48                    <p class="gallery-desc">${skill.description}</p>
 49                    
 50                    ${
 51											focusAreas.length > 0
 52												? `
 53                        <div class="gallery-tags">
 54                            ${focusAreas
 55															.slice(0, 4)
 56															.map(
 57																(area) => `
 58                                <span class="gallery-tag">${area.area}</span>
 59                            `,
 60															)
 61															.join("")}
 62                        </div>
 63                    `
 64												: ""
 65										}
 66                </div>
 67            </div>
 68        </article>
 69    `;
 70}
 71
 72function renderComingSoonVisual(id) {
 73	return `
 74        <div class="coming-soon-placeholder" style="text-align: center;">
 75            <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" style="opacity: 0.3">
 76                <path d="M12 6v6l4 2M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/>
 77            </svg>
 78            <p style="margin-top: 1rem; color: var(--color-ash); font-size: 0.875rem;">Coming Soon</p>
 79        </div>
 80    `;
 81}
 82
 83function formatName(id) {
 84	return id
 85		.split("-")
 86		.map((word) =>
 87			word === "ux" ? "UX" : word.charAt(0).toUpperCase() + word.slice(1),
 88		)
 89		.join(" ");
 90}
 91
 92function setupInteractions() {
 93	const track = document.querySelector(".gallery-track");
 94	const frames = document.querySelectorAll(".gallery-frame");
 95	const dots = document.querySelectorAll(".gallery-dot");
 96
 97	if (!track) return;
 98
 99	// Intersection Observer for Active State
100	const observer = new IntersectionObserver(
101		(entries) => {
102			entries.forEach((entry) => {
103				if (entry.isIntersecting) {
104					const index = entry.target.dataset.index;
105
106					// Update frames
107					frames.forEach((f) => f.classList.remove("active"));
108					entry.target.classList.add("active");
109
110					// Update dots
111					dots.forEach((d) => {
112						d.classList.remove("active");
113						d.setAttribute("aria-selected", "false");
114					});
115					if (dots[index]) {
116						dots[index].classList.add("active");
117						dots[index].setAttribute("aria-selected", "true");
118					}
119				}
120			});
121		},
122		{
123			root: track,
124			threshold: 0.6,
125		},
126	);
127
128	frames.forEach((frame) => observer.observe(frame));
129
130	// Dot navigation
131	dots.forEach((dot, index) => {
132		dot.addEventListener("click", () => {
133			const frame = frames[index];
134			if (frame) {
135				frame.scrollIntoView({ behavior: "smooth", inline: "center" });
136			}
137		});
138	});
139}
140
141
142