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