snapport 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  <p align="center">
2
- <img src="https://github.com/guilhermegodoydev/snapport/blob/main/preview.png" width="300" height="300" style="border-radius: 50%" alt="Logo" />
2
+ <img src="https://github.com/guilhermegodoydev/snapport/blob/main/preview.png" width="120" height="120" style="border-radius: 50%" alt="Logo" />
3
3
  </p>
4
4
 
5
5
  <h1 align="center">Snapport</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>Seu portfólio alimentado automaticamente pelos tópicos do seu GitHub.</strong>
8
+ <strong>Gere automaticamente a seção de projetos do seu portfólio a partir dos seus repositórios do GitHub.</strong>
9
9
  </p>
10
10
 
11
11
  <p align="center">
@@ -15,42 +15,107 @@
15
15
  </p>
16
16
 
17
17
  <p align="center">
18
- <a href="https://guilhermegodoydev.github.io/snapport"><strong>Explorar Documentação »</strong></a>
19
- <br /><br />
20
- <a href="https://github.com/guilhermegodoydev/snapport/issues">Reportar Bug</a>
21
- ·
22
- <a href="https://github.com/guilhermegodoydev/snapport/issues">Sugestão de Feature</a>
18
+ <a href="https://guilhermegodoydev.github.io/snapport"><strong>Ver Documentação »</strong></a>
23
19
  </p>
24
20
 
25
21
  ---
26
22
 
27
- ## 💡 Por que Snapport?
23
+ ## 💡 O problema
28
24
 
29
- Cansado de atualizar manualmente o HTML do seu portfólio toda vez que termina um projeto? O **Snapport** transforma seus repositórios do GitHub na sua única fonte de verdade. Marque com uma tag, e seu site se atualiza sozinho.
25
+ Manter a seção de projetos do portfólio atualizada manualmente é repetitivo:
30
26
 
31
- - **Zero Dependências:** TypeScript puro. Sem inchaço no seu bundle.
32
- - **Cache Inteligente:** Persistência local de 2 horas para respeitar os limites da API.
33
- - **Framework Agnostic:** Use com React, Vue, Angular ou apenas HTML puro.
27
+ - você cria um novo projeto
28
+ - atualiza o GitHub
29
+ - esquece de atualizar o portfólio
30
+ - seu portfólio fica desatualizado
34
31
 
35
- ## 🚀 Início Rápido
32
+ ---
33
+
34
+ ## ✨ O que o Snapport faz
35
+
36
+ O Snapport automatiza a geração da seção de projetos do seu portfólio com base nos seus repositórios do GitHub.
37
+
38
+ Você define onde os projetos serão renderizados e a lib cuida da sincronização.
39
+
40
+ ---
41
+
42
+ ## 🔥 Antes vs Depois
43
+
44
+ ### ❌ Antes (manual)
45
+
46
+ ```html
47
+ <div>
48
+ <h2>Projetos</h2>
49
+ <ul>
50
+ <li>Projeto A</li>
51
+ <li>Projeto B</li>
52
+ </ul>
53
+ </div>
54
+ ```
55
+
56
+ ### ✅ Depois (automático)
57
+
58
+ ```js
59
+ import { initPortfolio } from 'snapport';
60
+
61
+ initPortfolio('seu-usuario-github', {
62
+ tag: 'port',
63
+ searchContainer: 'meu-search',
64
+ filtersContainer: 'meu-filters',
65
+ projectsContainer: 'meu-projects'
66
+ });
67
+ ```
68
+
69
+ ---
70
+
71
+ ## 🚀 Início rápido
72
+
73
+ ### Via npm
74
+
75
+ Instale o pacote:
36
76
 
37
77
  ```bash
38
78
  npm install snapport
39
79
  ```
40
80
 
41
- ```typescript
81
+ No seu arquivo JavaScript/TypeScript, importe o JS e o CSS:
82
+
83
+ ```js
42
84
  import { initPortfolio } from 'snapport';
85
+ import 'snapport/dist/style.css';
43
86
 
