feat(ui): moderniza home/geral com estilo glass e tabela moderna

This commit is contained in:
Eduardo 2025-12-16 23:17:53 -03:00
parent b66eb96879
commit a4fb34146d
15 changed files with 2360 additions and 654 deletions

View File

@ -3,10 +3,11 @@ import { Home } from './pages/home/home';
import { Register } from './pages/register/register'; import { Register } from './pages/register/register';
import { LoginComponent } from './pages/login/login'; import { LoginComponent } from './pages/login/login';
import { Geral } from './pages/geral/geral'; import { Geral } from './pages/geral/geral';
import { authGuard } from './guards/auth.guard';
export const routes: Routes = [ export const routes: Routes = [
{ path: '', component: Home }, { path: '', component: Home },
{ path: "register", component: Register }, { path: "register", component: Register },
{ path: "login", component: LoginComponent }, { path: "login", component: LoginComponent },
{ path: "geral", component: Geral }, { path: 'geral', component: Geral, canActivate: [authGuard] },
]; ];

View File

@ -1,21 +1,31 @@
<div class="feature-card-container"> <div class="feature-card-container">
<!-- Use [ngStyle] para aplicar o alinhamento a todo o bloco do card --> <div
<div class="feature-card" class="feature-card"
(mouseenter)="isHovered = true" (mouseenter)="isHovered = true"
(mouseleave)="isHovered = false" (mouseleave)="isHovered = false"
[ngClass]="{'hover-state': isHovered}" [ngClass]="{ 'hover-state': isHovered }"
[ngStyle]="{'text-align': textAlign}"> <!-- 🎯 APLICA O ALINHAMENTO AQUI --> [ngStyle]="{ 'text-align': textAlign }"
>
<!-- brilho sutil -->
<span class="shine" aria-hidden="true"></span>
<!-- Ícone e Título Principal (em rosa) --> <!-- header -->
<h3 class="card-title"> <div class="card-head">
<i class="card-icon {{ iconClass }}"></i> <div class="icon-wrap" aria-hidden="true">
{{ title }} <i class="card-icon {{ iconClass }}"></i>
</h3> </div>
<!-- Conteúdo/Descrição do Card --> <h3 class="card-title">
{{ title }}
</h3>
</div>
<!-- descrição -->
<p class="card-description"> <p class="card-description">
<span [innerHTML]="description"></span> <span [innerHTML]="description"></span>
</p> </p>
<!-- linha decorativa -->
<div class="card-accent" aria-hidden="true"></div>
</div> </div>
</div> </div>

View File

@ -1,136 +1,250 @@
.feature-card-container { :host {
padding: 10px; --brand: #E33DCF;
--text: #111214;
--muted: rgba(17, 18, 20, 0.70);
--radius-xl: 22px;
--radius-lg: 16px;
display: block;
} }
/* CARD BASE — mantém o design exato do Figma em desktops */ /* ✅ CARD PRINCIPAL */
.feature-card { .feature-card {
border: 1px solid #000000; position: relative;
background-color: #FFFFFF; overflow: hidden;
padding: 30px; width: 365px;
border-radius: 5px; min-height: 175px;
padding: 18px 18px 16px 18px;
border-radius: 18px;
cursor: default; cursor: default;
/* DESKTOP (layout original) */ background: rgba(255, 255, 255, 0.82);
width: 365px; border: 1px solid rgba(227, 61, 207, 0.18);
height: 169px; backdrop-filter: blur(10px);
box-shadow: 0 18px 38px rgba(17, 18, 20, 0.10);
overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; gap: 10px;
transition: background-color 0.2s ease-in-out; transform: translateZ(0);
transition:
transform 180ms ease,
border-color 180ms ease,
box-shadow 180ms ease,
background 180ms ease;
/* borda interna sutil */
&::before {
content: '';
position: absolute;
inset: 1px;
border-radius: 17px;
pointer-events: none;
border: 1px solid rgba(255, 255, 255, 0.65);
opacity: 0.75;
}
/* efeito “radial” suave no fundo */
&::after {
content: '';
position: absolute;
inset: -80px;
pointer-events: none;
background:
radial-gradient(380px 200px at 20% 10%, rgba(227, 61, 207, 0.16), transparent 60%),
radial-gradient(320px 180px at 85% 70%, rgba(227, 61, 207, 0.10), transparent 60%);
opacity: 0.85;
}
/* ================================ /* ================================
📱 RESPONSIVIDADE 📱 RESPONSIVIDADE
================================ */ ================================ */
@media (min-width: 1200px) and (max-width: 1399.98px) { @media (min-width: 1200px) and (max-width: 1399.98px) {
width: 330px; width: 330px;
height: auto; min-height: 170px;
padding: 24px;
} }
/* Telas MUITO pequenas (≤ 360px) */
@media (max-width: 360px) {
width: 95%;
height: auto;
padding: 20px;
}
/* Celulares (≤ 480px) */
@media (max-width: 480px) {
width: 95%;
height: auto; /* Permite texto quebrar */
}
/* Tablets pequenos (≤ 768px) */
@media (max-width: 768px) {
width: 300px;
height: auto;
}
/* Tablets grandes e monitores pequenos (≤ 1024px) */
@media (max-width: 1024px) { @media (max-width: 1024px) {
width: 330px; width: 330px;
height: auto; min-height: 170px;
}
@media (max-width: 768px) {
width: 300px;
min-height: 170px;
}
@media (max-width: 480px) {
width: 95%;
min-height: 170px;
}
@media (max-width: 360px) {
width: 95%;
min-height: 165px;
padding: 16px;
} }
} }
/* HOVER */ /* HOVER PREMIUM (sem “cinza chapado”) */
.feature-card.hover-state { .feature-card.hover-state {
background-color: #E1E1E1; transform: translateY(-4px);
border-color: rgba(227, 61, 207, 0.32);
box-shadow: 0 26px 52px rgba(17, 18, 20, 0.14);
background: rgba(255, 255, 255, 0.90);
} }
/* ------------------------- /* brilho animado (fica bem SaaS) */
TÍTULO DO CARD .shine {
-------------------------- */ position: absolute;
.card-title { inset: -40%;
font-family: 'Inter', sans-serif; pointer-events: none;
font-size: 20px; transform: translateX(-120%) rotate(12deg);
font-weight: 700; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.55), transparent);
color: #E33DCF; opacity: 0.0;
transition: opacity 200ms ease;
z-index: 1;
}
margin-top: -15px; .feature-card.hover-state .shine {
margin-bottom: 10px; opacity: 1;
animation: shine 1.25s ease-in-out 1;
}
/* header */
.card-head {
position: relative;
z-index: 2;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; gap: 12px;
justify-content: center; /* seu layout atual é centralizado */
@media (min-width: 1200px) and (max-width: 1399.98px) {
font-size: 18px;
margin-top: -10px;
}
/* Mobile */
@media (max-width: 480px) {
font-size: 18px;
margin-top: -5px;
}
} }
/* ÍCONE */ /* ícone no estilo do mock */
.icon-wrap {
width: 42px;
height: 42px;
border-radius: 14px;
display: grid;
place-items: center;
background: rgba(227, 61, 207, 0.10);
border: 1px solid rgba(227, 61, 207, 0.22);
transition: transform 180ms ease, box-shadow 180ms ease, background 180ms ease;
}
.feature-card.hover-state .icon-wrap {
transform: translateY(-1px) scale(1.03);
box-shadow: 0 12px 24px rgba(227, 61, 207, 0.18);
background: rgba(227, 61, 207, 0.14);
}
/* ícone */
.card-icon { .card-icon {
margin-right: 10px; font-size: 18px;
font-size: 24px; color: var(--brand);
@media (min-width: 1200px) and (max-width: 1399.98px) {
font-size: 22px;
}
/* Mobile */
@media (max-width: 480px) {
font-size: 20px;
}
} }
/* ------------------------- /* título */
DESCRIÇÃO DO CARD .card-title {
-------------------------- */ position: relative;
.card-description { z-index: 2;
margin: 0;
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
font-size: 17px; font-size: 18px;
color: #000000; font-weight: 950;
line-height: 1; color: var(--text);
padding-left: 15px; letter-spacing: -0.2px;
@media (min-width: 1200px) and (max-width: 1399.98px) { transition: transform 180ms ease, color 180ms ease;
font-size: 16px; }
line-height: 1.2;
}
.feature-card.hover-state .card-title {
transform: translateY(-1px);
}
/* Mobile */ /* descrição */
.card-description {
position: relative;
z-index: 2;
margin: 0;
padding: 0 6px;
font-family: 'Inter', sans-serif;
font-size: 14px;
line-height: 1.35;
color: var(--muted);
/* deixa mais “SaaS” e menos apertado */
@media (max-width: 480px) { @media (max-width: 480px) {
font-size: 15px; font-size: 13.5px;
padding-left: 8px; padding: 0 2px;
} }
} }
.card-description strong { .card-description strong {
font-weight: 700; font-weight: 900;
color: #111214;
}
/* linha decorativa inferior */
.card-accent {
position: relative;
z-index: 2;
margin-top: auto;
height: 6px;
border-radius: 999px;
background: rgba(227, 61, 207, 0.12);
border: 1px solid rgba(227, 61, 207, 0.18);
overflow: hidden;
&::after {
content: '';
position: absolute;
inset: 0;
transform: translateX(-60%);
background: linear-gradient(90deg, transparent, rgba(227, 61, 207, 0.28), transparent);
opacity: 0.0;
transition: opacity 180ms ease;
}
}
.feature-card.hover-state .card-accent::after {
opacity: 1;
animation: sweep 1.2s ease-in-out 1;
}
/* acessibilidade: reduz animações */
@media (prefers-reduced-motion: reduce) {
.feature-card,
.icon-wrap,
.card-title {
transition: none !important;
}
.feature-card.hover-state {
transform: none;
}
.feature-card.hover-state .shine,
.feature-card.hover-state .card-accent::after {
animation: none !important;
}
}
/* animações */
@keyframes shine {
0% { transform: translateX(-120%) rotate(12deg); }
100% { transform: translateX(120%) rotate(12deg); }
}
@keyframes sweep {
0% { transform: translateX(-60%); }
100% { transform: translateX(60%); }
} }

View File

