Skip to content

Commit c588607

Browse files
authored
Merge pull request #13 from luizomf/feat/use-case-cards
feat: add use-case cards with guided scenarios
2 parents e5e96f6 + 4206f25 commit c588607

4 files changed

Lines changed: 243 additions & 0 deletions

File tree

src/pages/config.astro

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,22 @@ import Footer from '../components/Footer.astro';
1616
</p>
1717
</header>
1818

19+
<!-- Use-case cards -->
20+
<section class="section">
21+
<div class="usecase-cards">
22+
<button class="usecase-card" id="usecase-bastion">
23+
<span class="usecase-card__icon">🏰</span>
24+
<span class="usecase-card__title">Acessar via bastion host</span>
25+
<span class="usecase-card__desc">Dois hosts com ProxyJump configurado</span>
26+
</button>
27+
<button class="usecase-card" id="usecase-multi-server">
28+
<span class="usecase-card__icon">🖥</span>
29+
<span class="usecase-card__title">Múltiplos servidores</span>
30+
<span class="usecase-card__desc">Prod, staging e DB com mesma chave</span>
31+
</button>
32+
</div>
33+
</section>
34+
1935
<!-- Actions bar -->
2036
<section class="section actions-bar">
2137
<button class="btn btn--accent" id="btn-add-host">+ Adicionar Host</button>
@@ -565,6 +581,55 @@ import Footer from '../components/Footer.astro';
565581
if (content) downloadFile(content, 'config');
566582
});
567583

584+
// --- Use-case: bastion host ---
585+
$<HTMLButtonElement>('usecase-bastion').addEventListener('click', () => {
586+
const bastion = createEmptyHost();
587+
bastion.host = 'bastion';
588+
bastion.hostName = 'bastion.exemplo.com';
589+
bastion.user = 'deploy';
590+
bastion.identityFile = '~/.ssh/id_ed25519';
591+
592+
const internal = createEmptyHost();
593+
internal.host = 'db-server';
594+
internal.hostName = '10.0.1.50';
595+
internal.user = 'deploy';
596+
internal.proxyJump = 'bastion';
597+
internal.identityFile = '~/.ssh/id_ed25519';
598+
599+
hosts = [bastion, internal];
600+
render();
601+
// Open both cards
602+
hostsContainer.querySelectorAll('.host-card').forEach((c) => c.classList.add('host-card--open'));
603+
});
604+
605+
// --- Use-case: multi-server ---
606+
$<HTMLButtonElement>('usecase-multi-server').addEventListener('click', () => {
607+
const prod = createEmptyHost();
608+
prod.host = 'prod';
609+
prod.hostName = 'prod.exemplo.com';
610+
prod.user = 'deploy';
611+
prod.port = 22;
612+
prod.identityFile = '~/.ssh/id_ed25519';
613+
614+
const staging = createEmptyHost();
615+
staging.host = 'staging';
616+
staging.hostName = 'staging.exemplo.com';
617+
staging.user = 'deploy';
618+
staging.port = 22;
619+
staging.identityFile = '~/.ssh/id_ed25519';
620+
621+
const db = createEmptyHost();
622+
db.host = 'db';
623+
db.hostName = '10.0.1.50';
624+
db.user = 'admin';
625+
db.proxyJump = 'prod';
626+
db.identityFile = '~/.ssh/id_ed25519';
627+
628+
hosts = [prod, staging, db];
629+
render();
630+
hostsContainer.querySelectorAll('.host-card').forEach((c) => c.classList.add('host-card--open'));
631+
});
632+
568633
// Initial render
569634
render();
570635
</script>

src/pages/keygen.astro

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ import Tooltip from '../components/Tooltip.astro';
1717
</p>
1818
</header>
1919