44
- initPortfolio('seu-usuario', {
45
- searchContainer: 'search-id',
46
- filtersContainer: 'filters-id',
47
- projectsContainer: 'projects-id'
87
+ initPortfolio('seu-usuario-github', {
88
+ tag: 'port',
89
+ searchContainer: 'meu-search',
90
+ filtersContainer: 'meu-filters',
91
+ projectsContainer: 'meu-projects'
48
92
  });
49
93
  ```
50
94
 
51
- ## 🎨 Personalização Visual
95
+ ### Via CDN (vanilla JS)
96
+
97
+ No seu HTML, inclua o CSS no `<head>` e o script no `<body>`:
98
+
99
+ ```html
100
+ <!-- No seu <head> -->
101
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/snapport/dist/style.css"/>
102
+
103
+ <!-- No final do <body> -->
104
+ <script type="module">
105
+ import { initPortfolio } from 'https://cdn.jsdelivr.net/npm/snapport/dist/snapport.js';
106
+
107
+ initPortfolio('seu-usuario-github', {
108
+ tag: 'port',
109
+ searchContainer: 'meu-search',
110
+ filtersContainer: 'meu-filters',
111
+ projectsContainer: 'meu-projects'
112
+ });
113
+ </script>
114
+ ```
52
115
 
53
- O Snapport é totalmente customizável via **CSS Variables**. Adapte as cores ao seu tema sem esforço:
116
+ ---
117
+
118
+ ## 🎨 Personalização
54
119
 
55
120
  ```css
56
121
  :root {
@@ -60,17 +125,24 @@ O Snapport é totalmente customizável via **CSS Variables**. Adapte as cores ao
60
125
  }
61
126
  ```
62
127
 
63
- ## 🛠️ Stacks Suportadas
128
+ ---
129
+
130
+ ## ⚙️ Características
64
131
 
65
- A lib reconhece automaticamente ícones e cores oficiais para diversas tecnologias:
66
- `React` `TypeScript` • `Node.js` • `Docker` • `Tailwind` • `Sass` • `Python` • `e muito mais...`
132
+ - Zero dependências em runtime
133
+ - Cache local (2h)
134
+ - Funciona com qualquer framework
135
+ - Baseado em GitHub Topics
136
+ - Leve e direto ao ponto
67
137
 
68
138
  ---
69
139
 
70
- ## 🤝 Contribuição
140
+ ## 🛠 Tecnologias suportadas
71
141
 
72
- Contribuições são o que fazem a comunidade open source um lugar incrível para aprender e criar. Confira nosso [Guia de Contribuição](https://guilhermegodoydev.github.io/snapport/projeto/contribuir.html) para começar.
142
+ React TypeScript Node.js Docker Tailwind Python etc
73
143
 
74
- ## 📄 Licença
144
+ ---
145
+
146
+ ## 🤝 Contribuição
75
147
 
76
- Distribuído sob a licença MIT. Veja [`LICENSE`](https://github.com/guilhermegodoydev/snapport/blob/main/LICENSE) para mais informações.
148
+ Veja a documentação: [CONTRIBUIR](https://guilhermegodoydev.github.io/snapport/projeto/contribuir.html)
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export declare function getPortProjects(username: string, tag?: string, options?: {
2
2
  forceRefresh?: boolean;
3
+ cache?: boolean;
3
4
  }): Promise<SanitizedRepo[]>;
4
5
 
5
6
  export declare function initPortfolio(username: string, config?: PortfolioConfig): Promise<PortfolioResponse>;
@@ -0,0 +1,242 @@
1
+ const C = {
2
+ react: { name: "React", icon: "react", color: "61DAFB" },
3
+ vue: { name: "Vue.js", icon: "vuedotjs", color: "4FC08D" },
4
+ angular: { name: "Angular", icon: "angular", color: "DD0031" },
5
+ nextjs: { name: "Next.js", icon: "nextdotjs", color: "000000" },
6
+ typescript: { name: "TypeScript", icon: "typescript", color: "3178C6" },
7
+ javascript: { name: "JavaScript", icon: "javascript", color: "F7DF1E" },
8
+ html5: { name: "HTML5", icon: "html5", color: "E34F26" },
9
+ css3: { name: "CSS3", icon: "css3", color: "1572B6" },
10
+ sass: { name: "Sass", icon: "sass", color: "CC6699" },
11
+ "tailwind-css": { name: "Tailwind", icon: "tailwindcss", color: "06B6D4" },
12
+ bootstrap: { name: "Bootstrap", icon: "bootstrap", color: "7952B3" },
13
+ "styled-components": { name: "Styled Components", icon: "styledcomponents", color: "DB7093" },
14
+ figma: { name: "Figma", icon: "figma", color: "F24E1E" },
15
+ nodejs: { name: "Node.js", icon: "nodedotjs", color: "339933" },
16
+ express: { name: "Express", icon: "express", color: "000000" },
17
+ nestjs: { name: "NestJS", icon: "nestjs", color: "E0234E" },
18
+ python: { name: "Python", icon: "python", color: "3776AB" },
19
+ java: { name: "Java", icon: "openjdk", color: "007396" },
20
+ php: { name: "PHP", icon: "php", color: "777BB4" },
21
+ postgresql: { name: "PostgreSQL", icon: "postgresql", color: "4169E1" },
22
+ mongodb: { name: "MongoDB", icon: "mongodb", color: "47A248" },
23
+ mysql: { name: "MySQL", icon: "mysql", color: "4479A1" },
24
+ firebase: { name: "Firebase", icon: "firebase", color: "FFCA28" },
25
+ prisma: { name: "Prisma", icon: "prisma", color: "2D3748" },
26
+ docker: { name: "Docker", icon: "docker", color: "2496ED" },
27
+ git: { name: "Git", icon: "git", color: "F05032" },
28
+ jest: { name: "Jest", icon: "jest", color: "C21325" },
29
+ "github-actions": { name: "GitHub Actions", icon: "githubactions", color: "2088FF" }
30
+ }, j = 7200 * 1e3, w = (t = []) => t.filter(
31
+ (e) => !!C[e?.toLowerCase().trim()]
32
+ ), S = (t) => {
33
+ const e = Array.isArray(t.topics) ? t.topics : [];
34
+ return {
35
+ id: t.id,
36
+ name: t.name ?? "Projeto sem nome",
37
+ description: t.description ?? "Sem descrição disponível",
38
+ htmlUrl: t.html_url,
39
+ topics: e,
40
+ stacks: w(e),
41
+ deployUrl: t.homepage ?? null
42
+ };
43
+ };
44
+ function $(t) {
45
+ try {
46
+ Object.keys(localStorage).forEach((e) => {
47
+ e.startsWith("gh_projects_") && e !== `gh_projects_${t}` && localStorage.removeItem(e);
48
+ });
49
+ } catch (e) {
50
+ console.warn("Erro ao limpar caches antigos:", e);
51
+ }
52
+ }
53
+ async function b(t, e = "port", r = {}) {
54
+ if (!t)
55
+ return console.error("[Snapport]: Username é obrigatório."), [];
56
+ const o = `gh_projects_${t}`, c = r.cache !== !1, n = r.forceRefresh === !0, a = c && !n;
57
+ try {
58
+ if ($(t), a) {
59
+ const y = localStorage.getItem(o);
60
+ if (y)
61
+ try {
62
+ const d = JSON.parse(y);
63
+ if (d && typeof d == "object" && Array.isArray(d.data) && typeof d.timestamp == "number" && Date.now() - d.timestamp < j)
64
+ return d.data;
65
+ } catch {
66
+ localStorage.removeItem(o);
67
+ }
68
+ }
69
+ const i = encodeURIComponent(`user:${t} topic:${e}`), s = await fetch(`https://api.github.com/search/repositories?q=${i}&sort=updated&order=desc`);
70
+ if (!s.ok)
71
+ throw new Error(`GitHub API error: ${s.status}`);
72
+ const l = ((await s.json()).items || []).map(S), p = {
73
+ data: l,
74
+ timestamp: Date.now()
75
+ };
76
+ return c && localStorage.setItem(o, JSON.stringify(p)), l;
77
+ } catch (i) {
78
+ console.error(`[Snapport]: Erro ao buscar dados de ${t}:`, i);
79
+ const s = localStorage.getItem(o);
80
+ if (s)
81
+ try {
82
+ return JSON.parse(s).data;
83
+ } catch {
84
+ localStorage.removeItem(o);
85
+ }
86
+ return [];
87
+ }
88
+ }
89
+ function u(t, e = 5e3) {
90
+ return new Promise((r, o) => {
91
+ const c = Date.now(), n = () => {
92
+ const a = document.getElementById(t);
93
+ if (a) return r(a);
94
+ if (Date.now() - c > e)
95
+ return o(`Snapport: container "${t}" não encontrado`);
96
+ requestAnimationFrame(n);
97
+ };
98
+ n();
99
+ });
100
+ }
101
+ function g(t) {
102
+ return Array.isArray(t.topics) ? t.topics : [];
103
+ }
104
+ const m = (t) => {
105
+ if (typeof t == "string") {
106
+ const e = document.getElementById(t);
107
+ return e || console.warn(
108
+ `[Snapport] Container "${t}" não encontrado. Verifique se o elemento existe no DOM antes de inicializar.`
109
+ ), e;
110
+ }
111
+ return t;
112
+ }, v = (t, e = null) => {
113
+ if (!e)
114
+ return `
115
+ <button class="ghp-filter-btn active" data-topic="all">
116
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
117
+ <rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/>
118
+ <rect width="7" height="7" x="14" y="14" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/>
119
+ </svg>
120
+ <p>Todos</p>
121
+ </button>
122
+ `;
123
+ const r = `https://cdn.simpleicons.org/${e.icon}/${e.color}`;
124
+ return `
125
+ <button class="ghp-filter-btn" data-topic="${t}" style="--tech-color: #${e.color}">
126
+ <img src="${r}" alt="${e.name}" loading="lazy">
127
+ <p>${e.name}</p>
128
+ </button>
129
+ `;
130
+ }, E = (t, e) => {
131
+ const r = `https://raw.githubusercontent.com/${e}/${t.name}/main/preview.png`, o = `https://opengraph.githubassets.com/${e}/${t.name}`, c = `https://placehold.co/640x360?text=${encodeURIComponent(t.name)}`, n = t.deployUrl ? `<a href="${t.deployUrl}" target="_blank" rel="noopener noreferrer">Acessar</a>` : "", a = t.htmlUrl ? `<a href="${t.htmlUrl}" target="_blank" rel="noopener noreferrer">Github</a>` : "";
132
+ return `
133
+ <div class="ghp-project-card">
134
+ <div class="ghp-img-container">
135
+ <img
136
+ src="${r}"
137
+ alt="Preview do projeto ${t.name}"
138
+ class="ghp-card-img"
139
+ loading="lazy"
140
+ onerror="if (this.src.includes('preview.png')) { this.src='${o}'; } else { this.onerror=null; this.src='${c}'; }"
141
+ >
142
+ </div>
143
+ <div class="ghp-card-content">
144
+ <div>
145
+ <h3>${t.name}</h3>
146
+ <p title="${t.description}">${t.description}</p>
147
+ </div>
148
+ <div class="ghp-card-links">
149
+ ${a}
150
+ ${n}
151
+ </div>
152
+ </div>
153
+ </div>
154
+ `;
155
+ };
156
+ function x(t, e = 6) {
157
+ const r = m(t);
158
+ r && (r.classList.toggle("ghp-projects-grid", !0), r.innerHTML = Array(e).fill('<div class="ghp-project-card ghp-skeleton ghp-skeleton-card"></div>').join(""));
159
+ }
160
+ function B(t, e) {
161
+ const r = m(e);
162
+ if (!r) return;
163
+ const o = t.flatMap((a) => g(a)), n = [...new Set(o)].reduce((a, i) => {
164
+ const s = C[i];
165
+ return s ? a + v(i, s) : a;
166
+ }, v("all"));
167
+ r.innerHTML = `<div class="ghp-filters-content">${n}</div>`;
168
+ }
169
+ function f(t, e, r = "", o) {
170
+ const c = m(e);
171
+ if (c) {
172
+ if (c.className = "ghp-projects-grid", c.style.minHeight = "auto", t.length === 0) {
173
+ c.innerHTML = '<p class="ghp-empty-msg">Nenhum projeto encontrado.</p>';
174
+ return;
175
+ }
176
+ c.innerHTML = t.map((n) => o ? o(n) : E(n, r)).join("");
177
+ }
178
+ }
179
+ function F(t) {
180
+ const e = m(t);
181
+ e && (e.innerHTML = `
182
+ <div class="ghp-search-container">
183
+ <svg class="ghp-search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
184
+ <circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
185
+ </svg>
186
+ <input type="text" id="gh-port-search" class="ghp-search-input" placeholder="Buscar projeto..." autocomplete="off">
187
+ </div>
188
+ `);
189
+ }
190
+ function k(t, e, r, o, c) {
191
+ const n = document.getElementById(t);
192
+ if (!n) {
193
+ console.warn(
194
+ `[Snapport] setupFilters falhou: container "${t}" não encontrado. Possíveis causas: renderFilters não foi chamado ou DOM ainda não está pronto.`
195
+ );
196
+ return;
197
+ }
198
+ n.addEventListener("click", (a) => {
199
+ const s = a.target.closest(".ghp-filter-btn");
200
+ if (!s) return;
201
+ const h = s.dataset.topic, l = h === "all" ? e : e.filter((p) => g(p).includes(h));
202
+ f(l, r, o, c), n.querySelectorAll(".ghp-filter-btn").forEach((p) => p.classList.remove("active")), s.classList.add("active");
203
+ });
204
+ }
205
+ function A(t, e, r, o) {
206
+ (() => {
207
+ const n = document.getElementById("gh-port-search");
208
+ return n ? (n.addEventListener("input", (a) => {
209
+ const s = a.target.value.toLowerCase(), h = t.filter(
210
+ (l) => l.name.toLowerCase().includes(s) || (l.description || "").toLowerCase().includes(s) || g(l).some((p) => p.toLowerCase().includes(s))
211
+ );
212
+ f(h, e, r, o);
213
+ }), !0) : !1;
214
+ })() || console.warn("[Snapport] Search input ainda não existe no DOM");
215
+ }
216
+ async function L(t, e = {
217
+ searchContainer: "search-cont",
218
+ filtersContainer: "filters-cont",
219
+ projectsContainer: "projects-cont"
220
+ }) {
221
+ try {
222
+ const r = e.tag || "port";
223
+ await Promise.all([
224
+ u(e.searchContainer),
225
+ u(e.filtersContainer),
226
+ u(e.projectsContainer)
227
+ ]), F(e.searchContainer), x(e.projectsContainer, 6);
228
+ const o = await b(t, r);
229
+ return B(o, e.filtersContainer), f(o, e.projectsContainer, t, e.customCardTemplate), k(e.filtersContainer, o, e.projectsContainer, t, e.customCardTemplate), A(o, e.projectsContainer, t, e.customCardTemplate), { projects: o, status: "success" };
230
+ } catch (r) {
231
+ console.error("Erro ao inicializar portfólio:", r);
232
+ const o = document.getElementById(e.projectsContainer);
233
+ return o && (o.innerHTML = '<p class="ghp-empty-msg">Erro ao carregar projetos.</p>'), { status: "error", message: r.message };
234
+ }
235
+ }
236
+ export {
237
+ b as getPortProjects,
238
+ L as initPortfolio,
239
+ B as renderFilters,
240
+ f as renderProjects,
241
+ F as renderSearchBar
242
+ };
@@ -0,0 +1,43 @@
1
+ (function(i,h){typeof exports=="object"&&typeof module<"u"?h(exports):typeof define=="function"&&define.amd?define(["exports"],h):(i=typeof globalThis<"u"?globalThis:i||self,h(i.SnapPort={}))})(this,(function(i){"use strict";const h={react:{name:"React",icon:"react",color:"61DAFB"},vue:{name:"Vue.js",icon:"vuedotjs",color:"4FC08D"},angular:{name:"Angular",icon:"angular",color:"DD0031"},nextjs:{name:"Next.js",icon:"nextdotjs",color:"000000"},typescript:{name:"TypeScript",icon:"typescript",color:"3178C6"},javascript:{name:"JavaScript",icon:"javascript",color:"F7DF1E"},html5:{name:"HTML5",icon:"html5",color:"E34F26"},css3:{name:"CSS3",icon:"css3",color:"1572B6"},sass:{name:"Sass",icon:"sass",color:"CC6699"},"tailwind-css":{name:"Tailwind",icon:"tailwindcss",color:"06B6D4"},bootstrap:{name:"Bootstrap",icon:"bootstrap",color:"7952B3"},"styled-components":{name:"Styled Components",icon:"styledcomponents",color:"DB7093"},figma:{name:"Figma",icon:"figma",color:"F24E1E"},nodejs:{name:"Node.js",icon:"nodedotjs",color:"339933"},express:{name:"Express",icon:"express",color:"000000"},nestjs:{name:"NestJS",icon:"nestjs",color:"E0234E"},python:{name:"Python",icon:"python",color:"3776AB"},java:{name:"Java",icon:"openjdk",color:"007396"},php:{name:"PHP",icon:"php",color:"777BB4"},postgresql:{name:"PostgreSQL",icon:"postgresql",color:"4169E1"},mongodb:{name:"MongoDB",icon:"mongodb",color:"47A248"},mysql:{name:"MySQL",icon:"mysql",color:"4479A1"},firebase:{name:"Firebase",icon:"firebase",color:"FFCA28"},prisma:{name:"Prisma",icon:"prisma",color:"2D3748"},docker:{name:"Docker",icon:"docker",color:"2496ED"},git:{name:"Git",icon:"git",color:"F05032"},jest:{name:"Jest",icon:"jest",color:"C21325"},"github-actions":{name:"GitHub Actions",icon:"githubactions",color:"2088FF"}},b=7200*1e3,E=(e=[])=>e.filter(t=>!!h[t?.toLowerCase().trim()]),B=e=>{const t=Array.isArray(e.topics)?e.topics:[];return{id:e.id,name:e.name??"Projeto sem nome",description:e.description??"Sem descrição disponível",htmlUrl:e.html_url,topics:t,stacks:E(t),deployUrl:e.homepage??null}};function F(e){try{Object.keys(localStorage).forEach(t=>{t.startsWith("gh_projects_")&&t!==`gh_projects_${e}`&&localStorage.removeItem(t)})}catch(t){console.warn("Erro ao limpar caches antigos:",t)}}async function j(e,t="port",r={}){if(!e)return console.error("[Snapport]: Username é obrigatório."),[];const o=`gh_projects_${e}`,c=r.cache!==!1,n=r.forceRefresh===!0,a=c&&!n;try{if(F(e),a){const $=localStorage.getItem(o);if($)try{const m=JSON.parse($);if(m&&typeof m=="object"&&Array.isArray(m.data)&&typeof m.timestamp=="number"&&Date.now()-m.timestamp<b)return m.data}catch{localStorage.removeItem(o)}}const l=encodeURIComponent(`user:${e} topic:${t}`),s=await fetch(`https://api.github.com/search/repositories?q=${l}&sort=updated&order=desc`);if(!s.ok)throw new Error(`GitHub API error: ${s.status}`);const p=((await s.json()).items||[]).map(B),d={data:p,timestamp:Date.now()};return c&&localStorage.setItem(o,JSON.stringify(d)),p}catch(l){console.error(`[Snapport]: Erro ao buscar dados de ${e}:`,l);const s=localStorage.getItem(o);if(s)try{return JSON.parse(s).data}catch{localStorage.removeItem(o)}return[]}}function y(e,t=5e3){return new Promise((r,o)=>{const c=Date.now(),n=()=>{const a=document.getElementById(e);if(a)return r(a);if(Date.now()-c>t)return o(`Snapport: container "${e}" não encontrado`);requestAnimationFrame(n)};n()})}function v(e){return Array.isArray(e.topics)?e.topics:[]}const g=e=>{if(typeof e=="string"){const t=document.getElementById(e);return t||console.warn(`[Snapport] Container "${e}" não encontrado. Verifique se o elemento existe no DOM antes de inicializar.`),t}return e},C=(e,t=null)=>{if(!t)return`
2
+ <button class="ghp-filter-btn active" data-topic="all">
3
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
4
+ <rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/>
5
+ <rect width="7" height="7" x="14" y="14" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/>
6
+ </svg>
7
+ <p>Todos</p>
8
+ </button>
9
+ `;const r=`https://cdn.simpleicons.org/${t.icon}/${t.color}`;return`
10
+ <button class="ghp-filter-btn" data-topic="${e}" style="--tech-color: #${t.color}">
11
+ <img src="${r}" alt="${t.name}" loading="lazy">
12
+ <p>${t.name}</p>
13
+ </button>
14
+ `},P=(e,t)=>{const r=`https://raw.githubusercontent.com/${t}/${e.name}/main/preview.png`,o=`https://opengraph.githubassets.com/${t}/${e.name}`,c=`https://placehold.co/640x360?text=${encodeURIComponent(e.name)}`,n=e.deployUrl?`<a href="${e.deployUrl}" target="_blank" rel="noopener noreferrer">Acessar</a>`:"",a=e.htmlUrl?`<a href="${e.htmlUrl}" target="_blank" rel="noopener noreferrer">Github</a>`:"";return`
15
+ <div class="ghp-project-card">
16
+ <div class="ghp-img-container">
17
+ <img
18
+ src="${r}"
19
+ alt="Preview do projeto ${e.name}"
20
+ class="ghp-card-img"
21
+ loading="lazy"
22
+ onerror="if (this.src.includes('preview.png')) { this.src='${o}'; } else { this.onerror=null; this.src='${c}'; }"
23
+ >
24
+ </div>
25
+ <div class="ghp-card-content">
26
+ <div>
27
+ <h3>${e.name}</h3>
28
+ <p title="${e.description}">${e.description}</p>
29
+ </div>
30
+ <div class="ghp-card-links">
31
+ ${a}
32
+ ${n}
33
+ </div>
34
+ </div>
35
+ </div>
36
+ `};function T(e,t=6){const r=g(e);r&&(r.classList.toggle("ghp-projects-grid",!0),r.innerHTML=Array(t).fill('<div class="ghp-project-card ghp-skeleton ghp-skeleton-card"></div>').join(""))}function w(e,t){const r=g(t);if(!r)return;const o=e.flatMap(a=>v(a)),n=[...new Set(o)].reduce((a,l)=>{const s=h[l];return s?a+C(l,s):a},C("all"));r.innerHTML=`<div class="ghp-filters-content">${n}</div>`}function f(e,t,r="",o){const c=g(t);if(c){if(c.className="ghp-projects-grid",c.style.minHeight="auto",e.length===0){c.innerHTML='<p class="ghp-empty-msg">Nenhum projeto encontrado.</p>';return}c.innerHTML=e.map(n=>o?o(n):P(n,r)).join("")}}function S(e){const t=g(e);t&&(t.innerHTML=`
37
+ <div class="ghp-search-container">
38
+ <svg class="ghp-search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
39
+ <circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
40
+ </svg>
41
+ <input type="text" id="gh-port-search" class="ghp-search-input" placeholder="Buscar projeto..." autocomplete="off">
42
+ </div>
43
+ `)}function x(e,t,r,o,c){const n=document.getElementById(e);if(!n){console.warn(`[Snapport] setupFilters falhou: container "${e}" não encontrado. Possíveis causas: renderFilters não foi chamado ou DOM ainda não está pronto.`);return}n.addEventListener("click",a=>{const s=a.target.closest(".ghp-filter-btn");if(!s)return;const u=s.dataset.topic,p=u==="all"?t:t.filter(d=>v(d).includes(u));f(p,r,o,c),n.querySelectorAll(".ghp-filter-btn").forEach(d=>d.classList.remove("active")),s.classList.add("active")})}function k(e,t,r,o){(()=>{const n=document.getElementById("gh-port-search");return n?(n.addEventListener("input",a=>{const s=a.target.value.toLowerCase(),u=e.filter(p=>p.name.toLowerCase().includes(s)||(p.description||"").toLowerCase().includes(s)||v(p).some(d=>d.toLowerCase().includes(s)));f(u,t,r,o)}),!0):!1})()||console.warn("[Snapport] Search input ainda não existe no DOM")}async function A(e,t={searchContainer:"search-cont",filtersContainer:"filters-cont",projectsContainer:"projects-cont"}){try{const r=t.tag||"port";await Promise.all([y(t.searchContainer),y(t.filtersContainer),y(t.projectsContainer)]),S(t.searchContainer),T(t.projectsContainer,6);const o=await j(e,r);return w(o,t.filtersContainer),f(o,t.projectsContainer,e,t.customCardTemplate),x(t.filtersContainer,o,t.projectsContainer,e,t.customCardTemplate),k(o,t.projectsContainer,e,t.customCardTemplate),{projects:o,status:"success"}}catch(r){console.error("Erro ao inicializar portfólio:",r);const o=document.getElementById(t.projectsContainer);return o&&(o.innerHTML='<p class="ghp-empty-msg">Erro ao carregar projetos.</p>'),{status:"error",message:r.message}}}i.getPortProjects=j,i.initPortfolio=A,i.renderFilters=w,i.renderProjects=f,i.renderSearchBar=S,Object.defineProperty(i,Symbol.toStringTag,{value:"Module"})}));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "snapport",
3
3
  "private": false,