@ -1,16 +1,15 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common'; // <-- Importe aqui! import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'app-feature-card', selector: 'app-feature-card',
standalone: true, // <-- DEVE ser standalone standalone: true,
imports: [CommonModule], // <-- PRECISA do CommonModule para usar [ngClass] imports: [CommonModule],
templateUrl: './feature-card.html', templateUrl: './feature-card.html',
styleUrls: ['./feature-card.scss'] styleUrls: ['./feature-card.scss']
}) })
export class FeatureCardComponent implements OnInit { export class FeatureCardComponent implements OnInit {
// ... o restante da lógica ...
@Input() title: string = ''; @Input() title: string = '';
@Input() description: string = ''; @Input() description: string = '';
@Input() iconClass: string = ''; @Input() iconClass: string = '';
@ -18,7 +17,7 @@ export class FeatureCardComponent implements OnInit {
isHovered: boolean = false; isHovered: boolean = false;
constructor() { } constructor() {}
ngOnInit(): void { } ngOnInit(): void {}
} }

View File

@ -1,52 +1,115 @@
<header <header
class="header-container" class="header-container"
[class.header-scrolled]="isScrolled"> [class.header-scrolled]="isScrolled"
>
<div class="header-top"> <div class="header-top">
<!-- ESQUERDA: HAMBURGUER (só no /geral) + LOGO -->
<div class="left-area">
<button
*ngIf="isLoggedHeader"
type="button"
class="hamburger-btn"
aria-label="Abrir menu"
(click)="toggleMenu()"
>
<i class="bi bi-list"></i>
</button>
<!-- LOGO + TÍTULO (CLICÁVEIS) --> <!-- ✅ Logo some apenas quando menu estiver aberto (no /geral) -->
<a class="logo-area" routerLink="/"> <!-- ⬅️ AGORA É UM LINK ANGULAR --> <a class="logo-area" routerLink="/" *ngIf="!menuOpen">
<img src="logo.png" alt="Logo" class="logo"> <img src="logo.png" alt="Logo" class="logo" />
<div class="logo-text ms-2">
<span class="line">Line</span><span class="gestao">Gestão</span>
</div>
</a>
</div>
<div class="logo-text ms-2"> <!-- ✅ MENU HOME: só aparece fora do /geral -->
<span class="line">Line</span><span class="gestao">Gestão</span> <nav class="menu" *ngIf="!isLoggedHeader">
</div>
</a>
<!-- MENU -->
<nav class="menu">
<a href="https://www.linemovel.com.br/sobrenos" class="menu-item" target="_blank">O que é a Line Móvel?</a> <a href="https://www.linemovel.com.br/sobrenos" class="menu-item" target="_blank">O que é a Line Móvel?</a>
<a href="https://www.linemovel.com.br/empresas" class="menu-item" target="_blank">Para sua empresa</a> <a href="https://www.linemovel.com.br/empresas" class="menu-item" target="_blank">Para sua empresa</a>
<a href="https://www.linemovel.com.br/proposta" class="menu-item" target="_blank">Solicite sua Proposta</a> <a href="https://www.linemovel.com.br/proposta" class="menu-item" target="_blank">Solicite sua Proposta</a>
<a href="https://www.linemovel.com.br/indique" class="menu-item" target="_blank">Indique um amigo</a> <a href="https://www.linemovel.com.br/indique" class="menu-item" target="_blank">Indique um amigo</a>
</nav> </nav>
<!-- BOTÕES --> <!-- ✅ BOTÕES: só aparecem fora do /geral -->
<div class="btn-area"> <div class="btn-area" *ngIf="!isLoggedHeader">
<button <button type="button" class="btn btn-cadastrar" [routerLink]="['/register']">
type="button"
class="btn btn-cadastrar"
[routerLink]="['/register']">
Cadastre-se Cadastre-se
</button> </button>
<button <button type="button" class="btn btn-login" [routerLink]="['/login']">
type="button"
class="btn btn-login"
[routerLink]="['/login']">
Login Login
</button> </button>
</div> </div>
</div> </div>
<!-- FAIXA AZUL SÓ NA HOME --> <!-- FAIXA AZUL SÓ NA HOME -->
<div class="header-bar" *ngIf="isHome"> <div class="header-bar" *ngIf="isHome">
<span class="header-bar-text"> <span class="header-bar-text">
Somos a escolha certa para estar sempre conectado! Somos a escolha certa para estar sempre conectado!
</span> </span>
</div> </div>
</header> </header>
<!-- OVERLAY -->
<div class="menu-overlay" *ngIf="menuOpen" (click)="closeMenu()"></div>
<!-- MENU LATERAL -->
<aside class="side-menu" [class.open]="menuOpen" (click)="$event.stopPropagation()">
<div class="side-menu-header">
<a class="logo-area" routerLink="/" (click)="closeMenu()">
<img src="logo.png" alt="Logo" class="logo" />
<div class="logo-text ms-2">
<span class="line">Line</span><span class="gestao">Gestão</span>
</div>
</a>
<button
type="button"
class="close-btn"
aria-label="Fechar menu"
(click)="closeMenu()"
>
<i class="bi bi-x-lg"></i>
</button>
</div>
<div class="side-menu-body">
<!-- ✅ Opções do Geral (mantém) -->
<a routerLink="/geral" class="side-item" (click)="closeMenu()">
<i class="bi bi-clipboard-data me-2"></i> Controle de Contratos
</a>
<a routerLink="/geral" class="side-item" (click)="closeMenu()">
<i class="bi bi-sim me-2"></i> Gerenciar Linhas
</a>
<a routerLink="/geral" class="side-item" (click)="closeMenu()">
<i class="bi bi-people me-2"></i> Gerenciar Clientes
</a>
<a routerLink="/geral" class="side-item" (click)="closeMenu()">
<i class="bi bi-bar-chart me-2"></i> Relatórios
</a>
<hr class="my-2" />
<!-- ✅ Links da home só aqui dentro (no /geral) -->
<a href="https://www.linemovel.com.br/sobrenos" target="_blank" class="side-item" (click)="closeMenu()">
<i class="bi bi-info-circle me-2"></i> O que é a Line Móvel?
</a>
<a href="https://www.linemovel.com.br/empresas" target="_blank" class="side-item" (click)="closeMenu()">
<i class="bi bi-building me-2"></i> Para sua empresa
</a>
<a href="https://www.linemovel.com.br/proposta" target="_blank" class="side-item" (click)="closeMenu()">
<i class="bi bi-file-earmark-text me-2"></i> Solicite sua Proposta
</a>
<a href="https://www.linemovel.com.br/indique" target="_blank" class="side-item" (click)="closeMenu()">
<i class="bi bi-megaphone me-2"></i> Indique um amigo
</a>
</div>
</aside>

View File

@ -1,380 +1,420 @@
/* ===================== */ :host {
/* HEADER PRINCIPAL */ --brand: #E33DCF;
/* ===================== */ --brand-2: rgba(227, 61, 207, 0.18);
.header-container { --brand-3: rgba(227, 61, 207, 0.10);
position: sticky;
top: 0;
z-index: 1000;
width: 100%; --text: #0b0b0f;
/* mais transparente mesmo no topo */ --muted: rgba(0, 0, 0, 0.66);
background: rgba(255, 255, 255, 0.75);
font-family: 'Inter', sans-serif;
display: flex;
flex-direction: column;
backdrop-filter: blur(6px); --border: rgba(0, 0, 0, 0.08);
-webkit-backdrop-filter: blur(6px);
transition: --shadow: 0 18px 45px rgba(0, 0, 0, 0.10);
background-color 0.25s ease, --shadow-soft: 0 10px 26px rgba(0, 0, 0, 0.08);
backdrop-filter 0.25s ease,
-webkit-backdrop-filter 0.25s ease,
box-shadow 0.25s ease,
border-color 0.25s ease;
} }
/* HEADER “FOSCO” QUANDO SCROLLADO BEM TRANSPARENTE */ /* =============================== */
.header-container.header-scrolled { /* PÁGINA / FUNDO GLOBAL */
background: rgba(255, 255, 255, 0.22); /* quase vidro puro */ /* =============================== */
backdrop-filter: blur(14px); .home-page {
-webkit-backdrop-filter: blur(14px); position: relative;
min-height: 100vh;
background: #efefef;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12); /* 🔑 evita o background “atrapalhar” o conteúdo */
border-bottom: 1px solid rgba(201, 30, 181, 0.3); isolation: isolate;
} }
/* BACKGROUND GLOBAL (viewport inteiro) */
.page-bg {
position: fixed;
inset: 0;
z-index: -1; /* 🔑 sempre atrás */
pointer-events: none;
.grid {
position: absolute;
inset: -40px;
opacity: 0.30;
background:
radial-gradient(circle at 25% 25%, var(--brand-2), transparent 45%),
radial-gradient(circle at 80% 30%, var(--brand-3), transparent 55%),
radial-gradient(circle at 45% 90%, var(--brand-2), transparent 50%),
repeating-linear-gradient(
90deg,
rgba(0, 0, 0, 0.05) 0px,
rgba(0, 0, 0, 0.05) 1px,
transparent 1px,
transparent 46px
),
repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.04) 0px,
rgba(0, 0, 0, 0.04) 1px,
transparent 1px,
transparent 46px
);
/* ❌ SEM blur */
transform: translateZ(0);
animation: gridMove 12s ease-in-out infinite;
}
.blob {
position: absolute;
width: 520px;
height: 520px;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, var(--brand), transparent 62%);
opacity: 0.16; /* um pouco mais leve sem blur */
/* ❌ SEM blur */
transform: translateZ(0);
animation: floaty 9s ease-in-out infinite;
}
/* ===================== */ .blob-1 {
/* TOP AREA (LOGO + MENU + BOTÕES) */ top: -180px;
/* ===================== */ left: -180px;
.header-top { }
width: 100%;
height: 72px;
padding: 0 32px;
display: flex; .blob-2 {
bottom: -220px;
right: -220px;
opacity: 0.12;
animation-duration: 11s;
}
}
/* =============================== */
/* HERO */
/* =============================== */
.hero {
position: relative;
overflow: hidden;
padding: clamp(48px, 5vw, 70px) 0 20px;
}
.hero-content {
text-align: center;
max-width: 980px;
margin: 0 auto;
}
/* =============================== */
/* KICKER */
/* =============================== */
.kicker {
display: inline-flex;
align-items: center; align-items: center;
justify-content: space-between; /* Alinha logo / menu / botões */ gap: 10px;
gap: 40px;
@media (max-width: 900px) { padding: 10px 14px;
gap: 20px; border-radius: 999px;
} background: rgba(255, 255, 255, 0.65);
border: 1px solid var(--border);
box-shadow: var(--shadow-soft);
@media (max-width: 768px) { color: var(--muted);
padding: 0 20px; font-family: 'Poppins', sans-serif;
} font-size: 14px;
@media (max-width: 600px) { i {
flex-direction: column; color: var(--brand);
height: auto;
gap: 12px;
padding: 12px 0;
}
}
/* ===================== */
/* LOGO */
/* ===================== */
.logo {
width: 44px;
height: 44px;
/* NOTEBOOKS 12001399px */
@media (min-width: 1200px) and (max-width: 1399.98px) {
width: 38px;
height: 38px;
}
@media (max-width: 1280px) {
width: 38px;
height: 38px;
}
@media (max-width: 1024px) {
width: 28px;
height: 28px;
}
@media (max-width: 480px) {
width: 26px;
height: 26px;
}
}
.logo-area {
display: flex;
align-items: center;
gap: 8px;
text-decoration: none; /* ⬅️ tira sublinhado */
color: inherit; /* ⬅️ usa as cores definidas nos spans */
cursor: pointer; /* ⬅️ deixa com “carinha” de botão/link */
}
/* TEXTO DA LOGO */
.logo-text .line,
.logo-text .gestao {
font-weight: 600;
font-size: 32px; /* desktop grande */
/* NOTEBOOKS 12001399px */
@media (min-width: 1200px) and (max-width: 1399.98px) {
font-size: 26px;
}
@media (max-width: 1200px) {
font-size: 19px;
}
@media (max-width: 1024px) {
font-size: 18px;
}
@media (max-width: 900px) {
font-size: 17px;
}
@media (max-width: 768px) {
font-size: 16px; font-size: 16px;
} }
@media (max-width: 480px) {
font-size: 15px;
}
} }
.logo-text .line { color: #030FAA; } /* =============================== */
.logo-text .gestao { color: #000000; } /* TÍTULO PRINCIPAL */
/* =============================== */
.main-title {
font-family: 'Inter', sans-serif;
font-size: clamp(28px, 4.6vw, 54px);
line-height: 1.08;
margin: 22px auto 22px;
/* ===================== */
/* MENU */
/* ===================== */
.menu {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
gap: 24px; width: fit-content;
@media (max-width: 1200px) { .first-line,
gap: 18px; .second-line {
color: var(--brand);
font-weight: 600;
display: block;
text-align: center;
letter-spacing: -0.02em;
} }
@media (max-width: 1024px) { .second-line strong {
gap: 14px; background: linear-gradient(90deg, var(--brand), rgba(227, 61, 207, 0.55));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
position: relative;
} }
@media (max-width: 900px) { .second-line strong::after {
gap: 12px; content: "";
} position: absolute;
left: -6px;
@media (max-width: 768px) { right: -6px;
gap: 10px; bottom: -6px;
} height: 10px;
border-radius: 999px;
@media (max-width: 600px) { background: rgba(227, 61, 207, 0.18);
flex-wrap: wrap; z-index: -1;
justify-content: center; transform: scaleX(0);
gap: 10px; transform-origin: left;
width: 100%; animation: underlineGrow 900ms ease forwards;
animation-delay: 700ms;
} }
} }
.menu-item { /* deslocamento em telas grandes */
font-size: 16px; @media (min-width: 1200px) {
font-weight: 600; .main-title .first-line { transform: translateX(-70px); }
color: #000 !important; .main-title .second-line { transform: translateX(100px) translateY(-6px); }
text-decoration: none !important;
/* NOTEBOOKS 12001399px */
@media (min-width: 1200px) and (max-width: 1399.98px) {
font-size: 14.5px;
}
@media (max-width: 1200px) {
font-size: 14px;
}
@media (max-width: 1024px) {
font-size: 13.5px;
}
@media (max-width: 900px) {
font-size: 13px;
}
@media (max-width: 768px) {
font-size: 12.5px;
}
@media (max-width: 600px) {
font-size: 12px;
}
} }
.menu-item:hover { @media (max-width: 1199.98px) {
color: #030FAA !important; .main-title .first-line,
.main-title .second-line { transform: none; }
} }
/* ===================== */ /* =============================== */
/* BOTÕES */ /* PARÁGRAFO PRINCIPAL */
/* ===================== */ /* =============================== */
.btn-area { .main-paragraph {
display: flex; width: min(980px, 92%);
align-items: center; margin: 0 auto 26px;
gap: 12px;
margin-left: 0 !important;
@media (max-width: 900px) { font-family: 'Poppins', sans-serif;
gap: 10px; font-size: clamp(15px, 1.55vw, 20px);
} color: var(--text);
line-height: 1.45;
font-weight: 400;
text-align: center;
@media (max-width: 600px) { strong { font-weight: 700; }
width: 100%;
justify-content: center;
}
} }
/* --- Botão Cadastre-se --- */ .brand-name { color: var(--brand); }
.btn-cadastrar {
width: 164px;
height: 41px;
background: #E1E1E1;
border-radius: 8px;
color: #000;
font-size: 16px;
font-weight: 600;
border: none;
cursor: pointer;
transition: background-color 0.15s ease, transform 0.08s ease;
@media (min-width: 1200px) and (max-width: 1399.98px) { .highlight {
width: 140px; color: var(--text);
height: 36px; position: relative;
font-size: 14px; padding: 0 2px;
}
@media (max-width: 1024px) {
width: 140px;
height: 36px;
font-size: 14px;
}
@media (max-width: 900px) {
width: 135px;
height: 36px;
font-size: 14px;
}
@media (max-width: 768px) {
width: 120px;
height: 34px;
font-size: 13px;
}
@media (max-width: 600px) {
width: 45%;
}
} }
.btn-cadastrar:hover { .highlight::after {
background: #d7d7d7; content: "";
position: absolute;
left: -2px;
right: -2px;
bottom: 2px;
height: 10px;
background: rgba(227, 61, 207, 0.12);
border-radius: 999px;
z-index: -1;
} }
.btn-cadastrar:active { /* =============================== */
background: rgba(225, 225, 225, 0.7) !important; /* AÇÕES / LINKS */
transform: scale(0.98); /* =============================== */
} .hero-actions {
margin-top: 10px;
/* --- Botão Login --- */
.btn-login {
width: 164px;
height: 41px;
background: #E33DCF;
border-radius: 8px;
color: #fff;
font-size: 16px;
font-weight: 600;
border: none;
cursor: pointer;
transition: background-color 0.15s ease, transform 0.08s ease;
@media (min-width: 1200px) and (max-width: 1399.98px) {
width: 140px;
height: 36px;
font-size: 14px;
}
@media (max-width: 1024px) {
width: 150px;
height: 38px;
font-size: 15px;
}
@media (max-width: 900px) {
width: 135px;
height: 36px;
font-size: 14px;
}
@media (max-width: 768px) {
width: 120px;
height: 34px;
font-size: 13px;
}
@media (max-width: 600px) {
width: 45%;
}
}
/* garante que o texto SEMPRE fique branco */
.btn-login,
.btn-login:hover,
.btn-login:active,
.btn-login:focus {
color: #fff !important;
}
.btn-login:hover {
background: #d72bd0;
}
.btn-login:active {
background: rgba(227, 61, 207, 0.8) !important;
transform: scale(0.98);
}
/* ===================== */
/* FAIXA AZUL INFERIOR */
/* ===================== */
.header-bar {
width: 100%;
height: 34px; /* um pouco mais baixa */
background: linear-gradient(90deg, #030FAA 0%, #6066FF 45%, #C91EB5 100%);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 14px;
border-top: 1px solid rgba(255, 255, 255, 0.25); flex-wrap: wrap;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
@media (max-width: 480px) {
height: 30px;
}
} }
.header-bar-text { .secondary-link {
color: #ffffff; display: inline-flex;
font-size: 13px; align-items: center;
gap: 8px;
color: rgba(0, 0, 0, 0.72);
font-family: 'Poppins', sans-serif;
font-weight: 600; font-weight: 600;
letter-spacing: 0.08em; text-decoration: none;
text-transform: uppercase;
/* “pill” para destacar o texto */ padding: 12px 14px;
padding: 4px 14px; border-radius: 12px;
border: 1px solid var(--border);
background: rgba(255, 255, 255, 0.55);
box-shadow: var(--shadow-soft);
transition: transform 0.22s ease, box-shadow 0.22s ease;
}
.secondary-link:hover {
transform: translateY(-2px);
box-shadow: var(--shadow);
}
.hero-badges {
margin-top: 18px;
display: flex;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
}
.badge-pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 12px;
border-radius: 999px; border-radius: 999px;
background: rgba(0, 0, 0, 0.12);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
@media (max-width: 480px) { background: rgba(255, 255, 255, 0.70);
font-size: 11.5px; border: 1px solid var(--border);
padding: 3px 10px; box-shadow: var(--shadow-soft);
letter-spacing: 0.06em;
text-align: center; font-family: 'Poppins', sans-serif;
font-size: 13px;
color: rgba(0, 0, 0, 0.75);
i { color: var(--brand); }
}
/* =============================== */
/* BENEFÍCIOS */
/* =============================== */
.hero-benefits {
margin: 20px auto 0;
width: min(820px, 96%);
display: grid;
gap: 10px;
.benefit {
display: flex;
gap: 10px;
align-items: flex-start;
padding: 12px 14px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.58);
border: 1px solid var(--border);
box-shadow: var(--shadow-soft);
i {
color: var(--brand);
font-size: 18px;
margin-top: 1px;
}
span {
font-family: 'Poppins', sans-serif;
color: rgba(0, 0, 0, 0.78);
font-size: 14px;
line-height: 1.35;
text-align: left;
}
}
@media (min-width: 820px) {
grid-template-columns: repeat(3, 1fr);
} }
} }
/* =============================== */
/* FEATURES */
/* =============================== */
.section-head {
text-align: center;
margin: 0 auto 24px;
max-width: 820px;
}
.section-title {
font-family: 'Inter', sans-serif;
font-size: clamp(20px, 2.4vw, 30px);
color: rgba(0, 0, 0, 0.88);
margin-bottom: 8px;
letter-spacing: -0.02em;
}
.section-subtitle {
font-family: 'Poppins', sans-serif;
color: rgba(0, 0, 0, 0.64);
font-size: 15px;
margin: 0 auto;
}
.feature-cards-row { margin-top: 18px; }
.feature-item {
border-radius: 18px;
padding: 10px;
background: transparent;
transition: transform 0.25s ease, filter 0.25s ease;
}
.feature-item:hover {
transform: translateY(-6px);
filter: drop-shadow(0 14px 22px rgba(0, 0, 0, 0.10));
}
.button-section {
margin-top: 18px;
padding-bottom: 30px;
}
/* =============================== */
/* REVEAL (scroll) */
/* =============================== */
.reveal {
opacity: 0;
transform: translateY(18px) scale(0.98);
transition:
opacity 650ms ease,
transform 650ms ease;
transition-delay: var(--delay, 0ms);
}
.reveal.is-visible {
opacity: 1;
transform: translateY(0) scale(1);
}
/* =============================== */
/* ANIMAÇÕES */
/* =============================== */
@keyframes floaty {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(14px, 18px) scale(1.03); }
}
@keyframes gridMove {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(-10px, 10px); }
}
@keyframes underlineGrow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
/* =============================== */
/* Acessibilidade */
/* =============================== */
@media (prefers-reduced-motion: reduce) {
.reveal,
.page-bg .grid,
.page-bg .blob,
.main-title .second-line strong::after {
animation: none !important;
transition: none !important;
}
.reveal {
opacity: 1;
transform: none;
}
}