20+
<!-- Use-case card -->
21+
<section class="section">
22+
<div class="usecase-cards">
23+
<button class="usecase-card" id="usecase-first-key">
24+
<span class="usecase-card__icon">🔑</span>
25+
<span class="usecase-card__title">Minha primeira chave SSH</span>
26+
<span class="usecase-card__desc">Ed25519 com comentário pronto para usar</span>
27+
</button>
28+
</div>
29+
</section>
30+
2031
<!-- Browser warning -->
2132
<div class="warning-banner" id="crypto-warning" hidden>
2233
Seu navegador não suporta geração de chaves Ed25519.
@@ -381,6 +392,21 @@ import Tooltip from '../components/Tooltip.astro';
381392
});
382393
});
383394

395+
// Use-case: "My first SSH key"
396+
$<HTMLButtonElement>('usecase-first-key').addEventListener('click', () => {
397+
// Select Ed25519
398+
currentType = 'ed25519';
399+
typeBtns.forEach((b) => {
400+
b.classList.toggle('type-btn--active', b.dataset.type === 'ed25519');
401+
});
402+
403+
// Fill comment
404+
($<HTMLInputElement>('comment')).value = 'estudante@meu-laptop';
405+
406+
// Auto-generate
407+
btnGenerate.click();
408+
});
409+
384410
// Downloads
385411
$<HTMLButtonElement>('btn-download-pub').addEventListener('click', () => {
386412
if (!currentKey) return;

src/pages/tunnels.astro

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,31 @@ import Tooltip from '../components/Tooltip.astro';
1717
</p>
1818
</header>
1919

20+
<!-- Use-case cards -->
21+
<section class="section">
22+
<div class="usecase-cards">
23+
<button class="usecase-card" data-usecase="remote-db">
24+
<span class="usecase-card__icon">🗄</span>
25+
<span class="usecase-card__title">Acessar banco remoto</span>
26+
<span class="usecase-card__desc">PostgreSQL, MySQL via túnel local</span>
27+
</button>
28+
<button class="usecase-card" data-usecase="expose-local">
29+
<span class="usecase-card__icon">🌐</span>
30+
<span class="usecase-card__title">Expor servidor local</span>
31+
<span class="usecase-card__desc">Compartilhe seu dev server</span>
32+
</button>
33+
<button class="usecase-card" data-usecase="socks-proxy">
34+
<span class="usecase-card__icon">🔒</span>
35+
<span class="usecase-card__title">Navegar via proxy</span>
36+
<span class="usecase-card__desc">SOCKS proxy pela rede remota</span>
37+
</button>
38+
</div>
39+
<div class="usecase-banner" id="usecase-banner" hidden>
40+
<span id="usecase-banner-text"></span>
41+
<button class="usecase-banner__close" id="usecase-banner-close" aria-label="Fechar">&times;</button>
42+
</div>
43+
</section>
44+
2045
<!-- Tunnel Type Selector -->
2146
<section class="section">
2247
<div class="type-selector" id="type-selector">
@@ -536,6 +561,62 @@ import Tooltip from '../components/Tooltip.astro';
536561
});
537562
});
538563

