Skip to content

Commit 3e0eeb2

Browse files
Add StackHost logo branding
1 parent 2af7803 commit 3e0eeb2

5 files changed

Lines changed: 32 additions & 6 deletions

File tree

89.1 KB
Loading

‎web/src/App.vue‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
22
import { computed, onMounted, onUnmounted, ref } from 'vue'
33
import { RouterLink, RouterView, useRoute } from 'vue-router'
4-
import { Activity, FolderKanban, LayoutDashboard, Menu, Server, Settings2, X } from 'lucide-vue-next'
4+
import { Activity, FolderKanban, LayoutDashboard, Menu, Server, Settings2 } from 'lucide-vue-next'
55
import { useAuthStore } from './stores/auth'
66
import ToastContainer from './components/ui/ToastContainer.vue'
77
const route = useRoute(); const auth = useAuthStore(); const open = ref(false)
@@ -17,7 +17,7 @@ onMounted(() => document.addEventListener('keydown', keydown)); onUnmounted(() =
1717
<div v-else class="app">
1818
<div v-if="open" class="mobile-overlay" @click="closeMenu" />
1919
<aside :class="{ open }">
20-
<div class="brand"><span class="mark">S</span><span>StackHost</span><button class="icon-button mobile-close" aria-label="Fechar menu" type="button" @click="closeMenu"><X :size="18" /></button></div>
20+
<div class="brand"><img class="brand-logo" src="/brand/stackhost-logo.png" alt="" /><span>StackHost</span></div>
2121
<nav><RouterLink v-for="link in links" :key="link.to" :to="link.to" @click="closeMenu"><component :is="link.icon" :size="17" aria-hidden="true" />{{ link.label }}</RouterLink></nav>
2222
<div class="sidebar-foot"><span class="dot" /><div><strong>Ambiente local</strong><small>Docker conectado</small></div></div>
2323
<RouterLink class="account-link" to="/settings" @click="closeMenu"><span class="avatar">{{ auth.user?.name?.charAt(0).toUpperCase() || 'S' }}</span><span><strong>{{ auth.user?.name || 'Administrador' }}</strong><small>{{ auth.user?.role === 'admin' ? 'Administrador' : 'Usuário' }}</small></span></RouterLink>

‎web/src/style.css‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ button { cursor: pointer; }
1111
.public-app { min-height: 100vh; padding: 1px 20px; }
1212
aside { position: sticky; top: 0; width: 252px; height: 100vh; flex: 0 0 252px; align-self: flex-start; display: flex; flex-direction: column; overflow-y: auto; border-right: 1px solid #25282e; padding: 26px 16px 18px; background: #141519; }
1313
.brand { display: flex; align-items: center; gap: 10px; margin: 0 10px 54px; font-size: 18px; font-weight: 700; }
14+
.brand-logo { width: 29px; height: 29px; object-fit: contain; border-radius: 8px; }
1415
.mark { display: grid; width: 29px; height: 29px; place-items: center; border-radius: 8px; background: #e8f55b; color: #111; font-weight: 700; }
1516
nav { display: grid; gap: 5px; }
1617
nav a { display: flex; align-items: center; gap: 11px; padding: 11px 13px; color: #858b99; border-radius: 8px; text-decoration: none; transition: color .15s, background .15s; }
@@ -58,6 +59,7 @@ input, textarea, select { width: 100%; border: 1px solid #30343c; border-radius:
5859
textarea { min-height: 88px; resize: vertical; }
5960
input:focus, textarea:focus, select:focus { border-color: #a9b53c; box-shadow: 0 0 0 3px #e8f55b1a; }
6061
.center { max-width: 460px; margin: 8vh auto; }
62+
.public-logo { display: block; width: 58px; height: 58px; margin-bottom: 28px; object-fit: contain; border-radius: 15px; }
6163
.error { min-height: 18px; color: #ef8f8f; }
6264
.error-field { margin: 0; color: #ef8f8f; font-size: 12px; }
6365
.modal-backdrop { position: fixed; z-index: 20; inset: 0; display: grid; place-items: center; padding: 20px; background: #08090bcc; }

‎web/src/views/Login.vue‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,14 @@
1-
<script setup lang="ts">import { ref } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { api, RequestError } from '../composables/useApi'; import { useToast } from '../composables/useToast'; const router = useRouter(); const route = useRoute(); const toast = useToast(); const email = ref(''); const password = ref(''); const error = ref(''); const busy = ref(false); async function submit() { busy.value = true; error.value = ''; try { await api('/api/v1/auth/login', { method: 'POST', body: JSON.stringify({ email: email.value, password: password.value }) }); router.push(typeof route.query.redirect === 'string' ? route.query.redirect : '/') } catch (err) { error.value = err instanceof RequestError ? err.message : 'E-mail ou senha inválidos.'; toast.error(error.value) } finally { busy.value = false } }</script>
2-
<template><div class="center"><p class="kicker">STACKHOST</p><h1>Bem-vindo de volta.</h1><p class="muted">Entre para acessar seu ambiente.</p><form @submit.prevent="submit"><label>E-mail<input v-model="email" type="email" required autofocus autocomplete="email" /></label><label>Senha<input v-model="password" type="password" required autocomplete="current-password" /></label><p class="error">{{ error }}</p><button class="primary" type="submit" :disabled="busy">{{ busy ? 'Entrando…' : 'Entrar no painel' }}</button></form></div></template>
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
import { useRoute, useRouter } from 'vue-router'
4+
import { api, RequestError } from '../composables/useApi'
5+
import { useToast } from '../composables/useToast'
6+
7+
const router = useRouter(); const route = useRoute(); const toast = useToast()
8+
const email = ref(''); const password = ref(''); const error = ref(''); const busy = ref(false)
9+
async function submit() { busy.value = true; error.value = ''; try { await api('/api/v1/auth/login', { method: 'POST', body: JSON.stringify({ email: email.value, password: password.value }) }); router.push(typeof route.query.redirect === 'string' ? route.query.redirect : '/') } catch (err) { error.value = err instanceof RequestError ? err.message : 'E-mail ou senha inválidos.'; toast.error(error.value) } finally { busy.value = false } }
10+
</script>
11+
12+
<template>
13+
<div class="center"><img class="public-logo" src="/brand/stackhost-logo.png" alt="StackHost" /><p class="kicker">STACKHOST</p><h1>Bem-vindo de volta.</h1><p class="muted">Entre para acessar seu ambiente.</p><form @submit.prevent="submit"><label>E-mail<input v-model="email" type="email" required autofocus autocomplete="email" /></label><label>Senha<input v-model="password" type="password" required autocomplete="current-password" /></label><p class="error">{{ error }}</p><button class="primary" type="submit" :disabled="busy">{{ busy ? 'Entrando…' : 'Entrar no painel' }}</button></form></div>
14+
</template>

‎web/src/views/Setup.vue‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,14 @@
1-
<script setup lang="ts">import { onMounted, ref } from 'vue'; import { useRouter } from 'vue-router'; import { api, RequestError } from '../composables/useApi'; import { useToast } from '../composables/useToast'; const router = useRouter(); const toast = useToast(); const step = ref(1); const name = ref(''); const email = ref(''); const password = ref(''); const error = ref(''); const busy = ref(false); const environment = ref<any>({}); const fields = ref<Record<string, string>>({}); onMounted(async () => { environment.value = (await api<any>('/api/v1/setup/status')).infrastructure || {} }); async function submit() { busy.value = true; error.value = ''; fields.value = {}; try { await api('/api/v1/setup/admin', { method: 'POST', body: JSON.stringify({ name: name.value, email: email.value, password: password.value }) }); step.value = 4; toast.success('Administrador criado com sucesso.') } catch (err) { if (err instanceof RequestError) { fields.value = err.fields || {}; error.value = err.message } } finally { busy.value = false } }</script>
2-
<template><div class="center"><p class="kicker">ETAPA {{ step }} DE 4 · CONFIGURAÇÃO</p><div v-if="step === 1"><h1>Seu ambiente, sob controle.</h1><p class="muted">Uma forma simples e segura de cuidar da infraestrutura.</p><div class="empty"><h2>Bem-vindo ao StackHost</h2><p>Vamos criar seu administrador e verificar o ambiente em poucos segundos.</p><button class="primary" type="button" @click="step = 2">Começar</button></div></div><div v-else-if="step === 2"><h1>Crie seu administrador.</h1><p class="muted">Esta conta gerencia este ambiente local.</p><form @submit.prevent="step = 3"><label>Nome<input v-model="name" required autofocus autocomplete="name" /><small v-if="fields.name" class="error-field">{{ fields.name }}</small></label><label>E-mail<input v-model="email" type="email" required autocomplete="email" /><small v-if="fields.email" class="error-field">{{ fields.email }}</small></label><label>Senha<input v-model="password" type="password" minlength="8" required autocomplete="new-password" /><small v-if="fields.password" class="error-field">{{ fields.password }}</small></label><button class="primary" type="submit">Continuar</button></form></div><div v-else-if="step === 3"><h1>Verifique o ambiente.</h1><p class="muted">O Docker é opcional para criar sua conta e pode ser conectado depois.</p><div class="status"><span class="dot" :class="{ offline: !environment.available }" /><div><strong>{{ environment.available ? 'Docker conectado' : 'Docker opcional' }}</strong><p>{{ environment.available ? `Docker ${environment.engine_version || ''} · ${environment.operating_system || 'sistema não informado'} · ${environment.cpus || 'Não disponível'} CPUs` : 'O painel pode continuar sem Docker.' }}</p></div><span class="pill">{{ environment.swarm?.active ? 'Swarm pronto' : 'Sem Swarm' }}</span></div><button class="primary" type="button" :disabled="busy" @click="submit">{{ busy ? 'Criando…' : 'Criar administrador' }}</button></div><div v-else><h1>Tudo pronto.</h1><p class="muted">Seu administrador está ativo.</p><div class="empty"><h2>Bem-vindo ao StackHost</h2><p>Agora você pode começar a organizar seus projetos.</p><button class="primary" type="button" @click="router.push('/')">Abrir painel</button></div></div><p class="error">{{ error }}</p></div></template>
1+
<script setup lang="ts">
2+
import { onMounted, ref } from 'vue'
3+
import { useRouter } from 'vue-router'
4+
import { api, RequestError } from '../composables/useApi'
5+
import { useToast } from '../composables/useToast'
6+
7+
const router = useRouter(); const toast = useToast(); const step = ref(1); const name = ref(''); const email = ref(''); const password = ref(''); const error = ref(''); const busy = ref(false); const environment = ref<any>({}); const fields = ref<Record<string, string>>({})
8+
onMounted(async () => { environment.value = (await api<any>('/api/v1/setup/status')).infrastructure || {} })
9+
async function submit() { busy.value = true; error.value = ''; fields.value = {}; try { await api('/api/v1/setup/admin', { method: 'POST', body: JSON.stringify({ name: name.value, email: email.value, password: password.value }) }); step.value = 4; toast.success('Administrador criado com sucesso.') } catch (err) { if (err instanceof RequestError) { fields.value = err.fields || {}; error.value = err.message } } finally { busy.value = false } }
10+
</script>
11+
12+
<template>
13+
<div class="center"><img class="public-logo" src="/brand/stackhost-logo.png" alt="StackHost" /><p class="kicker">ETAPA {{ step }} DE 4 · CONFIGURAÇÃO</p><div v-if="step === 1"><h1>Seu ambiente, sob controle.</h1><p class="muted">Uma forma simples e segura de cuidar da infraestrutura.</p><div class="empty"><h2>Bem-vindo ao StackHost</h2><p>Vamos criar seu administrador e verificar o ambiente em poucos segundos.</p><button class="primary" type="button" @click="step = 2">Começar</button></div></div><div v-else-if="step === 2"><h1>Crie seu administrador.</h1><p class="muted">Esta conta gerencia este ambiente local.</p><form @submit.prevent="step = 3"><label>Nome<input v-model="name" required autofocus autocomplete="name" /><small v-if="fields.name" class="error-field">{{ fields.name }}</small></label><label>E-mail<input v-model="email" type="email" required autocomplete="email" /><small v-if="fields.email" class="error-field">{{ fields.email }}</small></label><label>Senha<input v-model="password" type="password" minlength="8" required autocomplete="new-password" /><small v-if="fields.password" class="error-field">{{ fields.password }}</small></label><button class="primary" type="submit">Continuar</button></form></div><div v-else-if="step === 3"><h1>Verifique o ambiente.</h1><p class="muted">O Docker é opcional para criar sua conta e pode ser conectado depois.</p><div class="status"><span class="dot" :class="{ offline: !environment.available }" /><div><strong>{{ environment.available ? 'Docker conectado' : 'Docker opcional' }}</strong><p>{{ environment.available ? `Docker ${environment.engine_version || ''} · ${environment.operating_system || 'sistema não informado'} · ${environment.cpus || 'Não disponível'} CPUs` : 'O painel pode continuar sem Docker.' }}</p></div><span class="pill">{{ environment.swarm?.active ? 'Swarm pronto' : 'Sem Swarm' }}</span></div><button class="primary" type="button" :disabled="busy" @click="submit">{{ busy ? 'Criando…' : 'Criar administrador' }}</button></div><div v-else><h1>Tudo pronto.</h1><p class="muted">Seu administrador está ativo.</p><div class="empty"><h2>Bem-vindo ao StackHost</h2><p>Agora você pode começar a organizar seus projetos.</p><button class="primary" type="button" @click="router.push('/')">Abrir painel</button></div></div><p class="error">{{ error }}</p></div>
14+
</template>

0 commit comments

Comments
 (0)