View File

@ -1,30 +1,61 @@
import { Component, HostListener } from '@angular/core'; import { Component, HostListener, Inject } from '@angular/core';
import { RouterLink, Router, NavigationEnd } from '@angular/router'; import { RouterLink, Router, NavigationEnd } from '@angular/router';
import { CommonModule } from '@angular/common'; import { CommonModule, isPlatformBrowser } from '@angular/common';
import { PLATFORM_ID } from '@angular/core';
@Component({ @Component({
selector: 'app-header', selector: 'app-header',
standalone: true, standalone: true,
imports: [RouterLink, CommonModule], // ⬅️ CommonModule para usar *ngIf imports: [RouterLink, CommonModule],
templateUrl: './header.html', templateUrl: './header.html',
styleUrls: ['./header.scss'], styleUrls: ['./header.scss'],
}) })
export class Header { export class Header {
isScrolled = false; isScrolled = false;
isHome = true; // valor inicial (ao abrir normalmente cai na home) isHome = true;
constructor(private router: Router) { // ✅ menu hamburguer
// escuta mudanças de rota para saber se está na home menuOpen = false;
// ✅ define quando mostrar header “logado”
isLoggedHeader = false;
constructor(
private router: Router,
@Inject(PLATFORM_ID) private platformId: object
) {
this.router.events.subscribe((event) => { this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) { if (event instanceof NavigationEnd) {
const url = event.urlAfterRedirects || event.url; const url = event.urlAfterRedirects || event.url;
this.isHome = (url === '/' || url === ''); this.isHome = (url === '/' || url === '');
// ✅ considera header logado quando está em /geral
this.isLoggedHeader = url.startsWith('/geral');
// ✅ ao trocar de rota, fecha o menu
this.menuOpen = false;
} }
}); });
} }
toggleMenu() {
this.menuOpen = !this.menuOpen;
}
closeMenu() {
this.menuOpen = false;
}
@HostListener('window:scroll', []) @HostListener('window:scroll', [])
onWindowScroll() { onWindowScroll() {
this.isScrolled = window.scrollY > 10; // passou 10px de scroll, ativa o “fosco” if (!isPlatformBrowser(this.platformId)) return;
this.isScrolled = window.scrollY > 10;
}
@HostListener('document:keydown.escape', [])
onEsc() {
if (!isPlatformBrowser(this.platformId)) return;
this.closeMenu();
} }
} }