564+
// --- Use-case cards ---
565+
const usecaseBanner = $<HTMLElement>('usecase-banner');
566+
const usecaseBannerText = $<HTMLElement>('usecase-banner-text');
567+
568+
const usecases: Record<string, { type: TunnelType; listenPort: number; destHost: string; destPort: number; server: string; user: string; alias: string; banner: string }> = {
569+
'remote-db': {
570+
type: 'local', listenPort: 5432, destHost: 'db.internal', destPort: 5432,
571+
server: 'bastion.exemplo.com', user: 'deploy', alias: 'db-tunnel',
572+
banner: 'Exemplo carregado — após conectar, use "psql -h localhost -p 5432" para acessar o banco remoto como se fosse local.',
573+
},
574+
'expose-local': {
575+
type: 'remote', listenPort: 8080, destHost: 'localhost', destPort: 3000,
576+
server: 'servidor.exemplo.com', user: 'deploy', alias: 'expose-dev',
577+
banner: 'Exemplo carregado — seu colega poderá acessar seu app local em servidor.exemplo.com:8080.',
578+
},
579+
'socks-proxy': {
580+
type: 'dynamic', listenPort: 1080, destHost: '', destPort: 0,
581+
server: 'proxy.exemplo.com', user: 'usuario', alias: 'socks-proxy',
582+
banner: 'Exemplo carregado — configure seu navegador para usar proxy SOCKS5 em localhost:1080.',
583+
},
584+
};
585+
586+
document.querySelectorAll<HTMLButtonElement>('.usecase-card').forEach((card) => {
587+
card.addEventListener('click', () => {
588+
const uc = usecases[card.dataset.usecase!];
589+
if (!uc) return;
590+
591+
// Switch type (this resets fields and updates labels)
592+
switchType(uc.type);
593+
typeBtns.forEach((b) => {
594+
b.classList.toggle('type-btn--active', b.dataset.type === uc.type);
595+
});
596+
597+
// Fill fields
598+
listenPort.value = String(uc.listenPort);
599+
destHost.value = uc.destHost;
600+
destPort.value = String(uc.destPort);
601+
($<HTMLInputElement>('ssh-server')).value = uc.server;
602+
($<HTMLInputElement>('ssh-user')).value = uc.user;
603+
($<HTMLInputElement>('host-alias')).value = uc.alias;
604+
605+
// Show banner
606+
usecaseBannerText.textContent = uc.banner;
607+
usecaseBanner.hidden = false;
608+
609+
update();
610+
611+
// Scroll to form
612+
$<HTMLElement>('tunnel-form').scrollIntoView({ behavior: 'smooth', block: 'start' });
613+
});
614+
});
615+
616+
$<HTMLElement>('usecase-banner-close').addEventListener('click', () => {
617+
usecaseBanner.hidden = true;
618+
});
619+
539620
// Initial render
540621
update();
541622
</script>

src/styles/global.css

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,8 +392,79 @@ label {
392392
}
393393
}
394394

395+
/* === Use-case Cards === */
396+
.usecase-cards {
397+
display: flex;
398+
gap: 0.5rem;
399+
}
400+
401+
.usecase-card {
402+
flex: 1;
403+
display: flex;
404+
flex-direction: column;
405+
align-items: center;
406+
gap: 0.25rem;
407+
padding: 0.75rem;
408+
background: var(--bg-card);
409+
border: 1px solid var(--border);
410+
border-radius: var(--radius);
411+
cursor: pointer;
412+
transition: all var(--transition);
413+
font-family: var(--font-mono);
414+
text-align: center;
415+
}
416+
417+
.usecase-card:hover {
418+
border-color: var(--accent);
419+
box-shadow: var(--glow-sm);
420+
}
421+
422+
.usecase-card__icon {
423+
font-size: 1.25rem;
424+
}
425+
426+
.usecase-card__title {
427+
font-size: 0.8rem;
428+
font-weight: 700;
429+
color: var(--text-primary);
430+
}
431+
432+
.usecase-card__desc {
433+
font-size: 0.7rem;
434+
color: var(--text-muted);
435+
}
436+
437+
.usecase-banner {
438+
margin-top: 0.75rem;
439+
padding: 0.5rem 0.75rem;
440+
background: var(--accent-glow);
441+
border: 1px solid var(--accent-dim);
442+
border-radius: var(--radius);
443+
font-size: 0.8rem;
444+
color: var(--accent);
445+
display: flex;
446+
align-items: center;
447+
justify-content: space-between;
448+
gap: 0.5rem;
449+
}
450+
451+
.usecase-banner[hidden] {
452+
display: none;
453+
}
454+
455+
.usecase-banner__close {
456+
background: none;
457+
border: none;
458+
color: var(--accent);
459+
font-size: 1.1rem;
460+
cursor: pointer;
461+
padding: 0 0.25rem;
462+
line-height: 1;
463+
}
464+
395465
/* === Responsive === */
396466
@media (max-width: 640px) {
397467
.container { padding: 0 1rem 1rem; }
398468
.form-grid { grid-template-columns: 1fr; }
469+
.usecase-cards { flex-direction: column; }
399470
}

0 commit comments

Comments
 (0)