snapport 1.0.1 → 1.1.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 +42 -96
- package/dist/index.d.ts +4 -1
- package/dist/snap-port.js +89 -85
- package/dist/snap-port.umd.cjs +6 -6
- package/package.json +21 -5
- package/README.pt-br.md +0 -137
package/README.md
CHANGED
|
@@ -1,130 +1,76 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<
|
|
2
|
+
<img src="https://github.com/guilhermegodoydev/snapport/blob/main/preview.png" width="300" height="300" style="border-radius: 50%" alt="Logo" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
+
<h1 align="center">Snapport</h1>
|
|
6
|
+
|
|
5
7
|
<p align="center">
|
|
6
|
-
<
|
|
7
|
-
<img src="https://img.shields.io/npm/v/snapport?style=for-the-badge" alt="NPM Version">
|
|
8
|
-
<img src="https://img.shields.io/github/license/guilhermegodoydev/snapport?style=for-the-badge" alt="License">
|
|
9
|
-
<img src="https://img.shields.io/bundlephobia/min/snapport?style=for-the-badge" alt="Bundle Size">
|
|
8
|
+
<strong>Seu portfólio alimentado automaticamente pelos tópicos do seu GitHub.</strong>
|
|
10
9
|
</p>
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
## 🛠 Technical Features
|
|
21
|
-
|
|
22
|
-
### 1. Project Selection & Stack Control
|
|
23
|
-
Snap-Port offers full control over what is displayed and how technologies are categorized:
|
|
24
|
-
|
|
25
|
-
- **Discovery Tag**: By default, the library looks for repositories tagged with `port`, but you can define any custom tag during initialization.
|
|
26
|
-
- **Stack Filters (Topics)**: For automatic filters and search to work correctly, list your technologies (e.g., `react`, `nodejs`, `css`) in your GitHub repository topics.
|
|
27
|
-
- **Why avoid automatic "Language"?**: Snap-Port ignores the default GitHub `language` field to let you highlight the actual stack. This prevents a React project from being labeled merely as "HTML" or "JavaScript" due to build files, ensuring the filter reflects the real project stack.
|
|
28
|
-
|
|
29
|
-
### 2. Intelligent Image Management
|
|
30
|
-
Since the GitHub API does not return direct preview image links, Snap-Port implements an **automatic generation logic**:
|
|
31
|
-
|
|
32
|
-
To ensure each project has its own image, follow these rules:
|
|
33
|
-
- **Preview File**: Create a file named `preview.png` in the root of your repository.
|
|
34
|
-
- **Note**: The filename must be exactly `preview.png` (case-sensitive).
|
|
35
|
-
|
|
36
|
-
If the file is missing or a loading error occurs (such as Rate Limits), the library executes a **cascading fallback strategy**:
|
|
37
|
-
1. **GitHub Open Graph**: Attempts to load the dynamic social card generated by GitHub.
|
|
38
|
-
2. **Safety Placeholder**: If the request is blocked, it generates a neutral card with the project name via `placehold.co`.
|
|
11
|
+
<p align="center">
|
|
12
|
+
<img src="https://img.shields.io/npm/v/snapport" alt="NPM Version">
|
|
13
|
+
<img src="https://img.shields.io/bundlephobia/minzip/snapport" alt="Bundle Size">
|
|
14
|
+
<img src="https://img.shields.io/npm/l/snapport" alt="License">
|
|
15
|
+
</p>
|
|
39
16
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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>
|
|
23
|
+
</p>
|
|
44
24
|
|
|
45
25
|
---
|
|
46
26
|
|
|
47
|
-
## 💡
|
|
27
|
+
## 💡 Por que Snapport?
|
|
48
28
|
|
|
49
|
-
|
|
50
|
-
- **Live Demo (Deploy)**: The "Acessar" (Visit) button only appears if the **"Homepage"** field is filled in your GitHub repository settings.
|
|
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.
|
|
51
30
|
|
|
52
|
-
|
|
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.
|
|
53
34
|
|
|
54
|
-
##
|
|
35
|
+
## 🚀 Início Rápido
|
|
55
36
|
|
|
56
|
-
### Via NPM
|
|
57
37
|
```bash
|
|
58
38
|
npm install snapport
|
|
59
39
|
```
|
|
60
40
|
|
|
61
|
-
|
|
62
|
-
|
|
41
|
+
```typescript
|
|
42
|
+
import { initPortfolio } from 'snapport';
|
|
63
43
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<script type="module">
|
|
70
|
-
import { initPortfolio } from 'https://cdn.jsdelivr.net';
|
|
71
|
-
|
|
72
|
-
initPortfolio('your-username', {
|
|
73
|
-
tag: 'port', // Optional: defaults to 'port'
|
|
74
|
-
searchContainer: 'id-search', // Search container ID
|
|
75
|
-
filtersContainer: 'id-filters', // Filters container ID
|
|
76
|
-
projectsContainer: 'id-projects' // Projects grid container ID
|
|
77
|
-
});
|
|
78
|
-
</script>
|
|
44
|
+
initPortfolio('seu-usuario', {
|
|
45
|
+
searchContainer: 'search-id',
|
|
46
|
+
filtersContainer: 'filters-id',
|
|
47
|
+
projectsContainer: 'projects-id'
|
|
48
|
+
});
|
|
79
49
|
```
|
|
80
50
|
|
|
81
|
-
|
|
51
|
+
## 🎨 Personalização Visual
|
|
82
52
|
|
|
83
|
-
|
|
84
|
-
Adapt the UI to your theme by overriding these variables in your global CSS:
|
|
53
|
+
O Snapport é totalmente customizável via **CSS Variables**. Adapte as cores ao seu tema sem esforço:
|
|
85
54
|
|
|
86
55
|
```css
|
|
87
56
|
:root {
|
|
88
|
-
--ghp-accent: #
|
|
89
|
-
--ghp-bg: #ffffff;
|
|
90
|
-
--ghp-text: #333;
|
|
91
|
-
--ghp-text-light: #666; /* Descriptions and secondary text */
|
|
92
|
-
--ghp-border: rgba(226, 226, 228, 0.8); /* Borders */
|
|
93
|
-
--ghp-shadow: rgba(0, 0, 0, 0.1); /* Card shadows */
|
|
57
|
+
--ghp-accent: #3178C6;
|
|
58
|
+
--ghp-bg: #ffffff;
|
|
59
|
+
--ghp-text: #333;
|
|
94
60
|
}
|
|
95
61
|
```
|
|
96
62
|
|
|
97
|
-
|
|
63
|
+
## 🛠️ Stacks Suportadas
|
|
98
64
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
### Custom Template Injection
|
|
102
|
-
Keep the search/cache logic but use your own design:
|
|
103
|
-
|
|
104
|
-
```javascript
|
|
105
|
-
initPortfolio('your-username', {
|
|
106
|
-
projectsContainer: 'id-projects',
|
|
107
|
-
customCardTemplate: (repo) => `
|
|
108
|
-
<div class="my-custom-card">
|
|
109
|
-
<h4>${repo.name}</h4>
|
|
110
|
-
<p>${repo.description}</p>
|
|
111
|
-
<a href="${repo.htmlUrl}">View Source</a>
|
|
112
|
-
</div>
|
|
113
|
-
`
|
|
114
|
-
});
|
|
115
|
-
```
|
|
65
|
+
A lib reconhece automaticamente ícones e cores oficiais para diversas tecnologias:
|
|
66
|
+
`React` • `TypeScript` • `Node.js` • `Docker` • `Tailwind` • `Sass` • `Python` • `e muito mais...`
|
|
116
67
|
|
|
117
|
-
|
|
118
|
-
Data is stored via localStorage for better performance:
|
|
68
|
+
---
|
|
119
69
|
|
|
120
|
-
|
|
121
|
-
- Isolation: Cache is separated by GitHub username.
|
|
70
|
+
## 🤝 Contribuição
|
|
122
71
|
|
|
123
|
-
|
|
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.
|
|
124
73
|
|
|
125
|
-
|
|
126
|
-
> This is an independent open-source project. Feel free to contribute! If you find a bug or have a feature idea, opening an Issue or a Pull Request is the best way to help.
|
|
127
|
-
>
|
|
128
|
-
> To learn how to collaborate with the code, please check our [**Contributing Guide**](./CONTRIBUTING.md).
|
|
74
|
+
## 📄 Licença
|
|
129
75
|
|
|
130
|
-
|
|
76
|
+
Distribuído sob a licença MIT. Veja [`LICENSE`](https://github.com/guilhermegodoydev/snapport/blob/main/LICENSE) para mais informações.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export declare function getPortProjects(username: string, tag?: string
|
|
1
|
+
export declare function getPortProjects(username: string, tag?: string, options?: {
|
|
2
|
+
forceRefresh?: boolean;
|
|
3
|
+
}): Promise<SanitizedRepo[]>;
|
|
2
4
|
|
|
3
5
|
export declare function initPortfolio(username: string, config?: PortfolioConfig): Promise<PortfolioResponse>;
|
|
4
6
|
|
|
@@ -28,6 +30,7 @@ export declare interface SanitizedRepo {
|
|
|
28
30
|
description: string | null;
|
|
29
31
|
htmlUrl: string;
|
|
30
32
|
topics: string[];
|
|
33
|
+
stacks: string[];
|
|
31
34
|
deployUrl: string | null;
|
|
32
35
|
}
|
|
33
36
|
|
package/dist/snap-port.js
CHANGED
|
@@ -1,47 +1,4 @@
|
|
|
1
|
-
const
|
|
2
|
-
id: t.id,
|
|
3
|
-
name: t.name ?? "Projeto sem nome",
|
|
4
|
-
description: t.description ?? "Sem descrição disponível",
|
|
5
|
-
htmlUrl: t.html_url,
|
|
6
|
-
topics: Array.isArray(t.topics) ? t.topics : [],
|
|
7
|
-
deployUrl: t.homepage ?? null
|
|
8
|
-
});
|
|
9
|
-
function f(t) {
|
|
10
|
-
try {
|
|
11
|
-
Object.keys(localStorage).forEach((e) => {
|
|
12
|
-
e.startsWith("gh_projects_") && !e.includes(t) && localStorage.removeItem(e);
|
|
13
|
-
});
|
|
14
|
-
} catch (e) {
|
|
15
|
-
console.warn("Erro ao limpar caches antigos:", e);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
async function y(t, e = "port") {
|
|
19
|
-
if (!t)
|
|
20
|
-
return console.error("GitHubPortfolio: Username é obrigatório."), [];
|
|
21
|
-
const o = `gh_projects_${t}`;
|
|
22
|
-
try {
|
|
23
|
-
f(t);
|
|
24
|
-
const r = localStorage.getItem(o);
|
|
25
|
-
if (r) {
|
|
26
|
-
const { data: l, timestamp: p } = JSON.parse(r);
|
|
27
|
-
if (Date.now() - p < 72e5)
|
|
28
|
-
return l;
|
|
29
|
-
}
|
|
30
|
-
const n = encodeURIComponent(`user:${t} topic:${e}`), c = await fetch(`https://api.github.com/search/repositories?q=${n}&sort=updated&order=desc`);
|
|
31
|
-
if (!c.ok)
|
|
32
|
-
throw new Error(`GitHub API error: ${c.status}`);
|
|
33
|
-
const a = ((await c.json()).items || []).map(u), i = {
|
|
34
|
-
data: a,
|
|
35
|
-
timestamp: Date.now()
|
|
36
|
-
};
|
|
37
|
-
return localStorage.setItem(o, JSON.stringify(i)), a;
|
|
38
|
-
} catch (r) {
|
|
39
|
-
console.error(`GitHubPortfolio: Erro ao buscar dados de ${t}:`, r);
|
|
40
|
-
const n = localStorage.getItem(o);
|
|
41
|
-
return n ? (console.warn("GitHubPortfolio: Usando cache expirado devido a erro de rede."), JSON.parse(n).data) : [];
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
const v = {
|
|
1
|
+
const f = {
|
|
45
2
|
react: { name: "React", icon: "react", color: "61DAFB" },
|
|
46
3
|
vue: { name: "Vue.js", icon: "vuedotjs", color: "4FC08D" },
|
|
47
4
|
angular: { name: "Angular", icon: "angular", color: "DD0031" },
|
|
@@ -70,7 +27,54 @@ const v = {
|
|
|
70
27
|
git: { name: "Git", icon: "git", color: "F05032" },
|
|
71
28
|
jest: { name: "Jest", icon: "jest", color: "C21325" },
|
|
72
29
|
"github-actions": { name: "GitHub Actions", icon: "githubactions", color: "2088FF" }
|
|
73
|
-
},
|
|
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) => {
|
|
74
78
|
if (!e)
|
|
75
79
|
return `
|
|
76
80
|
<button class="ghp-filter-btn active" data-topic="all">
|
|
@@ -88,8 +92,8 @@ const v = {
|
|
|
88
92
|
<p>${e.name}</p>
|
|
89
93
|
</button>
|
|
90
94
|
`;
|
|
91
|
-
},
|
|
92
|
-
const o = `https://raw.githubusercontent.com/${e}/${t.name}/main/preview.png`, r = `https://opengraph.githubassets.com/${e}/${t.name}`,
|
|
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>` : "";
|
|
93
97
|
return `
|
|
94
98
|
<div class="ghp-project-card">
|
|
95
99
|
<div class="ghp-img-container">
|
|
@@ -98,7 +102,7 @@ const v = {
|
|
|
98
102
|
alt="Preview do projeto ${t.name}"
|
|
99
103
|
class="ghp-card-img"
|
|
100
104
|
loading="lazy"
|
|
101
|
-
onerror="if (this.src.includes('preview.png')) { this.src='${r}'; } else { this.onerror=null; this.src='${
|
|
105
|
+
onerror="if (this.src.includes('preview.png')) { this.src='${r}'; } else { this.onerror=null; this.src='${c}'; }"
|
|
102
106
|
>
|
|
103
107
|
</div>
|
|
104
108
|
<div class="ghp-card-content">
|
|
@@ -108,37 +112,37 @@ const v = {
|
|
|
108
112
|
</div>
|
|
109
113
|
<div class="ghp-card-links">
|
|
110
114
|
${s}
|
|
111
|
-
${
|
|
115
|
+
${n}
|
|
112
116
|
</div>
|
|
113
117
|
</div>
|
|
114
118
|
</div>
|
|
115
119
|
`;
|
|
116
120
|
};
|
|
117
|
-
function
|
|
118
|
-
const o =
|
|
121
|
+
function E(t, e = 6) {
|
|
122
|
+
const o = h(t);
|
|
119
123
|
o && (o.className = "ghp-projects-grid", o.innerHTML = Array(e).fill('<div class="ghp-project-card ghp-skeleton ghp-skeleton-card"></div>').join(""));
|
|
120
124
|
}
|
|
121
|
-
function
|
|
122
|
-
const o =
|
|
125
|
+
function S(t, e) {
|
|
126
|
+
const o = h(e);
|
|
123
127
|
if (!o) return;
|
|
124
|
-
const r = t.flatMap((s) => s.topics || []),
|
|
125
|
-
const
|
|
126
|
-
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;
|
|
127
131
|
}, g("all"));
|
|
128
|
-
o.innerHTML = `<div class="ghp-filters-content">${
|
|
132
|
+
o.innerHTML = `<div class="ghp-filters-content">${n}</div>`;
|
|
129
133
|
}
|
|
130
134
|
function m(t, e, o = "", r) {
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
133
|
-
if (
|
|
134
|
-
|
|
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>';
|
|
135
139
|
return;
|
|
136
140
|
}
|
|
137
|
-
|
|
141
|
+
c.innerHTML = t.map((n) => r ? r(n) : b(n, o)).join("");
|
|
138
142
|
}
|
|
139
143
|
}
|
|
140
|
-
function
|
|
141
|
-
const e =
|
|
144
|
+
function x(t) {
|
|
145
|
+
const e = h(t);
|
|
142
146
|
e && (e.innerHTML = `
|
|
143
147
|
<div class="ghp-search-container">
|
|
144
148
|
<svg class="ghp-search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
@@ -148,34 +152,34 @@ function $(t) {
|
|
|
148
152
|
</div>
|
|
149
153
|
`);
|
|
150
154
|
}
|
|
151
|
-
function
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
if (!
|
|
156
|
-
const l =
|
|
157
|
-
m(p, o, r,
|
|
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");
|
|
158
162
|
});
|
|
159
163
|
}
|
|
160
|
-
function
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
(l) => l.name.toLowerCase().includes(
|
|
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))
|
|
165
169
|
);
|
|
166
|
-
m(
|
|
170
|
+
m(a, e, o, r);
|
|
167
171
|
});
|
|
168
172
|
}
|
|
169
|
-
async function
|
|
173
|
+
async function L(t, e = {
|
|
170
174
|
searchContainer: "search-cont",
|
|
171
175
|
filtersContainer: "filters-cont",
|
|
172
176
|
projectsContainer: "projects-cont"
|
|
173
177
|
}) {
|
|
174
178
|
try {
|
|
175
179
|
const o = e.tag || "port";
|
|
176
|
-
|
|
177
|
-
const r = await
|
|
178
|
-
return
|
|
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" };
|
|
179
183
|
} catch (o) {
|
|
180
184
|
console.error("Erro ao inicializar portfólio:", o);
|
|
181
185
|
const r = document.getElementById(e.projectsContainer);
|
|
@@ -183,9 +187,9 @@ async function S(t, e = {
|
|
|
183
187
|
}
|
|
184
188
|
}
|
|
185
189
|
export {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
190
|
+
$ as getPortProjects,
|
|
191
|
+
L as initPortfolio,
|
|
192
|
+
S as renderFilters,
|
|
189
193
|
m as renderProjects,
|
|
190
|
-
|
|
194
|
+
x as renderSearchBar
|
|
191
195
|
};
|
package/dist/snap-port.umd.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
(function(
|
|
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
2
|
<button class="ghp-filter-btn active" data-topic="all">
|
|
3
3
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
4
4
|
<rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<img src="${o}" alt="${t.name}" loading="lazy">
|
|
12
12
|
<p>${t.name}</p>
|
|
13
13
|
</button>
|
|
14
|
-
`}
|
|
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
15
|
<div class="ghp-project-card">
|
|
16
16
|
<div class="ghp-img-container">
|
|
17
17
|
<img
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
alt="Preview do projeto ${e.name}"
|
|
20
20
|
class="ghp-card-img"
|
|
21
21
|
loading="lazy"
|
|
22
|
-
onerror="if (this.src.includes('preview.png')) { this.src='${r}'; } else { this.onerror=null; this.src='${
|
|
22
|
+
onerror="if (this.src.includes('preview.png')) { this.src='${r}'; } else { this.onerror=null; this.src='${s}'; }"
|
|
23
23
|
>
|
|
24
24
|
</div>
|
|
25
25
|
<div class="ghp-card-content">
|
|
@@ -28,16 +28,16 @@
|
|
|
28
28
|
<p title="${e.description}">${e.description}</p>
|
|
29
29
|
</div>
|
|
30
30
|
<div class="ghp-card-links">
|
|
31
|
-
${i}
|
|
32
31
|
${c}
|
|
32
|
+
${n}
|
|
33
33
|
</div>
|
|
34
34
|
</div>
|
|
35
35
|
</div>
|
|
36
|
-
`};function
|
|
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
37
|
<div class="ghp-search-container">
|
|
38
38
|
<svg class="ghp-search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
39
39
|
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
|
|
40
40
|
</svg>
|
|
41
41
|
<input type="text" id="gh-port-search" class="ghp-search-input" placeholder="Buscar projeto..." autocomplete="off">
|
|
42
42
|
</div>
|
|
43
|
-
`)}function
|
|
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"})}));
|
package/package.json
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snapport",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.1",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"portfolio",
|
|
7
|
+
"github-api",
|
|
8
|
+
"typescript",
|
|
9
|
+
"zero-dependencies",
|
|
10
|
+
"automation",
|
|
11
|
+
"frontend",
|
|
12
|
+
"showcase",
|
|
13
|
+
"headless-ui"
|
|
14
|
+
],
|
|
5
15
|
"type": "module",
|
|
6
16
|
"files": [
|
|
7
17
|
"dist"
|
|
@@ -18,15 +28,21 @@
|
|
|
18
28
|
"./style.css": "./dist/snap-port.css"
|
|
19
29
|
},
|
|
20
30
|
"scripts": {
|
|
21
|
-
"dev": "vite",
|
|
31
|
+
"dev": "vite playground",
|
|
22
32
|
"build": "tsc && vite build",
|
|
23
|
-
"preview": "vite preview"
|
|
33
|
+
"preview": "vite preview",
|
|
34
|
+
"docs:dev": "vitepress dev docs",
|
|
35
|
+
"docs:build": "vitepress build docs",
|
|
36
|
+
"docs:preview": "vitepress preview docs",
|
|
37
|
+
"docs:deploy": "npm run docs:build && gh-pages -d docs/.vitepress/dist"
|
|
24
38
|
},
|
|
25
39
|
"devDependencies": {
|
|
26
|
-
"@types/node": "^25.
|
|
40
|
+
"@types/node": "^25.6.0",
|
|
41
|
+
"gh-pages": "^6.3.0",
|
|
27
42
|
"typescript": "~5.9.3",
|
|
28
43
|
"vite": "^7.2.4",
|
|
29
|
-
"vite-plugin-dts": "^4.5.4"
|
|
44
|
+
"vite-plugin-dts": "^4.5.4",
|
|
45
|
+
"vitepress": "^1.6.4"
|
|
30
46
|
},
|
|
31
47
|
"license": "MIT",
|
|
32
48
|
"author": "Guilherme Godoy"
|
package/README.pt-br.md
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<a href="./README.md">Read this in English</a>
|
|
3
|
-
</p>
|
|
4
|
-
|
|
5
|
-
<p align="center">
|
|
6
|
-
<img src="https://img.shields.io/npm/dm/snapport?style=for-the-badge&logo=npm" alt="NPM Downloads">
|
|
7
|
-
<img src="https://img.shields.io/npm/v/snapport?style=for-the-badge" alt="NPM Version">
|
|
8
|
-
<img src="https://img.shields.io/github/license/guilhermegodoydev/snapport?style=for-the-badge" alt="License">
|
|
9
|
-
<img src="https://img.shields.io/bundlephobia/min/snapport?style=for-the-badge" alt="Bundle Size">
|
|
10
|
-
</p>
|
|
11
|
-
|
|
12
|
-
# Snap-Port 🚀
|
|
13
|
-
|
|
14
|
-
O **Snap-Port** é uma biblioteca desenvolvida em **TypeScript**, sem dependências externas, projetada para automatizar a exibição de projetos do GitHub em sites pessoais ou portfólios.
|
|
15
|
-
|
|
16
|
-
A proposta central é utilizar o GitHub como **fonte única de verdade:** ao marcar seus repositórios com a tag escolhida, a biblioteca se encarrega de buscar, tratar, aplicar cache e renderizar os dados, eliminando a manutenção manual no código do seu site.
|
|
17
|
-
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
## 🛠 Funcionalidades Técnicas
|
|
21
|
-
|
|
22
|
-
### 1. Seleção de Projetos e Controle de Stacks
|
|
23
|
-
O Snap-Port oferece controle total sobre o que é exibido e como as tecnologias são categorizadas:
|
|
24
|
-
|
|
25
|
-
- **Tag de Descoberta**: Por padrão, a biblioteca busca repositórios com a tag ``port``, mas você pode definir qualquer outra tag no momento da inicialização.
|
|
26
|
-
- **Filtros por Stacks (Topics):** Para que os filtros automáticos e a barra de busca funcionem corretamente, você deve listar as tecnologias (ex: ``react``, ``nodejs``, ``css``) nos topics do seu repositório no GitHub.
|
|
27
|
-
- **Por que não usar a "Language" automática?** A lib ignora o campo ``language`` do GitHub para permitir que você decida quais ferramentas quer destacar. Isso evita que um projeto de React seja classificado apenas como "HTML" ou "JavaScript" devido ao volume de arquivos gerados por ferramentas de build, garantindo que o filtro reflita a stack real do projeto.
|
|
28
|
-
|
|
29
|
-
### 2. Gestão Inteligente de Imagens
|
|
30
|
-
Como a API do GitHub não retorna links diretos de imagens de preview, o Snap-Port utiliza uma lógica de **geração automática** integrada aos componentes de UI.
|
|
31
|
-
|
|
32
|
-
Para que cada projeto tenha sua própria imagem, siga estas regras:
|
|
33
|
-
|
|
34
|
-
- **Arquivo de Preview:** Você deve criar um arquivo chamado ``preview.png`` na raiz do seu repositório.
|
|
35
|
-
- **Importante:** O nome deve ser exatamente preview.png (letras minúsculas), pois o GitHub diferencia maiúsculas de minúsculas (*case-sensitive*).
|
|
36
|
-
|
|
37
|
-
Caso o arquivo não exista ou ocorra algum erro de carregamento (como *Rate Limits*), a lib executa uma **estratégia de fallback em cascata:**
|
|
38
|
-
|
|
39
|
-
- **GitHub Open Graph:** Tenta carregar o card dinâmico gerado pelo próprio GitHub.
|
|
40
|
-
- **Placeholder de Segurança:** Se o GitHub bloquear a requisição, gera um card neutro contendo o nome do projeto via placehold.co.
|
|
41
|
-
|
|
42
|
-
### 3. Componentes de UI Integrados
|
|
43
|
-
|
|
44
|
-
- **Search Bar:** Filtro textual em tempo real (nome, descrição e tópicos).
|
|
45
|
-
- **Filter** Carousel: Carrossel dinâmico baseado nos tópicos definidos nos repositórios.
|
|
46
|
-
- **Project Cards (Layout 16:9):** Cards responsivos com badges de tecnologia e botões de ação (Código e Deploy).
|
|
47
|
-
|
|
48
|
-
---
|
|
49
|
-
|
|
50
|
-
## 💡 Dicas para um melhor Resultado
|
|
51
|
-
|
|
52
|
-
- **Proporção de Imagem:** Para que as imagens não fiquem com partes cortadas nos cards, salve seus arquivos ``preview.png`` na proporção **16:9** (ex: 1280x720px).
|
|
53
|
-
- **Link de Acesso (Deploy):** O botão "Acessar" só aparecerá se o campo **"Homepage"** estiver preenchido nas configurações do seu repositório no GitHub.
|
|
54
|
-
|
|
55
|
-
---
|
|
56
|
-
|
|
57
|
-
## 📦 Instalação e Integração
|
|
58
|
-
|
|
59
|
-
### Via NPM
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
npm install snapport
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### Via CDN (Direto no HTML)
|
|
66
|
-
Se você preferir não usar gerenciadores de pacotes, pode importar os arquivos diretamente de um CDN. Recomendamos o uso de type="module" para melhor compatibilidade com o padrão moderno da biblioteca.
|
|
67
|
-
|
|
68
|
-
```html
|
|
69
|
-
<!-- 1. Estilos da Biblioteca -->
|
|
70
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/snapport/dist/snap-port.css">
|
|
71
|
-
|
|
72
|
-
<!-- 2. Lógica e Inicialização -->
|
|
73
|
-
<script type="module">
|
|
74
|
-
// Importação do módulo oficial ES
|
|
75
|
-
import { initPortfolio } from 'https://cdn.jsdelivr.net/npm/snapport/dist/snap-port.js';
|
|
76
|
-
|
|
77
|
-
initPortfolio('seu-usuario', {
|
|
78
|
-
tag: 'port', // Opcional: padrão é 'port'
|
|
79
|
-
searchContainer: 'id-search', // ID do container da busca
|
|
80
|
-
filtersContainer: 'id-filters', // ID do container dos filtros
|
|
81
|
-
projectsContainer: 'id-projects' // ID do container do grid
|
|
82
|
-
});
|
|
83
|
-
</script>
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
---
|
|
87
|
-
|
|
88
|
-
## 🎨 Personalização Visual (CSS Variables)
|
|
89
|
-
Se você utiliza o layout padrão da biblioteca, pode adaptar as cores e o estilo ao seu tema sem modificar o código interno. O Snap-Port utiliza **Variáveis CSS** que podem ser facilmente sobrescritas no seu arquivo global:
|
|
90
|
-
|
|
91
|
-
```css
|
|
92
|
-
:root {
|
|
93
|
-
--ghp-accent: #333; /* Cor de destaque (botões e ícones) */
|
|
94
|
-
--ghp-bg: #ffffff; /* Fundo dos cards */
|
|
95
|
-
--ghp-text: #333; /* Título e textos principais */
|
|
96
|
-
--ghp-text-light: #666; /* Descrições e textos secundários */
|
|
97
|
-
--ghp-border: rgba(226, 226, 228, 0.8); /* Bordas */
|
|
98
|
-
--ghp-shadow: rgba(0, 0, 0, 0.1); /* Sombras dos cards */
|
|
99
|
-
}
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
---
|
|
103
|
-
|
|
104
|
-
## ⚙️ Customização e Performance
|
|
105
|
-
|
|
106
|
-
### Injeção de Template Customizado
|
|
107
|
-
Mantenha a inteligência de busca e cache, mas use seu próprio design:
|
|
108
|
-
|
|
109
|
-
```javascript
|
|
110
|
-
initPortfolio('seu-usuario', {
|
|
111
|
-
searchContainer: 'id-search',
|
|
112
|
-
filtersContainer: 'id-filters',
|
|
113
|
-
projectsContainer: 'id-projects',
|
|
114
|
-
customCardTemplate: (repo) => `
|
|
115
|
-
<div class="meu-card-personalizado">
|
|
116
|
-
<h4>${repo.name}</h4>
|
|
117
|
-
<p>${repo.description}</p>
|
|
118
|
-
<a href="${repo.htmlUrl}">Ver código</a>
|
|
119
|
-
</div>
|
|
120
|
-
`
|
|
121
|
-
});
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### Cache e Estabilidade
|
|
125
|
-
A biblioteca utiliza localStorage para garantir performance:
|
|
126
|
-
|
|
127
|
-
- **Persistência:** Dados armazenados por até 2 horas.
|
|
128
|
-
- **Isolamento:** Cache separado por usuário do GitHub.
|
|
129
|
-
|
|
130
|
-
---
|
|
131
|
-
|
|
132
|
-
> **Nota sobre Manutenção:**
|
|
133
|
-
> Este é um projeto de código aberto mantido de forma independente. Sinta-se à vontade para contribuir! Se encontrar um bug ou tiver uma ideia de funcionalidade, abrir uma **Issue** ou um **Pull Request** é a melhor forma de ajudar o projeto a crescer.
|
|
134
|
-
>
|
|
135
|
-
> Para entender como colaborar com o código, consulte o nosso [**Guia de Contribuição**](./CONTRIBUTING.md).
|
|
136
|
-
|
|
137
|
-
**Autor**: Guilherme Godoy (@guilhermegodoydev)
|