View File

@ -0,0 +1,21 @@
import { inject, PLATFORM_ID } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { isPlatformBrowser } from '@angular/common';
export const authGuard: CanActivateFn = () => {
const router = inject(Router);
const platformId = inject(PLATFORM_ID);
// SSR: não existe localStorage. Bloqueia e manda pro login.
if (!isPlatformBrowser(platformId)) {
return router.parseUrl('/login');
}
const token = localStorage.getItem('token');
if (!token) {
return router.parseUrl('/login');
}
return true;
};

View File

@ -1 +1,265 @@
<p>geral works!</p> <!-- Toast (Sucesso Login) -->
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 2000;">
<div
#successToast
class="toast text-bg-success"
role="alert"
aria-live="assertive"
aria-atomic="true"
>
<div class="toast-header">
<strong class="me-auto">LineGestão</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Fechar"></button>
</div>
<div class="toast-body">
{{ toastMessage }}
</div>
</div>
</div>
<section class="geral-page">
<!-- background "home-like" -->
<span class="page-blob blob-1" aria-hidden="true"></span>
<span class="page-blob blob-2" aria-hidden="true"></span>
<span class="page-blob blob-3" aria-hidden="true"></span>
<span class="page-blob blob-4" aria-hidden="true"></span>
<div class="container-geral">
<div class="geral-card" data-animate>
<!-- HEADER -->
<div class="geral-header">
<div class="header-row-top">
<div class="header-title" data-animate>
<div class="title-badge">
<i class="bi bi-grid-1x2"></i>
Gestão centralizada
</div>
<h5 class="title mb-0">Geral</h5>
<small class="subtitle">Tabela de linhas e dados de telefonia</small>
</div>
<div class="header-actions d-flex gap-2" data-animate>
<button
type="button"
class="btn btn-outline-primary btn-sm btn-glass"
(click)="onImportExcel()"
title="Importar dados de planilha"
>
<i class="bi bi-file-earmark-excel me-1"></i>
Importar Excel
</button>
<button
type="button"
class="btn btn-primary btn-sm btn-brand"
(click)="onCadastrarLinha()"
title="Cadastrar uma nova linha"
>
<i class="bi bi-plus-circle me-1"></i>
Cadastrar Linha
</button>
</div>
</div>
<!-- CONTROLES -->
<div class="controls mt-3" data-animate>
<div class="input-group input-group-sm search-group">
<span class="input-group-text" title="Buscar">
<i class="bi bi-search"></i>
</span>
<input
class="form-control"
placeholder="Pesquisar..."
[(ngModel)]="searchTerm"
(ngModelChange)="onSearch()"
/>
<button
class="btn btn-outline-secondary btn-clear"
type="button"
(click)="clearSearch()"
*ngIf="searchTerm"
title="Limpar busca"
>
<i class="bi bi-x-lg"></i>
</button>
</div>
<div class="page-size">
<span class="text-muted small">Itens por página</span>
<select
class="form-select form-select-sm w-auto select-glass"
[(ngModel)]="pageSize"
(change)="onPageSizeChange()"
>
<option [ngValue]="10">10</option>
<option [ngValue]="20">20</option>
<option [ngValue]="50">50</option>
<option [ngValue]="100">100</option>
</select>
</div>
</div>
</div>
<!-- TABELA -->
<div class="geral-body">
<div class="table-wrap">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 table-geral">
<thead>
<tr>
<th class="sortable" (click)="setSort('item')">
ITÉM
<span class="sort-caret">
{{ sortKey==='item' ? (sortDir==='asc' ? '▲' : '▼') : '' }}
</span>
</th>
<th class="sortable" (click)="setSort('conta')">
CONTA
<span class="sort-caret">
{{ sortKey==='conta' ? (sortDir==='asc' ? '▲' : '▼') : '' }}
</span>
</th>
<th class="sortable" (click)="setSort('linha')">
LINHA
<span class="sort-caret">
{{ sortKey==='linha' ? (sortDir==='asc' ? '▲' : '▼') : '' }}
</span>
</th>
<th class="sortable" (click)="setSort('chip')">
CHIP
<span class="sort-caret">
{{ sortKey==='chip' ? (sortDir==='asc' ? '▲' : '▼') : '' }}
</span>
</th>
<th class="sortable" (click)="setSort('cliente')">
CLIENTE
<span class="sort-caret">
{{ sortKey==='cliente' ? (sortDir==='asc' ? '▲' : '▼') : '' }}
</span>
</th>
<th class="sortable" (click)="setSort('usuario')">
USUÁRIO
<span class="sort-caret">
{{ sortKey==='usuario' ? (sortDir==='asc' ? '▲' : '▼') : '' }}
</span>
</th>
<th class="sortable" (click)="setSort('plano')">
PLANO
<span class="sort-caret">
{{ sortKey==='plano' ? (sortDir==='asc' ? '▲' : '▼') : '' }}
</span>
</th>
<th class="sortable" (click)="setSort('contrato')">
CONTRATO
<span class="sort-caret">
{{ sortKey==='contrato' ? (sortDir==='asc' ? '▲' : '▼') : '' }}
</span>
</th>
<th class="text-end">AÇÕES</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let r of pagedRows; trackBy: trackById" data-row>
<td class="text-muted">{{ r.item }}</td>
<td>{{ r.conta }}</td>
<td class="fw-semibold td-highlight">{{ r.linha }}</td>
<td>{{ r.chip }}</td>
<td>{{ r.cliente }}</td>
<td>{{ r.usuario }}</td>
<td>{{ r.plano }}</td>
<td>{{ r.contrato }}</td>
<td class="text-end actions">
<button
type="button"
class="btn btn-sm btn-outline-secondary btn-icon"
(click)="onDetalhes(r)"
title="Detalhes"
>
<i class="bi bi-eye"></i>
</button>
<button
type="button"
class="btn btn-sm btn-outline-success btn-icon ms-1"
(click)="onFinanceiro(r)"
title="Financeiro"
>
<i class="bi bi-cash-coin"></i>
</button>
<button
type="button"
class="btn btn-sm btn-outline-danger btn-icon ms-1"
(click)="onRemover(r)"
title="Remover"
>
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
<tr *ngIf="pagedRows.length === 0">
<td colspan="9" class="text-center py-5 text-muted empty-state">
<i class="bi bi-inbox me-2"></i>
Nenhum registro encontrado.
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- FOOTER / PAGINAÇÃO -->
<div class="geral-footer">
<div class="small text-muted">
Mostrando {{ pageStart }}{{ pageEnd }} de {{ filteredCount }} registros
</div>
<nav aria-label="Paginação">
<ul class="pagination pagination-sm mb-0 pagination-modern">
<li class="page-item" [class.disabled]="page === 1">
<button class="page-link" (click)="goToPage(page - 1)">
<i class="bi bi-chevron-left"></i>
Anterior
</button>
</li>
<li class="page-item" *ngFor="let p of pageNumbers" [class.active]="p === page">
<button class="page-link" (click)="goToPage(p)">{{ p }}</button>
</li>
<li class="page-item" [class.disabled]="page === totalPages">
<button class="page-link" (click)="goToPage(page + 1)">
Próxima
<i class="bi bi-chevron-right"></i>
</button>
</li>
</ul>
</nav>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,400 @@
:host {
--brand: #E33DCF;
--blue: #030FAA;
--text: #111214;
--muted: rgba(17, 18, 20, 0.70);
--radius-xl: 22px;
--radius-lg: 16px;
display: block;
}
.geral-page {
min-height: calc(100vh - 69.2px);
padding: 24px 12px 90px;
display: flex;
justify-content: center;
font-family: 'Inter', sans-serif;
position: relative;
overflow: hidden;
background:
radial-gradient(900px 420px at 20% 10%, rgba(227, 61, 207, 0.14), transparent 60%),
radial-gradient(820px 380px at 80% 30%, rgba(227, 61, 207, 0.08), transparent 60%),
linear-gradient(180deg, #ffffff 0%, #f5f5f7 70%);
&::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background: rgba(255, 255, 255, 0.25);
}
}
/* blobs fixos */
.page-blob {
position: fixed;
pointer-events: none;
border-radius: 999px;
filter: blur(34px);
opacity: 0.55;
z-index: 0;
background: radial-gradient(circle at 30% 30%, rgba(227,61,207,0.55), rgba(227,61,207,0.06));
animation: floaty 10s ease-in-out infinite;
&.blob-1 { width: 420px; height: 420px; top: -140px; left: -140px; }
&.blob-2 { width: 520px; height: 520px; top: -220px; right: -240px; animation-duration: 12s; }
&.blob-3 { width: 360px; height: 360px; bottom: -180px; left: 25%; animation-duration: 14s; }
&.blob-4 { width: 520px; height: 520px; bottom: -260px; right: -260px; animation-duration: 16s; opacity: .45; }
@media (max-width: 992px) { opacity: 0.35; }
}
.container-geral {
width: 100%;
max-width: 1180px;
position: relative;
z-index: 1;
}
/* CARD GLASS */
.geral-card {
border-radius: var(--radius-xl);
overflow: hidden;
background: rgba(255, 255, 255, 0.82);
border: 1px solid rgba(227, 61, 207, 0.16);
backdrop-filter: blur(12px);
box-shadow: 0 22px 46px rgba(17, 18, 20, 0.10);
/* borda interna */
position: relative;
&::before {
content: '';
position: absolute;
inset: 1px;
border-radius: calc(var(--radius-xl) - 1px);
pointer-events: none;
border: 1px solid rgba(255, 255, 255, 0.65);
opacity: 0.75;
}
}
/* HEADER */
.geral-header {
padding: 16px 16px 14px 16px;
border-bottom: 1px solid rgba(17, 18, 20, 0.06);
background: linear-gradient(180deg, rgba(227,61,207,0.08), rgba(255,255,255,0.10));
}
.header-row-top {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 12px;
@media (max-width: 768px) {
grid-template-columns: 1fr;
text-align: center;
}
}
.header-title {
display: flex;
flex-direction: column;
align-items: flex-start;
@media (max-width: 768px) {
align-items: center;
}
}
.title-badge {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.78);
border: 1px solid rgba(227, 61, 207, 0.22);
backdrop-filter: blur(10px);
color: var(--text);
font-size: 13px;
font-weight: 800;
i { color: var(--brand); }
}
.title {
font-size: 26px;
font-weight: 950;
letter-spacing: -0.3px;
color: var(--text);
margin-top: 10px;
@media (max-width: 768px) { font-size: 24px; }
}
.subtitle {
color: rgba(17, 18, 20, 0.65);
font-weight: 700;
}
.header-actions {
justify-content: flex-end;
@media (max-width: 768px) {
justify-content: center;
}
}
/* BOTÕES */
.btn-brand {
background-color: var(--brand);
border-color: var(--brand);
font-weight: 900;
border-radius: 12px;
transition: transform 180ms ease, box-shadow 180ms ease, filter 180ms ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 14px 24px rgba(227, 61, 207, 0.22);
filter: brightness(0.97);
}
}
.btn-glass {
border-radius: 12px;
font-weight: 900;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(3, 15, 170, 0.25);
color: var(--blue);
transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 14px 24px rgba(17, 18, 20, 0.10);
border-color: rgba(227, 61, 207, 0.30);
}
}
/* CONTROLES */
.controls {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
@media (max-width: 768px) {
justify-content: center;
}
}
/* Busca */
.search-group {
max-width: 340px;
border-radius: 14px;
overflow: hidden;
.input-group-text {
background: rgba(255, 255, 255, 0.78);
border: 1px solid rgba(17, 18, 20, 0.10);
color: var(--brand);
}
.form-control {
border: 1px solid rgba(17, 18, 20, 0.10);
background: rgba(255, 255, 255, 0.78);
}
.form-control:focus {
border-color: rgba(227, 61, 207, 0.35);
box-shadow: 0 0 0 0.2rem rgba(227, 61, 207, 0.12);
}
.btn-clear {
border: 1px solid rgba(17, 18, 20, 0.10);
background: rgba(255, 255, 255, 0.78);
}
}
/* select */
.select-glass {
border-radius: 12px;
background: rgba(255, 255, 255, 0.78);
border: 1px solid rgba(17, 18, 20, 0.10);
}
/* BODY */
.geral-body {
padding: 0;
}
/* Wrapper com altura e scroll */
.table-wrap {
max-height: 58vh;
overflow: auto;
}
/* TABELA MODERNA */
.table-geral {
font-size: 0.875rem;
th, td {
white-space: nowrap;
padding: 10px 12px;
color: var(--text);
font-weight: 700;
}
thead th {
position: sticky;
top: 0;
z-index: 2;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(10px);
border-bottom: 2px solid rgba(227, 61, 207, 0.15);
color: rgba(17, 18, 20, 0.75);
font-weight: 950;
letter-spacing: 0.06em;
text-transform: uppercase;
}
th.sortable {
cursor: pointer;
user-select: none;
transition: color 180ms ease;
&:hover { color: var(--brand); }
}
.sort-caret {
font-size: 11px;
margin-left: 6px;
opacity: 0.75;
color: rgba(17, 18, 20, 0.60);
}
/* destaque da linha */
.td-highlight {
color: var(--blue);
font-weight: 950;
}
/* linhas mais “clean” */
tbody tr {
border-top: 1px solid rgba(17, 18, 20, 0.06);
transition: background-color 180ms ease, transform 180ms ease;
}
tbody tr:hover {
background-color: rgba(227, 61, 207, 0.06);
}
td.actions {
min-width: 160px;
}
}
/* botões ícone */
.btn-icon {
padding: 6px 10px;
border-radius: 12px;
transition: transform 150ms ease, box-shadow 150ms ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 12px 22px rgba(17, 18, 20, 0.10);
}
}
.empty-state {
background: rgba(255, 255, 255, 0.35);
}
/* FOOTER */
.geral-footer {
padding: 14px 16px;
border-top: 1px solid rgba(17, 18, 20, 0.06);
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
@media (max-width: 768px) {
justify-content: center;
text-align: center;
}
}
/* Paginação moderna */
.pagination-modern .page-link {
color: var(--blue);
font-weight: 900;
border-radius: 12px;
border: 1px solid rgba(17, 18, 20, 0.10);
background: rgba(255, 255, 255, 0.78);
transition: transform 150ms ease, border-color 150ms ease, box-shadow 150ms ease, color 150ms ease;
i { font-size: 12px; }
}
.pagination-modern .page-link:hover {
transform: translateY(-1px);
border-color: rgba(227, 61, 207, 0.28);
color: var(--brand);
box-shadow: 0 12px 22px rgba(17, 18, 20, 0.08);
}
.pagination-modern .page-item.active .page-link {
background-color: rgba(3, 15, 170, 0.92);
border-color: rgba(3, 15, 170, 0.92);
color: #fff;
}
/* =============================== */
/* ✅ ANIMAÇÕES SSR-SAFE */
/* =============================== */
[data-animate] { opacity: 1; transform: none; }
.js-animate [data-animate] {
opacity: 0;
transform: translateY(14px);
transition: opacity 600ms ease, transform 600ms ease;
will-change: opacity, transform;
}
.js-animate [data-animate].is-visible {
opacity: 1;
transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
.page-blob { animation: none; }
.js-animate [data-animate] { transition: none; transform: none; opacity: 1; }
}
/* animações */
@keyframes floaty {
0% { transform: translate(0, 0) scale(1); }
50% { transform: translate(18px, 10px) scale(1.03); }
100% { transform: translate(0, 0) scale(1); }
}

