snapport 0.1.0
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/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/index.d.ts +39 -0
- package/dist/snap-port.css +1 -0
- package/dist/snap-port.js +190 -0
- package/dist/snap-port.umd.cjs +43 -0
- package/package.json +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Guilherme Godoy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Snap-Port 🚀
|
|
2
|
+
O **Snap-Port** é uma biblioteca **vanilla JavaScript**, sem dependências, pensada para desenvolvedores que desejam automatizar a exibição de projetos do GitHub em sites pessoais ou portfólios.
|
|
3
|
+
|
|
4
|
+
A proposta é simples: você marca seus repositórios com a tag ``port`` no GitHub, e o Snap-Port se encarrega de **buscar, cachear, filtrar e renderizar** esses projetos na sua interface — eliminando a necessidade de atualizações manuais no código do site.
|
|
5
|
+
|
|
6
|
+
> **Use o GitHub como fonte única de verdade para o seu portfólio.**
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 🛠 O que a biblioteca oferece?
|
|
11
|
+
|
|
12
|
+
A biblioteca foi desenhada para ser modular, funcionando tanto como um **motor de dados (headless)** quanto como uma **solução visual pronta para uso.**
|
|
13
|
+
|
|
14
|
+
### 1. Camada de Dados (Headless)
|
|
15
|
+
|
|
16
|
+
Se você já possui seu próprio layout ou utiliza frameworks como **React** ou **Vue**, pode consumir apenas a lógica de dados.
|
|
17
|
+
|
|
18
|
+
O método ``getPortProjects`` retorna um JSON tratado, abstraindo o ruído da API do GitHub e entregando apenas o essencial:
|
|
19
|
+
|
|
20
|
+
- Nome
|
|
21
|
+
- Descrição
|
|
22
|
+
- Tópicos
|
|
23
|
+
- Link do repositório
|
|
24
|
+
- Link de deploy
|
|
25
|
+
- Linguagem principal
|
|
26
|
+
|
|
27
|
+
Exemplo básico de uso:
|
|
28
|
+
```bash
|
|
29
|
+
import { getPortProjects } from 'snap-port';
|
|
30
|
+
|
|
31
|
+
const projects = await getPortProjects('seu-usuario', 'topic-tag');
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
### 2. Componentes de UI (Plug & Play)
|
|
37
|
+
|
|
38
|
+
Para quem busca agilidade, o Snap-Port oferece componentes de interface prontos, que podem ser usados sem frameworks:
|
|
39
|
+
|
|
40
|
+
- **Search Bar**
|
|
41
|
+
Filtro textual instantâneo que atua sobre os dados em cache.
|
|
42
|
+
|
|
43
|
+
- **Filter Carousel**
|
|
44
|
+
Carrossel horizontal de tecnologias que identifica automaticamente suas stacks a partir dos tópicos do GitHub.
|
|
45
|
+
|
|
46
|
+
- **Project Cards**
|
|
47
|
+
Cards minimalistas que incluem:
|
|
48
|
+
- Social Preview (imagem do repositório)
|
|
49
|
+
- Descrição com limite de linhas
|
|
50
|
+
- Botões de ação para código-fonte e deploy
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 🚀 Instalação e Uso
|
|
55
|
+
|
|
56
|
+
### Via NPM
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install snap-port
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Via CDN (Direto no HTML)
|
|
63
|
+
Se preferir não usar gerenciadores de pacotes, você pode importar os arquivos de distribuição diretamente:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
<!-- Estilos da Lib -->
|
|
67
|
+
<link rel="stylesheet" href="https://unpkg.com/snap-port/dist/snap-port.css">
|
|
68
|
+
|
|
69
|
+
<!-- Lógica da Lib -->
|
|
70
|
+
<script type="module">
|
|
71
|
+
import { initPortfolio } from 'https://unpkg.com/snap-port/dist/snap-port.js';
|
|
72
|
+
|
|
73
|
+
initPortfolio('seu-usuario', {
|
|
74
|
+
search: 'id-do-input',
|
|
75
|
+
filters: 'id-container-dos-filtros',
|
|
76
|
+
projects: 'id-do-grid'
|
|
77
|
+
});
|
|
78
|
+
</script>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## ⚙️ Customização e Comportamento
|
|
82
|
+
|
|
83
|
+
### Gerenciamento de Tags e Imagens
|
|
84
|
+
|
|
85
|
+
A biblioteca utiliza os **topics** do seu repositório para duas funções:
|
|
86
|
+
|
|
87
|
+
- **Filtro de Descoberta**
|
|
88
|
+
Apenas repositórios com a tag **`port`** são processados.
|
|
89
|
+
|
|
90
|
+
- **Identidade Visual**
|
|
91
|
+
Tags como `react`, `nodejs` ou `typescript` são mapeadas para seus respectivos ícones e cores oficiais.
|
|
92
|
+
|
|
93
|
+
- **Imagens**
|
|
94
|
+
O Snap-Port consome o **Open Graph** do repositório para exibir automaticamente a imagem de preview do projeto.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
### Cache, Performance e Rate Limit
|
|
99
|
+
|
|
100
|
+
Para evitar chamadas excessivas à API do GitHub e reduzir impactos de **rate limit**, o Snap-Port implementa um **sistema de cache inteligente baseado em ``localStorage``**.
|
|
101
|
+
|
|
102
|
+
Esse sistema:
|
|
103
|
+
|
|
104
|
+
- Armazena os dados tratados por usuário de forma isolada, evitando conflitos quando múltiplos portfólios utilizam a biblioteca no mesmo ambiente (ex: recrutadores abrindo vários ports).
|
|
105
|
+
|
|
106
|
+
- Possui ciclo de expiração automática, garantindo que os dados sejam atualizados periodicamente (entre 1 e 2 horas).
|
|
107
|
+
|
|
108
|
+
- Realiza limpeza automática de entradas antigas, funcionando como um garbage collector manual, evitando crescimento indefinido do localStorage.
|
|
109
|
+
|
|
110
|
+
- Trata casos de borda para impedir reutilização indevida de cache entre usuários diferentes.
|
|
111
|
+
|
|
112
|
+
Na prática, isso garante:
|
|
113
|
+
|
|
114
|
+
- Melhor performance,
|
|
115
|
+
|
|
116
|
+
- Menos requisições,
|
|
117
|
+
|
|
118
|
+
- E maior previsibilidade no consumo da API do GitHub.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### Estilização
|
|
123
|
+
|
|
124
|
+
A interface é construída com **variáveis CSS**, permitindo que você adapte as cores ao seu tema sem modificar o código interno:
|
|
125
|
+
|
|
126
|
+
```css
|
|
127
|
+
:root {
|
|
128
|
+
--ghp-accent: #333; /* Cor de destaque (botões ativos e ícones) */
|
|
129
|
+
--ghp-bg: #fff; /* Fundo dos cards */
|
|
130
|
+
--ghp-border: #ddd; /* Bordas e divisores */
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## ⚠️ Status do Projeto e Contribuições
|
|
135
|
+
|
|
136
|
+
Este projeto está em sua fase **MVP (Minimum Viable Product)**.
|
|
137
|
+
|
|
138
|
+
Atualmente, os componentes de UI compartilham estado interno para otimizar filtragem e performance.
|
|
139
|
+
|
|
140
|
+
> Este projeto é mantido no tempo livre e não possui garantias de suporte contínuo. Mudanças na API do GitHub ou na própria biblioteca podem ocorrer sem aviso prévio.
|
|
141
|
+
|
|
142
|
+
Sugestões, ideias de funcionalidades e relatos de bugs são bem-vindos.
|
|
143
|
+
Sinta-se à vontade para abrir uma **Issue** ou enviar um **Pull Request**.
|
|
144
|
+
Para detalhes sobre como contribuir, consulte o [**Guia de Contribuição.**](https://github.com/guilhermegodoydev/snap-port/blob/main/CONTRIBUTING.md)
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
**Autor:** Guilherme Godoy ([@guilhermegodoydev](https://github.com/guilhermegodoydev)) • **Licença:** MIT • **Peso:** 2.8kB gzipped
|
|
149
|
+
|
|
150
|
+
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare function getPortProjects(username: string, tag?: string): Promise<SanitizedRepo[]>;
|
|
2
|
+
|
|
3
|
+
export declare function initPortfolio(username: string, config?: PortfolioConfig): Promise<PortfolioResponse>;
|
|
4
|
+
|
|
5
|
+
export declare interface PortfolioConfig {
|
|
6
|
+
searchContainer: string;
|
|
7
|
+
filtersContainer: string;
|
|
8
|
+
projectsContainer: string;
|
|
9
|
+
customCardTemplate?: (repo: SanitizedRepo) => string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export declare interface PortfolioResponse {
|
|
13
|
+
projects?: any[];
|
|
14
|
+
status: 'success' | 'error';
|
|
15
|
+
message?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export declare function renderFilters(projects: SanitizedRepo[], containerElement: HTMLElement | string): void;
|
|
19
|
+
|
|
20
|
+
export declare function renderProjects(projects: SanitizedRepo[], containerElement: HTMLElement | string, username?: string, customTemplate?: (repo: SanitizedRepo) => string): void;
|
|
21
|
+
|
|
22
|
+
export declare function renderSearchBar(containerElement: HTMLElement | string): void;
|
|
23
|
+
|
|
24
|
+
export declare interface SanitizedRepo {
|
|
25
|
+
id: number;
|
|
26
|
+
name: string;
|
|
27
|
+
description: string | null;
|
|
28
|
+
htmlUrl: string;
|
|
29
|
+
topics: string[];
|
|
30
|
+
deployUrl: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export declare interface TechInfo {
|
|
34
|
+
icon: string;
|
|
35
|
+
name: string;
|
|
36
|
+
color: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--ghp-accent: #333;--ghp-bg: #ffffff;--ghp-border: rgba(226, 226, 228, .8);--ghp-shadow: rgba(0, 0, 0, .1);--ghp-text: #333;--ghp-text-light: #666}.ghp-search-input,.ghp-filter-btn,.ghp-card-links a{transition:all .2s ease}.ghp-search-container{position:relative;margin:0 15px 20px;max-width:400px}.ghp-search-icon{position:absolute;left:12px;top:50%;transform:translateY(-50%);color:#888;pointer-events:none}.ghp-search-input{width:100%;padding:10px 12px 10px 38px;border:1px solid var(--ghp-border);border-radius:8px;font-size:14px;outline:none}.ghp-search-input:focus{border-color:var(--ghp-accent);box-shadow:0 0 0 3px #0000000d}.ghp-filters-content{display:flex;gap:12px;padding:10px 15px;margin-bottom:25px;overflow-x:auto;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;scrollbar-width:none}.ghp-filters-content::-webkit-scrollbar{display:none}.ghp-filter-btn{scroll-snap-align:start;flex:0 0 auto;display:flex;align-items:center;gap:8px;background-color:var(--ghp-bg);padding:8px 16px;border:solid 2px #e1e4e8;border-radius:30px;cursor:pointer;font-family:inherit;font-weight:500}.ghp-filter-btn img,.ghp-filter-btn svg{width:18px;height:18px;object-fit:contain}.ghp-filter-btn p{margin:0;font-size:14px}@media(max-width:600px){.ghp-filters-content{gap:8px;padding:8px}}.ghp-filter-btn:hover{transform:translateY(-3px);border-color:var(--tech-color, var(--ghp-accent));color:var(--tech-color, var(--ghp-accent));box-shadow:0 4px 8px var(--ghp-shadow)}.ghp-filter-btn.active{background-color:var(--tech-color, var(--ghp-accent));color:#fff;border-color:var(--tech-color, var(--ghp-accent))}.ghp-filter-btn.active img{filter:brightness(0) invert(1)}.ghp-projects-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:25px;padding:15px;align-items:start}@media(max-width:1024px){.ghp-projects-grid{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}}.ghp-project-card{background-color:var(--ghp-bg);border-radius:12px;border:solid 1px var(--ghp-border);box-shadow:0 2px 5px var(--ghp-shadow);display:flex;flex-direction:column;transition:transform .2s ease;height:100%;min-height:220px}.ghp-img-container,.ghp-card-img{border-radius:8px 8px 0 0}.ghp-img-container{width:100%;aspect-ratio:16 / 9;background-color:#161b22;border-bottom:1px solid var(--ghp-border);overflow:hidden}.ghp-card-img{width:100%;height:100%;object-fit:cover;object-position:center;display:block}.ghp-card-content{display:flex;flex-direction:column;justify-content:space-between;flex:1;padding:20px}.ghp-skeleton-card{height:220px;width:100%;border:1px solid transparent;box-sizing:border-box}.ghp-project-card:hover{transform:translateY(-5px)}.ghp-project-card h3{margin:0 0 10px;font-size:18px}.ghp-project-card p{font-size:14px;color:var(--ghp-text-light);display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis;line-height:1.5}.ghp-card-links{display:flex;gap:10px}.ghp-card-links a{text-decoration:none;padding:6px 14px;color:var(--ghp-text);font-size:13px;font-weight:500;border:solid 1px var(--ghp-border);border-radius:20px;background-color:#f6f8fa}.ghp-card-links a:hover{background-color:var(--ghp-accent);color:#fff;border-color:var(--ghp-accent)}.ghp-skeleton{background:linear-gradient(90deg,#f0f0f0 25%,#e6e6e6,#f0f0f0 75%);background-size:200% 100%;animation:ghp-loading 1.5s infinite ease-in-out;border-radius:4px}@keyframes ghp-loading{0%{background-position:200% 0}to{background-position:-200% 0}}.ghp-skeleton-card{height:200px;width:100%;border-radius:12px}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
const u = (t) => ({
|
|
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 = {
|
|
45
|
+
react: { name: "React", icon: "react", color: "61DAFB" },
|
|
46
|
+
vue: { name: "Vue.js", icon: "vuedotjs", color: "4FC08D" },
|
|
47
|
+
angular: { name: "Angular", icon: "angular", color: "DD0031" },
|
|
48
|
+
nextjs: { name: "Next.js", icon: "nextdotjs", color: "000000" },
|
|
49
|
+
typescript: { name: "TypeScript", icon: "typescript", color: "3178C6" },
|
|
50
|
+
javascript: { name: "JavaScript", icon: "javascript", color: "F7DF1E" },
|
|
51
|
+
html5: { name: "HTML5", icon: "html5", color: "E34F26" },
|
|
52
|
+
css3: { name: "CSS3", icon: "css3", color: "1572B6" },
|
|
53
|
+
sass: { name: "Sass", icon: "sass", color: "CC6699" },
|
|
54
|
+
"tailwind-css": { name: "Tailwind", icon: "tailwindcss", color: "06B6D4" },
|
|
55
|
+
bootstrap: { name: "Bootstrap", icon: "bootstrap", color: "7952B3" },
|
|
56
|
+
"styled-components": { name: "Styled Components", icon: "styledcomponents", color: "DB7093" },
|
|
57
|
+
figma: { name: "Figma", icon: "figma", color: "F24E1E" },
|
|
58
|
+
nodejs: { name: "Node.js", icon: "nodedotjs", color: "339933" },
|
|
59
|
+
express: { name: "Express", icon: "express", color: "000000" },
|
|
60
|
+
nestjs: { name: "NestJS", icon: "nestjs", color: "E0234E" },
|
|
61
|
+
python: { name: "Python", icon: "python", color: "3776AB" },
|
|
62
|
+
java: { name: "Java", icon: "openjdk", color: "007396" },
|
|
63
|
+
php: { name: "PHP", icon: "php", color: "777BB4" },
|
|
64
|
+
postgresql: { name: "PostgreSQL", icon: "postgresql", color: "4169E1" },
|
|
65
|
+
mongodb: { name: "MongoDB", icon: "mongodb", color: "47A248" },
|
|
66
|
+
mysql: { name: "MySQL", icon: "mysql", color: "4479A1" },
|
|
67
|
+
firebase: { name: "Firebase", icon: "firebase", color: "FFCA28" },
|
|
68
|
+
prisma: { name: "Prisma", icon: "prisma", color: "2D3748" },
|
|
69
|
+
docker: { name: "Docker", icon: "docker", color: "2496ED" },
|
|
70
|
+
git: { name: "Git", icon: "git", color: "F05032" },
|
|
71
|
+
jest: { name: "Jest", icon: "jest", color: "C21325" },
|
|
72
|
+
"github-actions": { name: "GitHub Actions", icon: "githubactions", color: "2088FF" }
|
|
73
|
+
}, d = (t) => typeof t == "string" ? document.getElementById(t) : t, g = (t, e = null) => {
|
|
74
|
+
if (!e)
|
|
75
|
+
return `
|
|
76
|
+
<button class="ghp-filter-btn active" data-topic="all">
|
|
77
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
78
|
+
<rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/>
|
|
79
|
+
<rect width="7" height="7" x="14" y="14" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/>
|
|
80
|
+
</svg>
|
|
81
|
+
<p>Todos</p>
|
|
82
|
+
</button>
|
|
83
|
+
`;
|
|
84
|
+
const o = `https://cdn.simpleicons.org/${e.icon}/${e.color}`;
|
|
85
|
+
return `
|
|
86
|
+
<button class="ghp-filter-btn" data-topic="${t}" style="--tech-color: #${e.color}">
|
|
87
|
+
<img src="${o}" alt="${e.name}" loading="lazy">
|
|
88
|
+
<p>${e.name}</p>
|
|
89
|
+
</button>
|
|
90
|
+
`;
|
|
91
|
+
}, j = (t, e) => {
|
|
92
|
+
const o = `https://raw.githubusercontent.com/${e}/${t.name}/main/preview.png`, r = `https://opengraph.githubassets.com/${e}/${t.name}`, n = `https://placehold.co/640x360?text=${encodeURIComponent(t.name)}`, c = 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
|
+
return `
|
|
94
|
+
<div class="ghp-project-card">
|
|
95
|
+
<div class="ghp-img-container">
|
|
96
|
+
<img
|
|
97
|
+
src="${o}"
|
|
98
|
+
alt="Preview do projeto ${t.name}"
|
|
99
|
+
class="ghp-card-img"
|
|
100
|
+
loading="lazy"
|
|
101
|
+
onerror="if (this.src.includes('preview.png')) { this.src='${r}'; } else { this.onerror=null; this.src='${n}'; }"
|
|
102
|
+
>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="ghp-card-content">
|
|
105
|
+
<div>
|
|
106
|
+
<h3>${t.name}</h3>
|
|
107
|
+
<p title="${t.description}">${t.description}</p>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="ghp-card-links">
|
|
110
|
+
${s}
|
|
111
|
+
${c}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
`;
|
|
116
|
+
};
|
|
117
|
+
function C(t, e = 6) {
|
|
118
|
+
const o = d(t);
|
|
119
|
+
o && (o.className = "ghp-projects-grid", o.innerHTML = Array(e).fill('<div class="ghp-project-card ghp-skeleton ghp-skeleton-card"></div>').join(""));
|
|
120
|
+
}
|
|
121
|
+
function w(t, e) {
|
|
122
|
+
const o = d(e);
|
|
123
|
+
if (!o) return;
|
|
124
|
+
const r = t.flatMap((s) => s.topics || []), c = [...new Set(r)].reduce((s, a) => {
|
|
125
|
+
const i = v[a];
|
|
126
|
+
return i ? s + g(a, i) : s;
|
|
127
|
+
}, g("all"));
|
|
128
|
+
o.innerHTML = `<div class="ghp-filters-content">${c}</div>`;
|
|
129
|
+
}
|
|
130
|
+
function m(t, e, o = "", r) {
|
|
131
|
+
const n = d(e);
|
|
132
|
+
if (n) {
|
|
133
|
+
if (n.className = "ghp-projects-grid", n.style.minHeight = "auto", t.length === 0) {
|
|
134
|
+
n.innerHTML = '<p class="ghp-empty-msg">Nenhum projeto encontrado.</p>';
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
n.innerHTML = t.map((c) => r ? r(c) : j(c, o)).join("");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function $(t) {
|
|
141
|
+
const e = d(t);
|
|
142
|
+
e && (e.innerHTML = `
|
|
143
|
+
<div class="ghp-search-container">
|
|
144
|
+
<svg class="ghp-search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
145
|
+
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
|
|
146
|
+
</svg>
|
|
147
|
+
<input type="text" id="gh-port-search" class="ghp-search-input" placeholder="Buscar projeto..." autocomplete="off">
|
|
148
|
+
</div>
|
|
149
|
+
`);
|
|
150
|
+
}
|
|
151
|
+
function b(t, e, o, r, n) {
|
|
152
|
+
const c = document.getElementById(t);
|
|
153
|
+
c && c.addEventListener("click", (s) => {
|
|
154
|
+
const i = s.target.closest(".ghp-filter-btn");
|
|
155
|
+
if (!i) return;
|
|
156
|
+
const l = i.dataset.topic, p = l === "all" ? e : e.filter((h) => h.topics.includes(l));
|
|
157
|
+
m(p, o, r, n), c.querySelectorAll(".ghp-filter-btn").forEach((h) => h.classList.remove("active")), i.classList.add("active");
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function E(t, e, o, r) {
|
|
161
|
+
const n = document.getElementById("gh-port-search");
|
|
162
|
+
n && n.addEventListener("input", (c) => {
|
|
163
|
+
const a = c.target.value.toLowerCase(), i = t.filter(
|
|
164
|
+
(l) => l.name.toLowerCase().includes(a) || (l.description || "").toLowerCase().includes(a) || l.topics.some((p) => p.toLowerCase().includes(a))
|
|
165
|
+
);
|
|
166
|
+
m(i, e, o, r);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async function S(t, e = {
|
|
170
|
+
searchContainer: "search-cont",
|
|
171
|
+
filtersContainer: "filters-cont",
|
|
172
|
+
projectsContainer: "projects-cont"
|
|
173
|
+
}) {
|
|
174
|
+
try {
|
|
175
|
+
$(e.searchContainer), C(e.projectsContainer, 6);
|
|
176
|
+
const o = await y(t);
|
|
177
|
+
return w(o, e.filtersContainer), m(o, e.projectsContainer, t, e.customCardTemplate), b(e.filtersContainer, o, e.projectsContainer, t, e.customCardTemplate), E(o, e.projectsContainer, t, e.customCardTemplate), { projects: o, status: "success" };
|
|
178
|
+
} catch (o) {
|
|
179
|
+
console.error("Erro ao inicializar portfólio:", o);
|
|
180
|
+
const r = document.getElementById(e.projectsContainer);
|
|
181
|
+
return r && (r.innerHTML = '<p class="ghp-empty-msg">Erro ao carregar projetos.</p>'), { status: "error", message: o.message };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
export {
|
|
185
|
+
y as getPortProjects,
|
|
186
|
+
S as initPortfolio,
|
|
187
|
+
w as renderFilters,
|
|
188
|
+
m as renderProjects,
|
|
189
|
+
$ as renderSearchBar
|
|
190
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
(function(s,h){typeof exports=="object"&&typeof module<"u"?h(exports):typeof define=="function"&&define.amd?define(["exports"],h):(s=typeof globalThis<"u"?globalThis:s||self,h(s.SnapPort={}))})(this,(function(s){"use strict";const C=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:[],deployUrl:e.homepage??null});function b(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 f(e,t="port"){if(!e)return console.error("GitHubPortfolio: Username é obrigatório."),[];const o=`gh_projects_${e}`;try{b(e);const r=localStorage.getItem(o);if(r){const{data:p,timestamp:d}=JSON.parse(r);if(Date.now()-d<72e5)return p}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 a=((await c.json()).items||[]).map(C),l={data:a,timestamp:Date.now()};return localStorage.setItem(o,JSON.stringify(l)),a}catch(r){console.error(`GitHubPortfolio: Erro ao buscar dados de ${e}:`,r);const n=localStorage.getItem(o);return n?(console.warn("GitHubPortfolio: Usando cache expirado devido a erro de rede."),JSON.parse(n).data):[]}}const w={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"}},m=e=>typeof e=="string"?document.getElementById(e):e,y=(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
|
+
`},$=(e,t)=>{const o=`https://raw.githubusercontent.com/${t}/${e.name}/main/preview.png`,r=`https://opengraph.githubassets.com/${t}/${e.name}`,n=`https://placehold.co/640x360?text=${encodeURIComponent(e.name)}`,c=e.deployUrl?`<a href="${e.deployUrl}" target="_blank" rel="noopener noreferrer">Acessar</a>`:"",i=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='${n}'; }"
|
|
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
|
+
${i}
|
|
32
|
+
${c}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
`};function S(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 v(e,t){const o=m(t);if(!o)return;const r=e.flatMap(i=>i.topics||[]),c=[...new Set(r)].reduce((i,a)=>{const l=w[a];return l?i+y(a,l):i},y("all"));o.innerHTML=`<div class="ghp-filters-content">${c}</div>`}function u(e,t,o="",r){const n=m(t);if(n){if(n.className="ghp-projects-grid",n.style.minHeight="auto",e.length===0){n.innerHTML='<p class="ghp-empty-msg">Nenhum projeto encontrado.</p>';return}n.innerHTML=e.map(c=>r?r(c):$(c,o)).join("")}}function j(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 E(e,t,o,r,n){const c=document.getElementById(e);c&&c.addEventListener("click",i=>{const l=i.target.closest(".ghp-filter-btn");if(!l)return;const p=l.dataset.topic,d=p==="all"?t:t.filter(g=>g.topics.includes(p));u(d,o,r,n),c.querySelectorAll(".ghp-filter-btn").forEach(g=>g.classList.remove("active")),l.classList.add("active")})}function B(e,t,o,r){const n=document.getElementById("gh-port-search");n&&n.addEventListener("input",c=>{const a=c.target.value.toLowerCase(),l=e.filter(p=>p.name.toLowerCase().includes(a)||(p.description||"").toLowerCase().includes(a)||p.topics.some(d=>d.toLowerCase().includes(a)));u(l,t,o,r)})}async function P(e,t={searchContainer:"search-cont",filtersContainer:"filters-cont",projectsContainer:"projects-cont"}){try{j(t.searchContainer),S(t.projectsContainer,6);const o=await f(e);return v(o,t.filtersContainer),u(o,t.projectsContainer,e,t.customCardTemplate),E(t.filtersContainer,o,t.projectsContainer,e,t.customCardTemplate),B(o,t.projectsContainer,e,t.customCardTemplate),{projects:o,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}}}s.getPortProjects=f,s.initPortfolio=P,s.renderFilters=v,s.renderProjects=u,s.renderSearchBar=j,Object.defineProperty(s,Symbol.toStringTag,{value:"Module"})}));
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "snapport",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"main": "./dist/snap-port.umd.cjs",
|
|
10
|
+
"module": "./dist/snap-port.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/snap-port.js",
|
|
16
|
+
"require": "./dist/snap-port.umd.cjs"
|
|
17
|
+
},
|
|
18
|
+
"./style.css": "./dist/style.css"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"dev": "vite",
|
|
22
|
+
"build": "tsc && vite build",
|
|
23
|
+
"preview": "vite preview"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^25.2.2",
|
|
27
|
+
"typescript": "~5.9.3",
|
|
28
|
+
"vite": "^7.2.4",
|
|
29
|
+
"vite-plugin-dts": "^4.5.4"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"author": "Guilherme Godoy"
|
|
33
|
+
}
|