Skip to content

Commit d975d0f

Browse files
committed
feat: load projects from dedicated JSON file
1 parent 0c645c6 commit d975d0f

5 files changed

Lines changed: 55 additions & 15 deletions

File tree

README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ editar HTML, CSS ou JavaScript para alterar as listas:
2020

2121
- `data/members.json` — pessoas colaboradoras;
2222
- `data/posts.json` — posts do blog (opcional);
23-
- `data/site.json`textos institucionais, links, projetos e código
24-
de conduta.
23+
- `data/projects.json` — projetos da comunidade (opcional);
24+
- `data/site.json` — textos institucionais, links e código de conduta.
2525

2626
Depois de mudar um JSON, valide a sintaxe:
2727

2828
```sh
2929
python3 -m json.tool data/members.json >/dev/null
30+
python3 -m json.tool data/projects.json >/dev/null
3031
```
3132

3233
### Membro
@@ -54,6 +55,26 @@ que sempre tem prioridade. Os campos de rede social aceitam um usuário (com ou
5455
sem `@`) ou a URL completa do perfil. Para `mastodon`, use a URL completa ou o
5556
formato `@usuario@servidor`.
5657

58+
### Projeto
59+
60+
`data/projects.json` começa como uma lista vazia. Adicione um objeto para cada
61+
projeto; os campos `name` e `description` são obrigatórios. Ambos podem ser um
62+
texto simples ou um objeto com traduções `pt` e `en`. `url` é opcional e torna
63+
o card clicável; `image` é opcional e aceita uma URL HTTPS ou um caminho em
64+
`assets/` para exibir uma imagem de capa.
65+
66+
```json
67+
{
68+
"name": { "pt": "Nome do projeto", "en": "Project name" },
69+
"description": { "pt": "Descrição do projeto.", "en": "Project description." },
70+
"url": "https://exemplo.org/projeto",
71+
"image": "assets/images/projetos/capa.png"
72+
}
73+
```
74+
75+
Sem projetos na lista, a seção Projetos e seus links de navegação não são
76+
exibidos.
77+
5778
## Eventos e GitHub Actions
5879

5980
O workflow `.github/workflows/sync-meetup-events.yml` executa toda segunda-feira

assets/css/site.css

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,13 @@ a.card {
342342
padding-top: 6px;
343343
}
344344

345+
.project-image {
346+
width: 100%;
347+
max-height: 148px;
348+
border-radius: 8px;
349+
object-fit: cover;
350+
}
351+
345352
.pill {
346353
display: inline-flex;
347354
align-items: center;
@@ -823,4 +830,4 @@ a.card {
823830
scroll-behavior: auto !important;
824831
transition-duration: .01ms !important;
825832
}
826-
}
833+
}

assets/js/site.js

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
return link;
7979
};
8080
const safeHref = (href) => /^(https?:|mailto:|#)/i.test(href || '') ? href : '#';
81+
const safeImageSrc = (src) => /^(https?:|assets\/)/i.test(src || '') ? src : '';
8182
const profileUrl = (network, value) => {
8283
const raw = String(value || '').trim();
8384
if (!raw) return '';
@@ -137,7 +138,9 @@
137138
[
138139
['comunidade', text('community')], ['conheca-tambem', text('partners')], ['membros', text('members')],
139140
['eventos', text('events')], ['projetos', text('projects')], ['blog', text('blog')], ['conduta', text('conduct')]
140-
].filter(([id]) => (id !== 'blog' || state.posts.length) && (id !== 'membros' || state.members.length))
141+
].filter(([id]) => (id !== 'blog' || state.posts.length)
142+
&& (id !== 'membros' || state.members.length)
143+
&& (id !== 'projetos' || state.projects.length))
141144
.forEach(([id, label]) => nav.append(element('a', { href: `#${id}`, text: label, 'data-section': id })));
142145
}
143146

@@ -158,7 +161,8 @@
158161