View File

@ -1,11 +1,188 @@
import { Component } from '@angular/core'; import { Component, ElementRef, ViewChild, Inject, PLATFORM_ID, AfterViewInit } from '@angular/core';
import { isPlatformBrowser, CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
type SortDir = 'asc' | 'desc';
type LineRow = {
id: number;
item: string;
conta: string;
linha: string;
chip: string;
cliente: string;
usuario: string;
plano: string;
contrato: string;
};
@Component({ @Component({
selector: 'app-geral', standalone: true,
imports: [], imports: [CommonModule, FormsModule],
templateUrl: './geral.html', templateUrl: './geral.html',
styleUrl: './geral.scss', styleUrls: ['./geral.scss']
}) })
export class Geral { export class Geral implements AfterViewInit {
toastMessage = '';
@ViewChild('successToast') successToast!: ElementRef;
constructor(@Inject(PLATFORM_ID) private platformId: object) {}
async ngAfterViewInit() {
if (!isPlatformBrowser(this.platformId)) return;
// ✅ animações (igual Home)
this.initAnimations();
const msg = (history.state && history.state.toastMessage) ? String(history.state.toastMessage) : '';
if (!msg) return;
this.toastMessage = msg;
await this.showToast(msg);
const newState = { ...history.state };
delete newState.toastMessage;
history.replaceState(newState, '', location.href);
}
private initAnimations() {
document.documentElement.classList.add('js-animate');
setTimeout(() => {
const items = Array.from(document.querySelectorAll<HTMLElement>('[data-animate]'));
if (!items.length) return;
const reduceMotion =
window.matchMedia &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduceMotion || !('IntersectionObserver' in window)) {
items.forEach(i => i.classList.add('is-visible'));
return;
}
const io = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
(entry.target as HTMLElement).classList.add('is-visible');
io.unobserve(entry.target);
}
}
}, { threshold: 0.12 });
items.forEach(el => io.observe(el));
}, 0);
}
private async showToast(message: string) {
this.toastMessage = message;
const bs = await import('bootstrap');
const toast = new bs.Toast(this.successToast.nativeElement, { autohide: true, delay: 1500 });
toast.show();
}
// ========= TABELA (mock por enquanto) =========
rows: LineRow[] = [
{ id: 1, item: '001', conta: 'Empresa A', linha: '71999990001', chip: 'ICCID-0001', cliente: 'Cliente A', usuario: 'João', plano: 'Pós 30GB', contrato: 'CT-2025-001' },
{ id: 2, item: '002', conta: 'Empresa B', linha: '71999990002', chip: 'ICCID-0002', cliente: 'Cliente B', usuario: 'Maria', plano: 'Pós Ilimitado', contrato: 'CT-2025-002' },
{ id: 3, item: '003', conta: 'Empresa A', linha: '71999990003', chip: 'ICCID-0003', cliente: 'Cliente A', usuario: 'Carlos', plano: 'Controle 20GB', contrato: 'CT-2025-003' },
];
searchTerm = '';
sortKey: keyof LineRow = 'item';
sortDir: SortDir = 'asc';
page = 1;
pageSize = 10;
onSearch() { this.page = 1; }
clearSearch() { this.searchTerm = ''; this.page = 1; }
setSort(key: keyof LineRow) {
if (this.sortKey === key) {
this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc';
return;
}
this.sortKey = key;
this.sortDir = 'asc';
}
onPageSizeChange() { this.page = 1; }
goToPage(p: number) {
const target = Math.max(1, Math.min(this.totalPages, p));
this.page = target;
}
trackById(_: number, row: LineRow) { return row.id; }
get filteredRows(): LineRow[] {
const term = this.searchTerm.trim().toLowerCase();
if (!term) return this.rows;
return this.rows.filter(r => {
const blob = [r.item, r.conta, r.linha, r.chip, r.cliente, r.usuario, r.plano, r.contrato].join(' ').toLowerCase();
return blob.includes(term);
});
}
get sortedRows(): LineRow[] {
const copy = [...this.filteredRows];
const key = this.sortKey;
const dir = this.sortDir;
copy.sort((a, b) => {
const av = String(a[key] ?? '').toLowerCase();
const bv = String(b[key] ?? '').toLowerCase();
if (av < bv) return dir === 'asc' ? -1 : 1;
if (av > bv) return dir === 'asc' ? 1 : -1;
return 0;
});
return copy;
}
get totalPages(): number {
const total = Math.ceil(this.sortedRows.length / this.pageSize);
return total > 0 ? total : 1;
}
get pagedRows(): LineRow[] {
const start = (this.page - 1) * this.pageSize;
const end = start + this.pageSize;
return this.sortedRows.slice(start, end);
}
get filteredCount(): number { return this.sortedRows.length; }
get pageStart(): number {
if (this.filteredCount === 0) return 0;
return (this.page - 1) * this.pageSize + 1;
}
get pageEnd(): number {
if (this.filteredCount === 0) return 0;
return Math.min(this.page * this.pageSize, this.filteredCount);
}
get pageNumbers(): number[] {
const total = this.totalPages;
const current = this.page;
const max = 5;
let start = Math.max(1, current - 2);
let end = Math.min(total, start + (max - 1));
start = Math.max(1, end - (max - 1));
const pages: number[] = [];
for (let i = start; i <= end; i++) pages.push(i);
return pages;
}
// ========= BOTÕES (placeholders) =========
async onImportExcel() { await this.showToast('Importação via Excel será implementada.'); }
async onCadastrarLinha() { await this.showToast('Cadastro de linha será implementado.'); }
async onDetalhes(row: LineRow) { await this.showToast(`Detalhes: linha ${row.linha}`); }
async onFinanceiro(row: LineRow) { await this.showToast(`Financeiro: linha ${row.linha} (em implementação)`); }
async onRemover(row: LineRow) { await this.showToast(`Remover: linha ${row.linha} (em implementação)`); }
} }