4
- "version": "1.1.1",
4
+ "version": "1.2.1",
5
5
  "keywords": [
6
6
  "portfolio",
7
7
  "github-api",
@@ -16,16 +16,16 @@
16
16
  "files": [
17
17
  "dist"
18
18
  ],
19
- "main": "./dist/snap-port.umd.cjs",
20
- "module": "./dist/snap-port.js",
19
+ "main": "./dist/snapport.umd.cjs",
20
+ "module": "./dist/snapport.js",
21
21
  "types": "./dist/index.d.ts",
22
22
  "exports": {
23
23
  ".": {
24
24
  "types": "./dist/index.d.ts",
25
- "import": "./dist/snap-port.js",
26
- "require": "./dist/snap-port.umd.cjs"
25
+ "import": "./dist/snapport.js",
26
+ "require": "./dist/snapport.umd.cjs"
27
27
  },
28
- "./style.css": "./dist/snap-port.css"
28
+ "./style.css": "./dist/snapport.css"
29
29
  },
30
30
  "scripts": {
31
31
  "dev": "vite playground",
@@ -34,7 +34,8 @@
34
34
  "docs:dev": "vitepress dev docs",
35
35
  "docs:build": "vitepress build docs",
36
36
  "docs:preview": "vitepress preview docs",
37
- "docs:deploy": "npm run docs:build && gh-pages -d docs/.vitepress/dist"
37
+ "docs:deploy": "npm run docs:build && gh-pages -d docs/.vitepress/dist",
38
+ "prepublishOnly": "npm run build"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@types/node": "^25.6.0",
package/dist/snap-port.js DELETED
@@ -1,195 +0,0 @@
1
- const f = {
2
- react: { name: "React", icon: "react", color: "61DAFB" },
3
- vue: { name: "Vue.js", icon: "vuedotjs", color: "4FC08D" },
4
- angular: { name: "Angular", icon: "angular", color: "DD0031" },
5
- nextjs: { name: "Next.js", icon: "nextdotjs", color: "000000" },
6
- typescript: { name: "TypeScript", icon: "typescript", color: "3178C6" },
7
- javascript: { name: "JavaScript", icon: "javascript", color: "F7DF1E" },
8
- html5: { name: "HTML5", icon: "html5", color: "E34F26" },
9
- css3: { name: "CSS3", icon: "css3", color: "1572B6" },
10
- sass: { name: "Sass", icon: "sass", color: "CC6699" },
11
- "tailwind-css": { name: "Tailwind", icon: "tailwindcss", color: "06B6D4" },
12
- bootstrap: { name: "Bootstrap", icon: "bootstrap", color: "7952B3" },
13
- "styled-components": { name: "Styled Components", icon: "styledcomponents", color: "DB7093" },
14
- figma: { name: "Figma", icon: "figma", color: "F24E1E" },
15
- nodejs: { name: "Node.js", icon: "nodedotjs", color: "339933" },
16
- express: { name: "Express", icon: "express", color: "000000" },
17
- nestjs: { name: "NestJS", icon: "nestjs", color: "E0234E" },
18
- python: { name: "Python", icon: "python", color: "3776AB" },
19
- java: { name: "Java", icon: "openjdk", color: "007396" },
20
- php: { name: "PHP", icon: "php", color: "777BB4" },
21
- postgresql: { name: "PostgreSQL", icon: "postgresql", color: "4169E1" },
22
- mongodb: { name: "MongoDB", icon: "mongodb", color: "47A248" },
23
- mysql: { name: "MySQL", icon: "mysql", color: "4479A1" },
24
- firebase: { name: "Firebase", icon: "firebase", color: "FFCA28" },
25
- prisma: { name: "Prisma", icon: "prisma", color: "2D3748" },
26
- docker: { name: "Docker", icon: "docker", color: "2496ED" },
27
- git: { name: "Git", icon: "git", color: "F05032" },
28
- jest: { name: "Jest", icon: "jest", color: "C21325" },
29
- "github-actions": { name: "GitHub Actions", icon: "githubactions", color: "2088FF" }
30
- }, u = typeof process < "u" && process.env.NODE_ENV !== "production" || typeof import.meta < "u" && import.meta.env.MODE !== "production", v = 7200 * 1e3, C = (t) => t ? t.filter((o) => !!f[o.toLocaleLowerCase().trim()]) : [], j = (t) => ({
31
- id: t.id,
32
- name: t.name ?? "Projeto sem nome",
33
- description: t.description ?? "Sem descrição disponível",
34
- htmlUrl: t.html_url,
35
- topics: Array.isArray(t.topics) ? t.topics : [],
36
- stacks: Array.isArray(t.topics) ? C(t.topics) : [],
37
- deployUrl: t.homepage ?? null
38
- });
39
- function w(t) {
40
- try {
41
- Object.keys(localStorage).forEach((e) => {
42
- e.startsWith("gh_projects_") && !e.includes(t) && localStorage.removeItem(e);
43
- });
44
- } catch (e) {
45
- console.warn("Erro ao limpar caches antigos:", e);
46
- }
47
- }
48
- async function $(t, e = "port", o = {}) {
49
- if (!t)
50
- return console.error("GitHubPortfolio: Username é obrigatório."), [];
51
- const r = `gh_projects_${t}`, c = u || o.forceRefresh;
52
- try {
53
- if (w(t), c)
54
- u && console.info(`[GitHubPortfolio]: Dev mode detectado. Cache ignorado para ${t}.`);
55
- else {
56
- const p = localStorage.getItem(r);
57
- if (p) {
58
- const { data: d, timestamp: y } = JSON.parse(p);
59
- if (Date.now() - y < v)
60
- return d;
61
- }
62
- }
63
- const n = encodeURIComponent(`user:${t} topic:${e}`), s = await fetch(`https://api.github.com/search/repositories?q=${n}&sort=updated&order=desc`);
64
- if (!s.ok)
65
- throw new Error(`GitHub API error: ${s.status}`);
66
- const a = ((await s.json()).items || []).map(j), l = {
67
- data: a,
68
- timestamp: Date.now()
69
- };
70
- return localStorage.setItem(r, JSON.stringify(l)), a;
71
- } catch (n) {
72
- console.error(`GitHubPortfolio: Erro ao buscar dados de ${t}:`, n);
73
- const s = localStorage.getItem(r);
74
- return s ? (console.warn("GitHubPortfolio: Usando cache expirado devido a erro de rede."), JSON.parse(s).data) : [];
75
- }
76
- }
77
- const h = (t) => typeof t == "string" ? document.getElementById(t) : t, g = (t, e = null) => {
78
- if (!e)
79
- return `
80
- <button class="ghp-filter-btn active" data-topic="all">
81
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
82
- <rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/>
83
- <rect width="7" height="7" x="14" y="14" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/>
84
- </svg>
85
- <p>Todos</p>
86
- </button>
87
- `;
88
- const o = `https://cdn.simpleicons.org/${e.icon}/${e.color}`;
89
- return `
90
- <button class="ghp-filter-btn" data-topic="${t}" style="--tech-color: #${e.color}">
91
- <img src="${o}" alt="${e.name}" loading="lazy">
92
- <p>${e.name}</p>
93
- </button>
94
- `;
95
- }, b = (t, e) => {
96
- const o = `https://raw.githubusercontent.com/${e}/${t.name}/main/preview.png`, r = `https://opengraph.githubassets.com/${e}/${t.name}`, c = `https://placehold.co/640x360?text=${encodeURIComponent(t.name)}`, n = t.deployUrl ? `<a href="${t.deployUrl}" target="_blank" rel="noopener noreferrer">Acessar</a>` : "", s = t.htmlUrl ? `<a href="${t.htmlUrl}" target="_blank" rel="noopener noreferrer">Github</a>` : "";
97
- return `
98
- <div class="ghp-project-card">
99
- <div class="ghp-img-container">
100
- <img
101
- src="${o}"
102
- alt="Preview do projeto ${t.name}"
103
- class="ghp-card-img"
104
- loading="lazy"
105
- onerror="if (this.src.includes('preview.png')) { this.src='${r}'; } else { this.onerror=null; this.src='${c}'; }"
106
- >
107
- </div>
108
- <div class="ghp-card-content">
109
- <div>
110
- <h3>${t.name}</h3>
111
- <p title="${t.description}">${t.description}</p>
112
- </div>
113
- <div class="ghp-card-links">
114
- ${s}
115
- ${n}
116
- </div>
117
- </div>
118
- </div>
119
- `;
120
- };
121
- function E(t, e = 6) {
122
- const o = h(t);
123
- o && (o.className = "ghp-projects-grid", o.innerHTML = Array(e).fill('<div class="ghp-project-card ghp-skeleton ghp-skeleton-card"></div>').join(""));
124
- }
125
- function S(t, e) {
126
- const o = h(e);
127
- if (!o) return;
128
- const r = t.flatMap((s) => s.topics || []), n = [...new Set(r)].reduce((s, i) => {
129
- const a = f[i];
130
- return a ? s + g(i, a) : s;
131
- }, g("all"));
132
- o.innerHTML = `<div class="ghp-filters-content">${n}</div>`;
133
- }
134
- function m(t, e, o = "", r) {
135
- const c = h(e);
136
- if (c) {
137
- if (c.className = "ghp-projects-grid", c.style.minHeight = "auto", t.length === 0) {
138
- c.innerHTML = '<p class="ghp-empty-msg">Nenhum projeto encontrado.</p>';
139
- return;
140
- }
141
- c.innerHTML = t.map((n) => r ? r(n) : b(n, o)).join("");
142
- }
143
- }
144
- function x(t) {
145
- const e = h(t);
146
- e && (e.innerHTML = `
147
- <div class="ghp-search-container">
148
- <svg class="ghp-search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
149
- <circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
150
- </svg>
151
- <input type="text" id="gh-port-search" class="ghp-search-input" placeholder="Buscar projeto..." autocomplete="off">
152
- </div>
153
- `);
154
- }
155
- function k(t, e, o, r, c) {
156
- const n = document.getElementById(t);
157
- n && n.addEventListener("click", (s) => {
158
- const a = s.target.closest(".ghp-filter-btn");
159
- if (!a) return;
160
- const l = a.dataset.topic, p = l === "all" ? e : e.filter((d) => d.topics.includes(l));
161
- m(p, o, r, c), n.querySelectorAll(".ghp-filter-btn").forEach((d) => d.classList.remove("active")), a.classList.add("active");
162
- });
163
- }
164
- function B(t, e, o, r) {
165
- const c = document.getElementById("gh-port-search");
166
- c && c.addEventListener("input", (n) => {
167
- const i = n.target.value.toLowerCase(), a = t.filter(
168
- (l) => l.name.toLowerCase().includes(i) || (l.description || "").toLowerCase().includes(i) || l.topics.some((p) => p.toLowerCase().includes(i))
169
- );
170
- m(a, e, o, r);
171
- });
172
- }
173
- async function L(t, e = {
174
- searchContainer: "search-cont",
175
- filtersContainer: "filters-cont",
176
- projectsContainer: "projects-cont"
177
- }) {
178
- try {
179
- const o = e.tag || "port";
180
- x(e.searchContainer), E(e.projectsContainer, 6);
181
- const r = await $(t, o);
182
- return S(r, e.filtersContainer), m(r, e.projectsContainer, t, e.customCardTemplate), k(e.filtersContainer, r, e.projectsContainer, t, e.customCardTemplate), B(r, e.projectsContainer, t, e.customCardTemplate), { projects: r, status: "success" };
183
- } catch (o) {
184
- console.error("Erro ao inicializar portfólio:", o);
185
- const r = document.getElementById(e.projectsContainer);
186
- return r && (r.innerHTML = '<p class="ghp-empty-msg">Erro ao carregar projetos.</p>'), { status: "error", message: o.message };
187
- }
188
- }
189
- export {
190
- $ as getPortProjects,
191
- L as initPortfolio,
192
- S as renderFilters,
193
- m as renderProjects,
194
- x as renderSearchBar
195
- };
@@ -1,43 +0,0 @@
1
- (function(a,p){typeof exports=="object"&&typeof module<"u"?p(exports):typeof define=="function"&&define.amd?define(["exports"],p):(a=typeof globalThis<"u"?globalThis:a||self,p(a.SnapPort={}))})(this,(function(a){"use strict";var p=typeof document<"u"?document.currentScript:null;const f={react:{name:"React",icon:"react",color:"61DAFB"},vue:{name:"Vue.js",icon:"vuedotjs",color:"4FC08D"},angular:{name:"Angular",icon:"angular",color:"DD0031"},nextjs:{name:"Next.js",icon:"nextdotjs",color:"000000"},typescript:{name:"TypeScript",icon:"typescript",color:"3178C6"},javascript:{name:"JavaScript",icon:"javascript",color:"F7DF1E"},html5:{name:"HTML5",icon:"html5",color:"E34F26"},css3:{name:"CSS3",icon:"css3",color:"1572B6"},sass:{name:"Sass",icon:"sass",color:"CC6699"},"tailwind-css":{name:"Tailwind",icon:"tailwindcss",color:"06B6D4"},bootstrap:{name:"Bootstrap",icon:"bootstrap",color:"7952B3"},"styled-components":{name:"Styled Components",icon:"styledcomponents",color:"DB7093"},figma:{name:"Figma",icon:"figma",color:"F24E1E"},nodejs:{name:"Node.js",icon:"nodedotjs",color:"339933"},express:{name:"Express",icon:"express",color:"000000"},nestjs:{name:"NestJS",icon:"nestjs",color:"E0234E"},python:{name:"Python",icon:"python",color:"3776AB"},java:{name:"Java",icon:"openjdk",color:"007396"},php:{name:"PHP",icon:"php",color:"777BB4"},postgresql:{name:"PostgreSQL",icon:"postgresql",color:"4169E1"},mongodb:{name:"MongoDB",icon:"mongodb",color:"47A248"},mysql:{name:"MySQL",icon:"mysql",color:"4479A1"},firebase:{name:"Firebase",icon:"firebase",color:"FFCA28"},prisma:{name:"Prisma",icon:"prisma",color:"2D3748"},docker:{name:"Docker",icon:"docker",color:"2496ED"},git:{name:"Git",icon:"git",color:"F05032"},jest:{name:"Jest",icon:"jest",color:"C21325"},"github-actions":{name:"GitHub Actions",icon:"githubactions",color:"2088FF"}},y=typeof process<"u"&&process.env.NODE_ENV!=="production"||typeof{url:typeof document>"u"&&typeof location>"u"?require("url").pathToFileURL(__filename).href:typeof document>"u"?location.href:p&&p.tagName.toUpperCase()==="SCRIPT"&&p.src||new URL("snap-port.umd.cjs",document.baseURI).href}<"u"&&(void 0).MODE!=="production",w=7200*1e3,S=e=>e?e.filter(o=>!!f[o.toLocaleLowerCase().trim()]):[],$=e=>({id:e.id,name:e.name??"Projeto sem nome",description:e.description??"Sem descrição disponível",htmlUrl:e.html_url,topics:Array.isArray(e.topics)?e.topics:[],stacks:Array.isArray(e.topics)?S(e.topics):[],deployUrl:e.homepage??null});function E(e){try{Object.keys(localStorage).forEach(t=>{t.startsWith("gh_projects_")&&!t.includes(e)&&localStorage.removeItem(t)})}catch(t){console.warn("Erro ao limpar caches antigos:",t)}}async function v(e,t="port",o={}){if(!e)return console.error("GitHubPortfolio: Username é obrigatório."),[];const r=`gh_projects_${e}`,s=y||o.forceRefresh;try{if(E(e),s)y&&console.info(`[GitHubPortfolio]: Dev mode detectado. Cache ignorado para ${e}.`);else{const u=localStorage.getItem(r);if(u){const{data:h,timestamp:x}=JSON.parse(u);if(Date.now()-x<w)return h}}const n=encodeURIComponent(`user:${e} topic:${t}`),c=await fetch(`https://api.github.com/search/repositories?q=${n}&sort=updated&order=desc`);if(!c.ok)throw new Error(`GitHub API error: ${c.status}`);const i=((await c.json()).items||[]).map($),d={data:i,timestamp:Date.now()};return localStorage.setItem(r,JSON.stringify(d)),i}catch(n){console.error(`GitHubPortfolio: Erro ao buscar dados de ${e}:`,n);const c=localStorage.getItem(r);return c?(console.warn("GitHubPortfolio: Usando cache expirado devido a erro de rede."),JSON.parse(c).data):[]}}const m=e=>typeof e=="string"?document.getElementById(e):e,C=(e,t=null)=>{if(!t)return`
2
- <button class="ghp-filter-btn active" data-topic="all">
3
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
4
- <rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/>
5
- <rect width="7" height="7" x="14" y="14" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/>
6
- </svg>
7
- <p>Todos</p>
8
- </button>
9
- `;const o=`https://cdn.simpleicons.org/${t.icon}/${t.color}`;return`
10
- <button class="ghp-filter-btn" data-topic="${e}" style="--tech-color: #${t.color}">
11
- <img src="${o}" alt="${t.name}" loading="lazy">
12
- <p>${t.name}</p>
13
- </button>
14
- `},P=(e,t)=>{const o=`https://raw.githubusercontent.com/${t}/${e.name}/main/preview.png`,r=`https://opengraph.githubassets.com/${t}/${e.name}`,s=`https://placehold.co/640x360?text=${encodeURIComponent(e.name)}`,n=e.deployUrl?`<a href="${e.deployUrl}" target="_blank" rel="noopener noreferrer">Acessar</a>`:"",c=e.htmlUrl?`<a href="${e.htmlUrl}" target="_blank" rel="noopener noreferrer">Github</a>`:"";return`
15
- <div class="ghp-project-card">
16
- <div class="ghp-img-container">
17
- <img
18
- src="${o}"
19
- alt="Preview do projeto ${e.name}"
20
- class="ghp-card-img"
21
- loading="lazy"
22
- onerror="if (this.src.includes('preview.png')) { this.src='${r}'; } else { this.onerror=null; this.src='${s}'; }"
23
- >
24
- </div>
25
- <div class="ghp-card-content">
26
- <div>
27
- <h3>${e.name}</h3>
28
- <p title="${e.description}">${e.description}</p>
29
- </div>
30
- <div class="ghp-card-links">
31
- ${c}
32
- ${n}
33
- </div>
34
- </div>
35
- </div>
36
- `};function T(e,t=6){const o=m(e);o&&(o.className="ghp-projects-grid",o.innerHTML=Array(t).fill('<div class="ghp-project-card ghp-skeleton ghp-skeleton-card"></div>').join(""))}function j(e,t){const o=m(t);if(!o)return;const r=e.flatMap(c=>c.topics||[]),n=[...new Set(r)].reduce((c,l)=>{const i=f[l];return i?c+C(l,i):c},C("all"));o.innerHTML=`<div class="ghp-filters-content">${n}</div>`}function g(e,t,o="",r){const s=m(t);if(s){if(s.className="ghp-projects-grid",s.style.minHeight="auto",e.length===0){s.innerHTML='<p class="ghp-empty-msg">Nenhum projeto encontrado.</p>';return}s.innerHTML=e.map(n=>r?r(n):P(n,o)).join("")}}function b(e){const t=m(e);t&&(t.innerHTML=`
37
- <div class="ghp-search-container">
38
- <svg class="ghp-search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
39
- <circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
40
- </svg>
41
- <input type="text" id="gh-port-search" class="ghp-search-input" placeholder="Buscar projeto..." autocomplete="off">
42
- </div>
43
- `)}function k(e,t,o,r,s){const n=document.getElementById(e);n&&n.addEventListener("click",c=>{const i=c.target.closest(".ghp-filter-btn");if(!i)return;const d=i.dataset.topic,u=d==="all"?t:t.filter(h=>h.topics.includes(d));g(u,o,r,s),n.querySelectorAll(".ghp-filter-btn").forEach(h=>h.classList.remove("active")),i.classList.add("active")})}function B(e,t,o,r){const s=document.getElementById("gh-port-search");s&&s.addEventListener("input",n=>{const l=n.target.value.toLowerCase(),i=e.filter(d=>d.name.toLowerCase().includes(l)||(d.description||"").toLowerCase().includes(l)||d.topics.some(u=>u.toLowerCase().includes(l)));g(i,t,o,r)})}async function L(e,t={searchContainer:"search-cont",filtersContainer:"filters-cont",projectsContainer:"projects-cont"}){try{const o=t.tag||"port";b(t.searchContainer),T(t.projectsContainer,6);const r=await v(e,o);return j(r,t.filtersContainer),g(r,t.projectsContainer,e,t.customCardTemplate),k(t.filtersContainer,r,t.projectsContainer,e,t.customCardTemplate),B(r,t.projectsContainer,e,t.customCardTemplate),{projects:r,status:"success"}}catch(o){console.error("Erro ao inicializar portfólio:",o);const r=document.getElementById(t.projectsContainer);return r&&(r.innerHTML='<p class="ghp-empty-msg">Erro ao carregar projetos.</p>'),{status:"error",message:o.message}}}a.getPortProjects=v,a.initPortfolio=L,a.renderFilters=j,a.renderProjects=g,a.renderSearchBar=b,Object.defineProperty(a,Symbol.toStringTag,{value:"Module"})}));
File without changes