-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscript.js
More file actions
153 lines (137 loc) · 5.26 KB
/
script.js
File metadata and controls
153 lines (137 loc) · 5.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
const projects = [
{
id: 'p1',
title: 'Static Site Boilerplate',
desc: 'A minimal, responsive static site starter with build scripts and deploy action.',
tags: ['html','css','workflow', 'github', 'actions', 'static', 'boilerplate', 'starter', 'template', 'web'],
link: '#'
},
{
id: 'p2',
title: 'Interactive Map Demo',
desc: 'Client-side interactive maps with vector tiles and custom markers.',
tags: ['javascript','maps','ui'],
link: '#'
},
{
id: 'p3',
title: 'Data Visualizer',
desc: 'Small library to visualize CSV and JSON datasets with charts and export.',
tags: ['javascript','charts','data'],
link: '#'
},
{
id: 'p4',
title: 'Component Library',
desc: 'Collection of accessible UI components styled with CSS variables.',
tags: ['css','ui','accessibility'],
link: '#'
}
];
const grid = document.getElementById('grid');
const tagList = document.getElementById('tagList');
const search = document.getElementById('search');
const modal = document.getElementById('modal');
const modalTitle = document.getElementById('modalTitle');
const modalBody = document.getElementById('modalBody');
const modalClose = document.getElementById('modalClose');
// build unique tags
const allTags = Array.from(new Set(projects.flatMap(p => p.tags))).sort();
// render tag buttons
function renderTags(activeTag){
tagList.innerHTML = '';
const allBtn = document.createElement('button');
allBtn.className = 'tag' + (activeTag === '' ? ' active' : '');
allBtn.textContent = 'All';
allBtn.onclick = () => { applyFilter(''); };
tagList.appendChild(allBtn);
allTags.forEach(t => {
const b = document.createElement('button');
b.className = 'tag' + (t === activeTag ? ' active' : '');
b.textContent = t;
b.onclick = () => { applyFilter(t); };
tagList.appendChild(b);
});
}
// render project cards based on query and tag
let currentTag = '';
function renderGrid(q='', tag=''){
grid.innerHTML = '';
const ql = q.trim().toLowerCase();
const filtered = projects.filter(p => {
const matchText = ql === '' || (p.title + ' ' + p.desc + ' ' + p.tags.join(' ')).toLowerCase().includes(ql);
const matchTag = tag === '' || p.tags.includes(tag);
return matchText && matchTag;
});
if(filtered.length === 0){
const empty = document.createElement('div');
empty.style.color = 'var(--muted)';
empty.textContent = 'No projects match your search.';
grid.appendChild(empty);
return;
}
filtered.forEach(p => {
const card = document.createElement('article');
card.className = 'card';
card.tabIndex = 0;
card.innerHTML = `
<div class="thumb" aria-hidden="true">Preview</div>
<div>
<div style="display:flex;justify-content:space-between;align-items:center">
<div>
<div class="title">${escapeHtml(p.title)}</div>
<div class="desc">${escapeHtml(p.desc)}</div>
</div>
<div class="meta"><span style="color:var(--muted);font-size:12px">${p.tags[0] || ''}</span></div>
</div>
<div class="badges" style="margin-top:8px"></div>
</div>
`;
const badges = card.querySelector('.badges');
p.tags.forEach(t => {
const b = document.createElement('span');
b.className = 'badge';
b.textContent = t;
b.onclick = (e) => { e.stopPropagation(); applyFilter(t); };
badges.appendChild(b);
});
card.onclick = () => openModal(p);
card.onkeydown = (e) => { if(e.key === 'Enter') openModal(p); };
grid.appendChild(card);
});
}
function applyFilter(tag){
currentTag = tag;
renderTags(tag);
renderGrid(search.value, tag);
}
function openModal(p){
modalTitle.textContent = p.title;
modalBody.innerHTML = `
<p style="margin-top:6px">${escapeHtml(p.desc)}</p>
<p style="margin-top:12px"><strong>Tags:</strong> ${p.tags.map(t=>'<span style="display:inline-block;margin-left:8px;background:rgba(255,255,255,0.03);padding:4px 8px;border-radius:8px;font-size:12px;color:var(--muted)">'+escapeHtml(t)+'</span>').join('')}</p>
<p style="margin-top:12px"><a href="${p.link}" style="color:var(--accent);text-decoration:none">Open project</a></p>
`;
modal.classList.add('open');
modal.setAttribute('aria-hidden','false');
}
function closeModal(){
modal.classList.remove('open');
modal.setAttribute('aria-hidden','true');
}
modalClose.addEventListener('click', closeModal);
modal.addEventListener('click', (e)=>{ if(e.target === modal) closeModal(); });
document.addEventListener('keydown', (e)=>{ if(e.key === 'Escape') closeModal(); });
search.addEventListener('input', ()=> renderGrid(search.value, currentTag));
// small sanitizer for text
function escapeHtml(s){
return String(s)
.replace(/&/g,'&')
.replace(/</g,'<')
.replace(/>/g,'>')
.replace(/"/g,'"')
.replace(/'/g,''');
}
// initial render
renderTags('');
renderGrid();