View File

@ -1,62 +1,201 @@
<section class="hero-text-section"> <section class="home-page">
<h1 class="main-title">
<span class="first-line">Gerencie suas linhas móveis</span>
<span class="second-line">com <strong>inteligência e praticidade</strong></span>
</h1>
<p class="main-paragraph"> <!-- BACKGROUND GLOBAL -->
<strong class="brand-name">LineGestão</strong> é a solução completa para empresas que <span class="page-blob blob-1" aria-hidden="true"></span>
<strong class="highlight">desejam controlar suas linhas móveis com eficiência e segurança</strong>. <span class="page-blob blob-2" aria-hidden="true"></span>
Com recursos como <strong class="highlight">gerenciamento de clientes</strong>, <span class="page-blob blob-3" aria-hidden="true"></span>
<strong class="highlight">importação de dados via Excel</strong> e <span class="page-blob blob-4" aria-hidden="true"></span>
<strong class="highlight">monitoramento estratégico de contratos e linhas</strong>,
você <strong class="highlight">simplifica processos</strong>,
<strong class="highlight">reduz erros</strong> e
<strong class="highlight">ganha mais controle sobre seus recursos corporativos</strong>.
</p>
</section>
<div class="container my-5"> <!-- HERO -->
<div class="row justify-content-center feature-cards-row"> <section class="hero">
<div class="container">
<div class="col-auto mb-4"> <div class="hero-inner">
<app-feature-card
title="Monitoramento Completo"
[textAlign]="'center'"
iconClass="bi bi-laptop"
description="<strong>Acompanhe contratos, valores e suas linhas</strong> com visão estratégica, garantindo controle total sobre seus recursos móveis."
></app-feature-card>
</div>
<div class="col-auto mb-4"> <!-- COLUNA TEXTO (CENTRALIZADA) -->
<app-feature-card <div class="hero-copy">
title="Gerenciamento de Clientes"
[textAlign]="'center'"
iconClass="bi bi-people"
description="<strong>Organize e acompanhe seus clientes</strong> com praticidade e segurança, garantindo uma gestão eficiente."
></app-feature-card>
</div>
<div class="col-auto mb-4"> <div class="hero-badge" data-animate>
<app-feature-card <i class="bi bi-stars"></i>
title="Importação via Excel" SaaS para gestão de linhas corporativas
[textAlign]="'center'" </div>
iconClass="bi bi-table"
description="<strong>Integre dados rapidamente</strong> sem esforço manual, substituindo planilhas por uma solução moderna e automatizada."
></app-feature-card>
</div>
</div> <section class="hero-text-section">
<h1 class="main-title" data-animate>
<span class="first-line">Gerencie suas linhas móveis</span>
<span class="second-line">com <strong>inteligência e praticidade</strong></span>
</h1>
<p class="main-paragraph" data-animate>
<strong class="brand-name">LineGestão</strong> é a solução completa para empresas que
<strong class="highlight">desejam controlar suas linhas móveis com eficiência e segurança</strong>.
Com recursos como <strong class="highlight">gerenciamento de clientes</strong>,
<strong class="highlight">importação de dados via Excel</strong> e
<strong class="highlight">monitoramento estratégico de contratos e linhas</strong>,
você <strong class="highlight">simplifica processos</strong>,
<strong class="highlight">reduz erros</strong> e
<strong class="highlight">ganha mais controle sobre seus recursos corporativos</strong>.
</p>
<div class="hero-actions" data-animate>
<app-cta-button
label="COMEÇAR AGORA"
(clicked)="iniciar()">
</app-cta-button>
<button type="button" class="cta-secondary" (click)="scrollToFeatures()">
<i class="bi bi-arrow-down-circle"></i>
Ver recursos
</button>
</div>
</section>
</div>
<!-- CARD/MOCK (fica na direita) -->
<div class="hero-mock" data-animate aria-label="Prévia visual do painel">
<div class="mock-card">
<div class="mock-top">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
<span class="mock-title">Visão Geral</span>
</div>
<div class="mock-grid">
<div class="mock-kpi">
<span class="kpi-label">Linhas ativas</span>
<span class="kpi-value">128</span>
<span class="kpi-tag"><i class="bi bi-graph-up"></i> controle</span>
</div>
<div class="mock-kpi">
<span class="kpi-label">Contratos</span>
<span class="kpi-value">12</span>
<span class="kpi-tag"><i class="bi bi-file-earmark-text"></i> organizado</span>
</div>
<div class="mock-kpi">
<span class="kpi-label">Clientes</span>
<span class="kpi-value">34</span>
<span class="kpi-tag"><i class="bi bi-people"></i> centralizado</span>
</div>
<div class="mock-line">
<div class="line-icon"><i class="bi bi-sim"></i></div>
<div class="line-info">
<div class="line-title">Linha 55XX9XXXXXXXX</div>
<div class="line-sub">Status: Ativa • Operadora: Vivo</div>
</div>
<div class="line-pill">OK</div>
</div>
</div>
</div>
</div>
<div class="row justify-content-center button-section">
<div class="col-auto">
<app-cta-button
label="COMEÇAR AGORA"
(clicked)="iniciar()">
</app-cta-button>
</div> </div>
<!-- NOVO: CENTRALIZAR OS CARDS NO MEIO -->
<div class="hero-metrics-wide" data-animate>
<div class="hero-metrics">
<div class="metric">
<i class="bi bi-lightning-charge"></i>
<div>
<span class="metric-title">Setup rápido</span>
<span class="metric-sub">comece em minutos</span>
</div>
</div>
<div class="metric">
<i class="bi bi-file-earmark-spreadsheet"></i>
<div>
<span class="metric-title">Excel → Sistema</span>
<span class="metric-sub">importação inteligente</span>
</div>
</div>
<div class="metric">
<i class="bi bi-shield-check"></i>
<div>
<span class="metric-title">Mais segurança</span>
<span class="metric-sub">menos erro manual</span>
</div>
</div>
</div>
</div>
</div> </div>
</section>
</div> <!-- FEATURES -->
<section id="features" class="features-section">
<div class="container my-5">
<div class="section-head" data-animate>
<h2 class="section-title">
Tudo o que você precisa para <span class="brand">gestão de linhas</span>
</h2>
<p class="section-subtitle">
Um painel simples, bonito e direto ao ponto — feito para empresa que quer controle.
</p>
</div>
<!-- NOVO: Centralizar os cards -->
<div class="row justify-content-center feature-cards-row">
<div class="col-auto mb-4 feature-item" data-animate>
<app-feature-card
title="Monitoramento Completo"
[textAlign]="'center'"
iconClass="bi bi-laptop"
description="<strong>Acompanhe contratos, valores e suas linhas</strong> com visão estratégica, garantindo controle total sobre seus recursos móveis."
></app-feature-card>
</div>
<div class="col-auto mb-4 feature-item" data-animate>
<app-feature-card
title="Gerenciamento de Clientes"
[textAlign]="'center'"
iconClass="bi bi-people"
description="<strong>Organize e acompanhe seus clientes</strong> com praticidade e segurança, garantindo uma gestão eficiente."
></app-feature-card>
</div>
<div class="col-auto mb-4 feature-item" data-animate>
<app-feature-card
title="Importação via Excel"
[textAlign]="'center'"
iconClass="bi bi-table"
description="<strong>Integre dados rapidamente</strong> sem esforço manual, substituindo planilhas por uma solução moderna e automatizada."
></app-feature-card>
</div>
</div>
<div class="row justify-content-center button-section" data-animate>
<div class="col-auto">
<app-cta-button
label="COMEÇAR AGORA"
(clicked)="iniciar()">
</app-cta-button>
</div>
</div>
<div class="value-strip" data-animate>
<div class="value">
<i class="bi bi-check2-circle"></i>
<span><strong>Menos planilha</strong>, mais controle.</span>
</div>
<div class="value">
<i class="bi bi-check2-circle"></i>
<span><strong>Mais agilidade</strong> no dia a dia.</span>
</div>
<div class="value">
<i class="bi bi-check2-circle"></i>
<span><strong>Mais segurança</strong> na gestão.</span>
</div>
</div>
</div>
</section>
</section>