159162
const cards = byId('home-cards');
160163
clear(cards);
161-
(site.home.cards || []).filter((card) => card.route !== 'membros' || state.members.length).forEach((card, index) => {
164+
(site.home.cards || []).filter((card) => (card.route !== 'membros' || state.members.length)
165+
&& (card.route !== 'projetos' || state.projects.length)).forEach((card, index) => {
162166
const cardLink = element('a', { class: 'card', href: `#${card.route}` });
163167
cardLink.append(
164168
element('span', { class: 'card-index', text: String(index + 1).padStart(2, '0') }),
@@ -263,12 +267,22 @@
263267
});
264268
}
265269

266-
function renderProjectsAndConduct() {
270+
function renderProjects() {
271+
const section = byId('projetos');
267272
const projects = byId('project-cards');
268273
clear(projects);
269-
(state.site.projects || []).forEach((project) => {
270-
projects.append(element('article', { class: 'card' }, [element('h3', { text: project.title }), element('p', { text: pick(project.text) })]));
274+
section.hidden = !state.projects.length;
275+
if (!state.projects.length) return;
276+
state.projects.forEach((project) => {
277+
const card = project.url ? external(project.url, '', 'card') : element('article', { class: 'card' });
278+
const imageSrc = safeImageSrc(project.image);
279+
if (imageSrc) card.append(element('img', { class: 'project-image', src: imageSrc, alt: '' }));
280+
card.append(element('h3', { text: pick(project.name) }), element('p', { text: pick(project.description) }));
281+
projects.append(card);
271282
});
283+
}
284+
285+
function renderConduct() {
272286
const conduct = byId('conduct-list');
273287
clear(conduct);
274288
(state.site.conduct.items || []).forEach((item) => conduct.append(element('li', { text: pick(item) })));
@@ -312,7 +326,7 @@
312326
const upcoming = events.filter(isUpcoming).sort((a, b) => a.date - b.date);
313327
const past = events.filter((event) => !isUpcoming(event)).sort((a, b) => b.date - a.date);
314328
renderNavigation(); renderHome(upcoming); renderCommunity(); renderPartners(); renderMembers();
315-
renderEvents(upcoming, past); renderProjectsAndConduct(); renderPosts(); renderFooter(); observeSections();
329+
renderEvents(upcoming, past); renderProjects(); renderConduct(); renderPosts(); renderFooter(); observeSections();
316330
}
317331

318332
let observer;
@@ -327,16 +341,17 @@
327341
}
328342

329343
async function load() {
330-
const files = ['site.json', 'members.json', 'events.json', 'posts.json'];
344+
const files = ['site.json', 'members.json', 'events.json', 'projects.json', 'posts.json'];
331345
try {
332346
const results = await Promise.all(files.map(async (file) => {
333347
const response = await fetch(`data/${file}`);
334348
if (!response.ok) throw new Error(`${file}: ${response.status}`);
335349
return response.json();
336350
}));
337-
[state.site, state.members, state.events, state.posts] = results;
351+
[state.site, state.members, state.events, state.projects, state.posts] = results;
338352
state.members = Array.isArray(state.members) ? state.members : [];
339353
state.events = Array.isArray(state.events) ? state.events : [];
354+
state.projects = Array.isArray(state.projects) ? state.projects : [];
340355
state.posts = Array.isArray(state.posts) ? state.posts : [];
341356
render();
342357
} catch (error) {

data/projects.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]

data/site.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,6 @@
2727
{ "title": { "pt": "Associação Python Brasil", "en": "Python Brasil Association" }, "text": { "pt": "A APyB apoia e representa a comunidade Python no Brasil.", "en": "APyB supports and represents the Python community in Brazil." }, "linkText": { "pt": "Conheça a APyB", "en": "Meet APyB" }, "href": "https://apyb.python.org.br/" },
2828
{ "title": { "pt": "Outras comunidades locais", "en": "Other local communities" }, "text": { "pt": "Encontre grupos Python em outras cidades e regiões do país.", "en": "Find Python groups in other cities and regions across Brazil." }, "linkText": { "pt": "Ver comunidades locais", "en": "View local communities" }, "href": "https://python.org.br/comunidades-locais/" }
2929
],
30-
"projects": [
31-
{ "title": "Traduções do Django docs", "text": { "pt": "Tradução colaborativa da documentação oficial do Django para o português.", "en": "Collaborative translation of the official Django documentation into Portuguese." } },
32-
{ "title": "Python on Campus", "text": { "pt": "Palestras e oficinas que levam Python para universidades de Minas Gerais.", "en": "Talks and workshops that bring Python to universities in Minas Gerais." } }
33-
],
3430
"conduct": {
3531
"title": { "pt": "Código de conduta", "en": "Code of conduct" },
3632
"items": [

0 commit comments

Comments
 (0)