View File

@ -1,136 +1,544 @@
/* =============================== */ :host {
/* CONTAINER PRINCIPAL */ --brand: #E33DCF;
/* =============================== */ --brand-soft: rgba(227, 61, 207, 0.14);
.hero-text-section { --brand-soft-2: rgba(227, 61, 207, 0.08);
text-align: center;
padding-top: 50px; --text: #111214;
margin: 0 auto; --muted: rgba(17, 18, 20, 0.70);
--radius-xl: 22px;
--radius-lg: 16px;
display: block;
}
/* ✅ FUNDO GLOBAL (vale para a Home TODA até o footer) */
.home-page {
position: relative;
min-height: 100vh;
overflow: hidden;
background:
radial-gradient(900px 420px at 20% 10%, var(--brand-soft), transparent 60%),
radial-gradient(820px 380px at 80% 30%, var(--brand-soft-2), transparent 60%),
linear-gradient(180deg, #ffffff 0%, #f5f5f7 70%);
/* névoa suave pra dar sensação de blur */
&::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background: rgba(255, 255, 255, 0.25);
}
}
/* ✅ BLOBS FIXOS (continuam no scroll) */
.page-blob {
position: fixed;
pointer-events: none;
border-radius: 999px;
filter: blur(34px);
opacity: 0.55;
z-index: 0;
background: radial-gradient(circle at 30% 30%, rgba(227,61,207,0.55), rgba(227,61,207,0.06));
animation: floaty 10s ease-in-out infinite;
&.blob-1 { width: 420px; height: 420px; top: -140px; left: -140px; }
&.blob-2 { width: 520px; height: 520px; top: -220px; right: -240px; animation-duration: 12s; }
&.blob-3 { width: 360px; height: 360px; bottom: -180px; left: 25%; animation-duration: 14s; }
&.blob-4 { width: 520px; height: 520px; bottom: -260px; right: -260px; animation-duration: 16s; opacity: .45; }
@media (max-width: 992px) {
opacity: 0.35;
}
}
/* ✅ garante que o conteúdo fique acima do fundo */
.hero,
.features-section,
.container {
position: relative;
z-index: 1;
} }
/* =============================== */ /* =============================== */
/* TÍTULO PRINCIPAL */ /* HERO */
/* =============================== */ /* =============================== */
.hero {
padding: 56px 0 18px 0;
}
.hero-inner {
display: grid;
grid-template-columns: 1.1fr 0.9fr;
gap: 36px;
align-items: center;
@media (max-width: 992px) {
grid-template-columns: 1fr;
gap: 26px;
}
}
/* texto centralizado */
.hero-copy {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
/* badge */
.hero-badge {
display: inline-flex;
align-items: center;
gap: 10px;
width: fit-content;
padding: 10px 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.78);
border: 1px solid rgba(227, 61, 207, 0.22);
backdrop-filter: blur(10px);
color: var(--text);
font-family: 'Inter', sans-serif;
font-size: 14px;
font-weight: 700;
i { color: var(--brand); font-size: 16px; }
}
.hero-text-section {
width: 100%;
margin-top: 12px;
}
/* título */
.main-title { .main-title {
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
font-size: 48px; font-size: 52px;
line-height: 1.2; line-height: 1.05;
margin-bottom: 30px; margin: 18px 0 18px 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
width: fit-content; @media (max-width: 1400px) { font-size: 44px; }
margin: 0 auto 80px auto; @media (max-width: 1024px) { font-size: 38px; }
@media (max-width: 768px) { font-size: 32px; }
/* ========== RESPONSIVO ========== */ @media (max-width: 480px) { font-size: 28px; }
/* até 1400px diminui um pouco */
@media (max-width: 1400px) {
font-size: 40px;
}
/* tablets e notebooks pequenos */
@media (max-width: 1024px) {
font-size: 36px;
}
/* tablets menores */
@media (max-width: 768px) {
font-size: 32px;
margin-bottom: 50px;
}
/* celulares */
@media (max-width: 480px) {
font-size: 28px;
margin-bottom: 40px;
}
} }
/* Primeira linha */ .main-title .first-line,
.main-title .first-line {
color: #E33DCF;
font-weight: 500;
display: block;
text-align: center;
}
/* Segunda linha */
.main-title .second-line { .main-title .second-line {
color: #E33DCF; font-weight: 650;
font-weight: 500;
display: block; display: block;
color: var(--text);
text-align: center; text-align: center;
} }
/* -------- Efeito deslocado só em telas grandes -------- */ .main-title strong {
@media (min-width: 1200px) { color: var(--brand);
.main-title .first-line { position: relative;
transform: translateX(-70px);
}
.main-title .second-line {
transform: translateX(100px) translateY(-6px);
}
} }
/* Em telas menores que 1200px o título fica plenamente centralizado */ .main-title strong::after {
@media (max-width: 1199.98px) { content: '';
.main-title .first-line, position: absolute;
.main-title .second-line { left: 0;
transform: none; /* garante que não herda nenhum translate */ bottom: 6px;
} width: 100%;
height: 10px;
border-radius: 999px;
background: rgba(227, 61, 207, 0.18);
z-index: -1;
} }
/* =============================== */ /* parágrafo */
/* PARÁGRAFO PRINCIPAL */
/* =============================== */
.main-paragraph { .main-paragraph {
width: 985px; width: min(980px, 100%);
height: 132px; margin: 0 auto 16px auto;
margin: 0 auto 50px auto;
font-family: 'Poppins', sans-serif; font-family: 'Poppins', sans-serif;
font-size: 24px; font-size: 20px;
color: #000000; color: var(--muted);
line-height: 1; line-height: 1.45;
font-weight: 400;
@media (max-width: 1400px) { font-size: 19px; }
@media (max-width: 1024px) { font-size: 18px; }
@media (max-width: 768px) { font-size: 16px; }
}
.main-paragraph .brand-name { color: var(--text); }
.main-paragraph .highlight { color: var(--text); font-weight: 800; }
.main-paragraph strong { font-weight: 800; }
/* botões */
.hero-actions {
display: flex;
gap: 14px;
align-items: center;
justify-content: center;
margin-top: 16px;
flex-wrap: wrap;
}
.cta-secondary {
height: 44px;
padding: 0 14px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(17, 18, 20, 0.10);
color: var(--text);
font-weight: 800;
display: inline-flex;
align-items: center;
gap: 10px;
transition: transform 180ms ease, border-color 180ms ease, box-shadow 180ms ease;
i { color: var(--brand); }
&:hover {
transform: translateY(-2px);
border-color: rgba(227, 61, 207, 0.28);
box-shadow: 0 12px 24px rgba(17, 18, 20, 0.10);
}
}
/* =============================== */
/* MOCK (direita) */
/* =============================== */
.hero-mock {
display: flex;
justify-content: flex-end;
@media (max-width: 992px) {
justify-content: center;
}
}
.mock-card {
width: min(460px, 100%);
border-radius: var(--radius-xl);
background: rgba(255, 255, 255, 0.78);
border: 1px solid rgba(227, 61, 207, 0.14);
backdrop-filter: blur(12px);
box-shadow: 0 22px 46px rgba(17, 18, 20, 0.10);
overflow: hidden;
transform: perspective(900px) rotateY(-6deg) rotateX(2deg);
transition: transform 200ms ease;
&:hover {
transform: perspective(900px) rotateY(-2deg) rotateX(1deg) translateY(-2px);
}
}
.mock-top {
display: flex;
align-items: center;
gap: 8px;
padding: 14px 16px;
background: linear-gradient(180deg, rgba(227, 61, 207, 0.10), rgba(255, 255, 255, 0.20));
.dot {
width: 10px;
height: 10px;
border-radius: 999px;
background: rgba(17, 18, 20, 0.12);
}
.mock-title {
margin-left: 6px;
font-weight: 950;
font-family: 'Inter', sans-serif;
color: var(--text);
}
}
.mock-grid {
padding: 16px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
@media (max-width: 380px) { grid-template-columns: 1fr; }
}
.mock-kpi {
border-radius: var(--radius-lg);
background: #fff;
border: 1px solid rgba(17, 18, 20, 0.08);
padding: 12px;
.kpi-label {
display: block;
font-size: 12px;
color: rgba(17, 18, 20, 0.65);
font-family: 'Inter', sans-serif;
font-weight: 800;
}
.kpi-value {
display: block;
font-size: 26px;
font-family: 'Inter', sans-serif;
font-weight: 950;
color: var(--text);
margin-top: 2px;
}
.kpi-tag {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 8px;
padding: 6px 10px;
border-radius: 999px;
border: 1px solid rgba(227, 61, 207, 0.20);
background: rgba(227, 61, 207, 0.07);
color: var(--text);
font-size: 12px;
font-weight: 900;
i { color: var(--brand); }
}
}
.mock-line {
grid-column: 1 / -1;
border-radius: var(--radius-lg);
background: #fff;
border: 1px solid rgba(17, 18, 20, 0.08);
padding: 12px;
display: grid;
grid-template-columns: auto 1fr auto;
gap: 12px;
align-items: center;
.line-icon {
width: 40px;
height: 40px;
border-radius: 12px;
background: rgba(227, 61, 207, 0.10);
display: grid;
place-items: center;
i { color: var(--brand); font-size: 18px; }
}
.line-title {
font-weight: 950;
font-family: 'Inter', sans-serif;
color: var(--text);
font-size: 14px;
}
.line-sub {
color: rgba(17, 18, 20, 0.65);
font-size: 12px;
margin-top: 2px;
font-family: 'Inter', sans-serif;
}
.line-pill {
padding: 8px 10px;
border-radius: 999px;
font-weight: 950;
font-size: 12px;
background: rgba(227, 61, 207, 0.10);
border: 1px solid rgba(227, 61, 207, 0.22);
color: var(--text);
}
}
/* =============================== */
/* MÉTRICAS CENTRALIZADAS (MEIO) */
/* =============================== */
.hero-metrics-wide {
margin-top: 18px;
display: flex;
justify-content: center;
}
.hero-metrics {
display: flex;
gap: 14px;
flex-wrap: wrap;
justify-content: center;
}
.metric {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.78);
border: 1px solid rgba(227, 61, 207, 0.16);
backdrop-filter: blur(10px);
i { color: var(--brand); font-size: 18px; }
.metric-title {
display: block;
font-weight: 900;
color: var(--text);
font-family: 'Inter', sans-serif;
font-size: 14px;
line-height: 1.1;
}
.metric-sub {
display: block;
font-family: 'Inter', sans-serif;
font-size: 12px;
color: rgba(17, 18, 20, 0.65);
}
}
/* =============================== */
/* FEATURES */
/* =============================== */
.features-section {
padding: 18px 0 60px 0;
background: transparent;
}
.section-head {
text-align: center; text-align: center;
margin-bottom: 24px;
}
/* ========== RESPONSIVO ========== */ .section-title {
font-family: 'Inter', sans-serif;
font-weight: 950;
color: var(--text);
font-size: 30px;
@media (max-width: 1400px) { @media (max-width: 768px) { font-size: 24px; }
width: 65%; }
height: auto;
font-size: 20px; .section-title .brand { color: var(--brand); }
.section-subtitle {
margin-top: 10px;
color: var(--muted);
font-family: 'Poppins', sans-serif;
}
/* ✅ AQUI: 3 CARDS CENTRALIZADOS LADO A LADO NO NOTEBOOK */
.feature-cards-row {
display: flex;
justify-content: center;
align-items: stretch;
gap: 26px;
flex-wrap: nowrap; /* ✅ força ficar em 1 linha */
/* notebook / telas médias */
@media (max-width: 1199.98px) {
gap: 18px;
} }
/* Tablets */ /* quando a tela ficar pequena de verdade, aí quebra */
@media (max-width: 1024px) { @media (max-width: 992px) {
width: 70%; flex-wrap: wrap;
height: auto;
font-size: 20px;
line-height: 1.2;
}
/* Tablets pequenos */
@media (max-width: 768px) {
width: 80%;
font-size: 18px;
line-height: 1.25;
margin-bottom: 40px;
}
/* Celulares */
@media (max-width: 480px) {
width: 90%;
font-size: 16px;
line-height: 1.3;
margin-bottom: 30px;
} }
} }
.main-paragraph strong { /* Garante que o col-auto do bootstrap não atrapalhe */
font-weight: 700; .feature-item {
display: flex;
justify-content: center;
align-items: stretch;
margin-bottom: 0 !important;
}
/* melhora o respiro no botão */
.button-section {
margin-top: 18px;
}
/* faixa de valores */
.value-strip {
margin-top: 26px;
padding: 14px 16px;
border-radius: var(--radius-xl);
background: rgba(255, 255, 255, 0.80);
border: 1px solid rgba(227, 61, 207, 0.14);
backdrop-filter: blur(10px);
display: flex;
justify-content: center;
gap: 18px;
flex-wrap: wrap;
.value {
display: inline-flex;
align-items: center;
gap: 10px;
i { color: var(--brand); font-size: 18px; }
span { color: var(--text); font-family: 'Inter', sans-serif; }
}
}
/* =============================== */
/* ✅ ANIMAÇÕES SSR-SAFE */
/* =============================== */
[data-animate] { opacity: 1; transform: none; }
.js-animate [data-animate] {
opacity: 0;
transform: translateY(14px);
transition: opacity 600ms ease, transform 600ms ease;
will-change: opacity, transform;
}
.js-animate [data-animate].is-visible {
opacity: 1;
transform: translateY(0);
}
/* brilho suave no botão */
:host ::ng-deep app-cta-button .btn,
:host ::ng-deep app-cta-button button {
position: relative;
overflow: hidden;
transform: translateZ(0);
}
:host ::ng-deep app-cta-button .btn::after,
:host ::ng-deep app-cta-button button::after {
content: '';
position: absolute;
inset: 0;
transform: translateX(-120%) rotate(12deg);
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.55), transparent);
animation: shine 3.4s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.page-blob { animation: none; }
.js-animate [data-animate] { transition: none; transform: none; opacity: 1; }
:host ::ng-deep app-cta-button .btn::after,
:host ::ng-deep app-cta-button button::after { animation: none; }
}
@keyframes floaty {
0% { transform: translate(0, 0) scale(1); }
50% { transform: translate(18px, 10px) scale(1.03); }
100% { transform: translate(0, 0) scale(1); }
}
@keyframes shine {
0%, 65% { transform: translateX(-120%) rotate(12deg); opacity: 0.0; }
75% { opacity: 1; }
100% { transform: translateX(120%) rotate(12deg); opacity: 0.0; }
} }

View File

@ -1,29 +1,68 @@
import { Component } from '@angular/core'; import { Component, AfterViewInit, Inject, PLATFORM_ID } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule, isPlatformBrowser } from '@angular/common';
import { FeatureCardComponent } from '../../components/feature-card/feature-card'; import { FeatureCardComponent } from '../../components/feature-card/feature-card';
import { CtaButtonComponent } from '../../components/cta-button/cta-button'; import { CtaButtonComponent } from '../../components/cta-button/cta-button';
import { Router } from '@angular/router'; // ⬅️ IMPORT CERTO (ANGULAR) import { Router } from '@angular/router';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
standalone: true, standalone: true,
imports: [ imports: [CommonModule, FeatureCardComponent, CtaButtonComponent],
CommonModule,
FeatureCardComponent,
CtaButtonComponent
],
templateUrl: './home.html', templateUrl: './home.html',
styleUrls: ['./home.scss'], // ⬅️ styleUrls (array) styleUrls: ['./home.scss'],
}) })
export class Home { export class Home implements AfterViewInit {
private readonly isBrowser: boolean;
// ⬅️ Aqui o Angular injeta o Router e guarda em this.router constructor(
constructor(private router: Router) {} private router: Router,
@Inject(PLATFORM_ID) platformId: Object
) {
this.isBrowser = isPlatformBrowser(platformId);
}
iniciar(): void { iniciar(): void {
// só pra debug, se quiser: this.router.navigate(['/register']);
// console.log('Botão COMEÇAR AGORA clicado! Redirecionando para /register...'); }
this.router.navigate(['/register']); // ⬅️ agora this.router não é mais undefined scrollToFeatures(): void {
if (!this.isBrowser) return;
document.getElementById('features')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
ngAfterViewInit(): void {
if (!this.isBrowser) return;
document.documentElement.classList.add('js-animate');
setTimeout(() => {
const items = Array.from(document.querySelectorAll<HTMLElement>('[data-animate]'));
if (!items.length) return;
const reduceMotion =
window.matchMedia &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduceMotion) {
items.forEach(i => i.classList.add('is-visible'));
return;
}
if (!('IntersectionObserver' in window)) {
items.forEach(i => i.classList.add('is-visible'));
return;
}
const io = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
(entry.target as HTMLElement).classList.add('is-visible');
io.unobserve(entry.target);
}
}
}, { threshold: 0.12 });
items.forEach(el => io.observe(el));
}, 0);
} }
} }

View File

@ -77,15 +77,15 @@ export class LoginComponent {
}; };
this.authService.login(payload).subscribe({ this.authService.login(payload).subscribe({
next: async (res) => { next: (res) => {
this.isSubmitting = false; this.isSubmitting = false;
const nome = this.getNameFromToken(res.token); const nome = this.getNameFromToken(res.token);
await this.showToast(`Bem-vindo, ${nome}!`);
setTimeout(() => { // ✅ Vai para /geral já levando a mensagem do toast
this.router.navigate(['/geral']); this.router.navigate(['/geral'], {
}, 900); state: { toastMessage: `Bem-vindo, ${nome}!` }
});
}, },
error: (err) => { error: (err) => {
this.isSubmitting = false; this.isSubmitting = false;