MetacaseForm

This commit is contained in:
2026-03-14 20:31:10 +00:00
parent 057717b0b7
commit 9bebe1de72
68 changed files with 3930 additions and 15 deletions
+4 -3
View File
@@ -2,7 +2,8 @@
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "npm"
"packageManager": "npm",
"analytics": false
},
"newProjectRoot": "projects",
"projects": {
@@ -38,8 +39,8 @@
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
"maximumWarning": "10kB",
"maximumError": "12kB"
}
],
"outputHashing": "all"
+311
View File
@@ -0,0 +1,311 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
interface Course {
category: string;
title: string;
description: string;
level: string;
duration: string;
badge: string;
image: string;
}
@Component({
selector: 'app-courses-page',
standalone: true,
imports: [CommonModule],
template: `
<section class="hero">
<div class="hero__content">
<span class="eyebrow">Learning Hub</span>
<h1>As nossas Componentes</h1>
<p>
Conehcimento das componentes de TargetOne, atraves de um conjunto de formações interativas e práticas, projetadas para potencializar o dominio das nossas soluções.
</p>
</div>
</section>
<main class="courses-page">
<section class="courses-header">
<div>
<h2>Formações Dispiniveis</h2>
<p>Formações diponiveis de TargetOne disponiveis para fazer</p>
</div>
<div class="courses-summary">
<span>{{ courses.length }} courses</span>
</div>
</section>
<section class="courses-grid">
<article class="course-card" *ngFor="let course of courses">
<div class="course-card__image" [style.background-image]="'url(' + course.image + ')'">
<span class="badge">{{ course.badge }}</span>
</div>
<div class="course-card__content">
<span class="category">{{ course.category }}</span>
<h3>{{ course.title }}</h3>
<p>{{ course.description }}</p>
<div class="meta">
<span>{{ course.level }}</span>
<span>{{ course.duration }}</span>
</div>
<button type="button">Ver curso</button>
</div>
</article>
</section>
</main>
`,
styles: [`
:host {
display: block;
min-height: 100vh;
background:
radial-gradient(circle at top left, rgba(94, 96, 206, 0.20), transparent 28%),
radial-gradient(circle at top right, rgba(76, 201, 240, 0.18), transparent 30%),
linear-gradient(180deg, #0b1020 0%, #11182d 35%, #f5f7fb 35%, #f5f7fb 100%);
color: #101828;
font-family: Inter, Arial, sans-serif;
}
* {
box-sizing: border-box;
}
.hero {
padding: 72px 24px 56px;
color: #ffffff;
}
.hero__content {
max-width: 1200px;
margin: 0 auto;
}
.eyebrow {
display: inline-flex;
padding: 8px 14px;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 18px;
}
.hero h1 {
margin: 0 0 16px;
font-size: clamp(2.2rem, 5vw, 4rem);
line-height: 1.05;
font-weight: 800;
}
.hero p {
max-width: 700px;
margin: 0;
color: rgba(255, 255, 255, 0.82);
font-size: 1.05rem;
line-height: 1.7;
}
.courses-page {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px 72px;
}
.courses-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 28px;
}
.courses-header h2 {
margin: 0 0 8px;
font-size: 2rem;
color: #111827;
}
.courses-header p {
margin: 0;
color: #667085;
}
.courses-summary {
background: #ffffff;
border: 1px solid #e4e7ec;
border-radius: 16px;
padding: 14px 18px;
box-shadow: 0 10px 30px rgba(16, 24, 40, 0.06);
font-weight: 700;
white-space: nowrap;
}
.courses-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 24px;
}
.course-card {
overflow: hidden;
border-radius: 24px;
background: #ffffff;
border: 1px solid #eaecf0;
box-shadow: 0 20px 45px rgba(16, 24, 40, 0.08);
transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.course-card:hover {
transform: translateY(-6px);
box-shadow: 0 24px 55px rgba(16, 24, 40, 0.12);
}
.course-card__image {
position: relative;
min-height: 220px;
background-size: cover;
background-position: center;
}
.course-card__image::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(17, 24, 39, 0.10) 0%, rgba(17, 24, 39, 0.45) 100%);
}
.badge {
position: absolute;
top: 18px;
left: 18px;
z-index: 1;
display: inline-flex;
padding: 8px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.92);
color: #111827;
font-size: 12px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.course-card__content {
padding: 24px;
}
.category {
display: inline-block;
margin-bottom: 12px;
color: #4f46e5;
font-weight: 700;
font-size: 0.88rem;
}
.course-card h3 {
margin: 0 0 12px;
font-size: 1.35rem;
line-height: 1.3;
color: #101828;
}
.course-card p {
margin: 0 0 18px;
color: #667085;
line-height: 1.65;
}
.meta {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.meta span {
padding: 8px 12px;
border-radius: 999px;
background: #f2f4f7;
color: #344054;
font-size: 0.88rem;
font-weight: 600;
}
button {
width: 100%;
border: 0;
border-radius: 14px;
padding: 14px 16px;
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
color: #ffffff;
font-size: 0.98rem;
font-weight: 700;
cursor: pointer;
}
button:hover {
filter: brightness(1.05);
}
@media (max-width: 1024px) {
.courses-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.hero {
padding-top: 56px;
}
.courses-header {
flex-direction: column;
align-items: flex-start;
}
.courses-grid {
grid-template-columns: 1fr;
}
}
`]
})
export class CoursesPageComponent {
courses: Course[] = [
{
category: 'Automation Basics',
title: 'Introduction to Intelligent Automation',
description: 'Aprenda os fundamentos da automação, os principais conceitos e como identificar oportunidades de melhoria em processos.',
level: 'Beginner',
duration: '2h 30min',
badge: 'Popular',
image: 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=1200&q=80'
},
{
category: 'Development',
title: 'Build Your First Automation Project',
description: 'Curso prático com abordagem hands-on para criar, testar e publicar o seu primeiro fluxo de automação passo a passo.',
level: 'Intermediate',
duration: '4h 10min',
badge: 'Hands-on',
image: 'https://images.unsplash.com/photo-1555066931-4365d14bab8c?auto=format&fit=crop&w=1200&q=80'
},
{
category: 'AI + Processes',
title: 'AI Tools for Modern Operations',
description: 'Descubra como combinar automação e ferramentas de IA para acelerar tarefas repetitivas e melhorar a eficiência operacional.',
level: 'Advanced',
duration: '3h 45min',
badge: 'New',
image: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?auto=format&fit=crop&w=1200&q=80'
}
];
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="124" viewBox="194 194 124 124" width="124"><linearGradient id="a"><stop offset="0" stop-color="#f18a34"></stop><stop offset=".5029" stop-color="#ed7441"></stop><stop offset="1" stop-color="#e5274c"></stop></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="276.8213" x2="292.749" xlink:href="#a" y1="271.2646" y2="271.2646"></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="219.251" x2="235.1787" xlink:href="#a" y1="271.2646" y2="271.2646"></linearGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="194" x2="318" xlink:href="#a" y1="256" y2="256"></linearGradient><path d="m284.785 273.825c3.531 0 6.582-2.102 7.964-5.121h-15.928c1.383 3.02 4.433 5.121 7.964 5.121z" fill="url(#b)"></path><path d="m227.215 273.825c3.531 0 6.581-2.102 7.964-5.121h-15.928c1.382 3.02 4.432 5.121 7.964 5.121z" fill="url(#c)"></path><path d="m256 194c-34.242 0-62 27.758-62 62s27.758 62 62 62 62-27.758 62-62-27.758-62-62-62zm28.785 87.091c-8.833 0-16.02-7.186-16.02-16.02 0-.514.108-1.002.3-1.445.003-.007.005-.014.008-.021l10.945-28.753c-1.847-.919-3.562-2.236-5.243-3.53-5.008-3.848-8.701-6.68-15.143-1.718v58.902c9.218.988 16.697 5.326 17.051 5.533 1.729 1.019 2.307 3.244 1.288 4.974-.676 1.15-1.889 1.792-3.134 1.792-.627 0-1.261-.162-1.839-.502-.075-.045-8.213-4.732-16.999-4.732-8.852 0-16.919 4.685-17 4.732-1.729 1.017-3.957.439-4.973-1.29-1.017-1.729-.44-3.955 1.289-4.974.353-.207 7.833-4.545 17.051-5.533v-59.039c-6.335-4.779-10.005-1.96-14.968 1.854-1.345 1.034-2.71 2.083-4.147 2.932l6.231 15.506c.748 1.861-.155 3.977-2.016 4.725-.444.179-.903.264-1.354.264-1.44 0-2.802-.862-3.372-2.28l-4.774-11.878-7.937 20.848h19.57c2.006 0 3.633 1.627 3.633 3.633 0 8.834-7.186 16.02-16.019 16.02s-16.02-7.186-16.02-16.02c0-.013.002-.025.002-.037-.018-.475.057-.959.235-1.429l10.956-28.781c-3.336-1.791-5.61-5.311-5.61-9.354 0-2.006 1.626-3.633 3.633-3.633s3.633 1.626 3.633 3.633c0 1.837 1.49 3.334 3.326 3.345 1.384-.011 3.528-1.659 5.602-3.253 2.904-2.232 6.195-4.762 10.391-5.604 3.02-.606 6.001-.25 9.005 1.086v-6.215c0-2.006 1.626-3.633 3.633-3.633s3.633 1.626 3.633 3.633v6.295c3.062-1.401 6.101-1.783 9.18-1.165 4.195.842 7.486 3.372 10.391 5.604 2.073 1.594 4.218 3.242 5.602 3.253 1.837-.01 3.326-1.507 3.326-3.345 0-2.006 1.627-3.633 3.633-3.633s3.633 1.626 3.633 3.633c0 4.078-2.313 7.623-5.695 9.399l5.959 14.897c.745 1.863-.161 3.976-2.024 4.722-.442.177-.899.261-1.348.261-1.441 0-2.806-.865-3.374-2.285l-4.01-10.025-7.232 18.999h19.5c2.007 0 3.633 1.627 3.633 3.633-.001 8.833-7.188 16.019-16.021 16.019z" fill="url(#d)"></path></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="124" viewBox="194 194 124 124" width="124"><linearGradient id="a"><stop offset="0" stop-color="#f18a34"></stop><stop offset=".5029" stop-color="#ed7441"></stop><stop offset="1" stop-color="#e5274c"></stop></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="194" x2="318" xlink:href="#a" y1="256" y2="256"></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="223.4248" x2="272.5908" xlink:href="#a" y1="255.7578" y2="255.7578"></linearGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="236.1021" x2="258.7275" xlink:href="#a" y1="237.5254" y2="237.5254"></linearGradient><path d="m256 194c-34.242 0-62 27.758-62 62s27.758 62 62 62 62-27.758 62-62-27.758-62-62-62zm41.293 73.651c0 2.007-1.626 3.633-3.633 3.633s-3.633-1.626-3.633-3.633v-4.191h-11.514v19.841c0 3.174 2.582 5.757 5.756 5.757h.001c1.537 0 2.983-.598 4.071-1.686 1.087-1.087 1.686-2.533 1.686-4.071 0-2.007 1.627-3.633 3.633-3.633 2.007 0 3.633 1.626 3.633 3.633 0 3.479-1.354 6.749-3.814 9.209-2.439 2.439-5.677 3.792-9.124 3.813-.028.001-.057.001-.085.001l-55.087-.001c-7.181 0-13.023-5.842-13.023-13.022l.001-64.477c0-2.006 1.626-3.633 3.633-3.633h55.087c2.007 0 3.633 1.626 3.633 3.633v37.37h15.147c2.007 0 3.633 1.626 3.633 3.633v7.824z" fill="url(#b)"></path><path d="m271.247 222.457h-47.821l-.001 60.844c0 3.175 2.583 5.757 5.758 5.757l43.408.001c-.86-1.737-1.344-3.691-1.344-5.758zm-29.231 61.348h-9.546c-2.006 0-3.633-1.627-3.633-3.633 0-2.007 1.627-3.633 3.633-3.633h9.546c2.006 0 3.633 1.626 3.633 3.633-.001 2.006-1.627 3.633-3.633 3.633zm0-12.05h-9.546c-2.006 0-3.633-1.628-3.633-3.633 0-2.007 1.627-3.633 3.633-3.633h9.546c2.006 0 3.633 1.626 3.633 3.633-.001 2.005-1.627 3.633-3.633 3.633zm0-12.208h-9.546c-2.006 0-3.633-1.626-3.633-3.633 0-2.006 1.627-3.633 3.633-3.633h9.546c2.006 0 3.633 1.627 3.633 3.633-.001 2.007-1.627 3.633-3.633 3.633zm20.344 24.258h-9.547c-2.005 0-3.633-1.627-3.633-3.633 0-2.007 1.627-3.633 3.633-3.633h9.547c2.007 0 3.633 1.626 3.633 3.633 0 2.006-1.626 3.633-3.633 3.633zm0-12.05h-9.547c-2.005 0-3.633-1.628-3.633-3.633 0-2.007 1.627-3.633 3.633-3.633h9.547c2.007 0 3.633 1.626 3.633 3.633 0 2.005-1.626 3.633-3.633 3.633zm0-12.208h-9.547c-2.005 0-3.633-1.626-3.633-3.633 0-2.006 1.627-3.633 3.633-3.633h9.547c2.007 0 3.633 1.627 3.633 3.633 0 2.007-1.626 3.633-3.633 3.633zm3.633-15.84c0 2.007-1.626 3.633-3.633 3.633h-29.891c-2.006 0-3.633-1.626-3.633-3.633v-12.363c0-2.007 1.627-3.633 3.633-3.633h29.891c2.007 0 3.633 1.626 3.633 3.633z" fill="url(#c)"></path><path d="m236.102 234.977h22.625v5.097h-22.625z" fill="url(#d)"></path></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="124" viewBox="194 194 124 124" width="124"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="194" x2="318" y1="256" y2="256"><stop offset="0" stop-color="#f18a34"></stop><stop offset=".5029" stop-color="#ed7441"></stop><stop offset="1" stop-color="#e5274c"></stop></linearGradient><path d="m256 194c-34.242 0-62 27.758-62 62s27.758 62 62 62 62-27.758 62-62-27.758-62-62-62zm-32.645 23.591 5.297-5.325c1.414-1.423 3.714-1.428 5.137-.013s1.429 3.714.014 5.137l-5.31 5.338c-.01.009-.021.02-.031.03-.599.588-.929 1.365-.929 2.188 0 .822.33 1.6.929 2.187.011.01.021.021.031.031l5.31 5.337c1.415 1.423 1.409 3.723-.014 5.138-.708.705-1.636 1.057-2.562 1.057-.933 0-1.866-.356-2.575-1.071l-5.297-5.324c-1.992-1.964-3.088-4.575-3.088-7.355 0-2.779 1.096-5.391 3.088-7.355zm65.116 76.818-5.297 5.324c-.709.714-1.642 1.071-2.575 1.071-.927 0-1.853-.352-2.562-1.058-1.422-1.414-1.428-3.714-.013-5.137l5.31-5.339c.011-.01.02-.02.03-.03.6-.589.93-1.365.93-2.188 0-.822-.33-1.6-.93-2.188-.011-.01-.02-.021-.03-.03l-5.31-5.338c-1.415-1.423-1.409-3.723.013-5.138 1.423-1.415 3.723-1.409 5.138.014l5.297 5.324c1.992 1.963 3.088 4.575 3.088 7.355-.001 2.783-1.097 5.394-3.089 7.358zm-7.983-26.055c-.369.118-.743.175-1.111.175-1.536 0-2.963-.981-3.459-2.522-.613-1.91.438-3.956 2.348-4.57 9.136-2.935 15.273-11.358 15.273-20.962 0-12.138-9.875-22.013-22.013-22.013s-22.013 9.875-22.013 22.013c0 1.102.083 2.202.245 3.286 3.872 1.298 7.386 3.384 10.349 6.068.198-.521.511-.984.91-1.358.645-.692 1.553-1.135 2.566-1.16.387-.113 1.325-.938 1.49-3.377h-.177c-2.007 0-3.633-1.626-3.633-3.633 0-1.768 1.264-3.24 2.938-3.564-.087-.841-.146-1.744-.146-2.715 0-4.519 3.689-8.196 8.225-8.196h4.311c2.007 0 3.633 1.627 3.633 3.633s-1.626 3.633-3.633 3.633h-4.311c-.547 0-.959.4-.959.931 0 .944.077 1.816.189 2.647h2.109c2.007 0 3.633 1.627 3.633 3.633 0 2.007-1.626 3.633-3.633 3.633h-1.267c-.052 1.324-.241 2.502-.533 3.551h6.336c2.006 0 3.633 1.627 3.633 3.633s-1.627 3.633-3.633 3.633h-13.703c3.337 4.755 5.298 10.542 5.298 16.779 0 4.126-.842 8.116-2.504 11.861-.812 1.834-2.959 2.66-4.793 1.848-1.834-.813-2.661-2.96-1.848-4.793 1.248-2.812 1.88-5.811 1.88-8.916 0-12.138-9.875-22.013-22.013-22.013s-22.013 9.875-22.013 22.013 9.875 22.013 22.013 22.013c3.593 0 7.158-.884 10.307-2.557 1.772-.941 3.971-.268 4.912 1.505.941 1.771.268 3.972-1.504 4.911-4.195 2.229-8.938 3.406-13.715 3.406-16.145 0-29.278-13.134-29.278-29.278s13.134-29.278 29.278-29.277c.616 0 1.228.021 1.834.059-.038-.61-.06-1.223-.06-1.834 0-16.145 13.134-29.278 29.278-29.278 16.145 0 29.278 13.134 29.278 29.278.003 12.766-8.161 23.97-20.314 27.874zm-45.597 16.924c-2.007 0-3.633-1.627-3.633-3.633 0-2.007 1.626-3.633 3.633-3.633h7.082c.554 0 .973-.396.973-.921v-1.398c0-.516-.419-.903-.973-.903h-2.322c-4.535 0-8.225-3.733-8.225-8.323v-.146c0-3.536 2.259-6.556 5.415-7.702v-.874c0-2.006 1.626-3.632 3.633-3.632 2.005 0 3.633 1.626 3.633 3.632v.388c1.938.075 3.489 1.667 3.489 3.625 0 2.007-1.627 3.633-3.633 3.633h-4.311c-.546 0-.958.4-.958.931v.146c0 .564.448 1.058.958 1.058h2.322c4.543 0 8.239 3.664 8.239 8.169v1.398c0 3.78-2.594 6.97-6.105 7.906v.485c0 2.006-1.626 3.633-3.633 3.633s-3.633-1.627-3.633-3.633v-.205h-1.951z" fill="url(#a)"></path></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><defs><linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#f59e0b" /><stop offset="100%" stop-color="#f43f5e" /></linearGradient></defs><circle cx="256" cy="256" r="240" fill="url(#grad)" /><line x1="120" y1="180" x2="380" y2="180" stroke="white" stroke-width="24" stroke-linecap="round" /><line x1="120" y1="256" x2="380" y2="256" stroke="white" stroke-width="24" stroke-linecap="round" /><line x1="120" y1="332" x2="380" y2="332" stroke="white" stroke-width="24" stroke-linecap="round" /><circle cx="200" cy="180" r="28" fill="none" stroke="white" stroke-width="16" /><circle cx="300" cy="256" r="28" fill="none" stroke="white" stroke-width="16" /><circle cx="240" cy="332" r="28" fill="none" stroke="white" stroke-width="16" /></svg>

After

Width:  |  Height:  |  Size: 863 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="124" viewBox="194 194 124 124" width="124"><linearGradient id="a"><stop offset="0" stop-color="#f18a34"></stop><stop offset=".5029" stop-color="#ed7441"></stop><stop offset="1" stop-color="#e5274c"></stop></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="269.5049" x2="273.6309" xlink:href="#a" y1="269.2891" y2="269.2891"></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="238.1802" x2="242.3052" xlink:href="#a" y1="242.7104" y2="242.7104"></linearGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="194" x2="318" xlink:href="#a" y1="256" y2="256"></linearGradient><circle cx="271.567" cy="269.29" fill="url(#b)" r="2.063"></circle><circle cx="240.243" cy="242.71" fill="url(#c)" r="2.063"></circle><path d="m256 194c-34.242 0-62 27.758-62 62s27.758 62 62 62 62-27.758 62-62-27.758-62-62-62zm40.23 88.89c-1.114 1.668-3.371 2.115-5.038 1.001-1.668-1.115-2.117-3.371-1.001-5.039 4.566-6.831 6.98-14.733 6.98-22.852 0-22.702-18.47-41.172-41.172-41.172s-41.172 18.47-41.172 41.172 18.47 41.172 41.172 41.172c7.529 0 14.895-2.05 21.298-5.93 1.717-1.04 3.95-.49 4.99 1.225 1.039 1.717.49 3.951-1.226 4.991-7.539 4.565-16.206 6.979-25.062 6.979-26.709 0-48.438-21.729-48.438-48.438s21.729-48.438 48.438-48.438 48.438 21.729 48.438 48.438c.001 9.562-2.837 18.859-8.207 26.891zm-53.334-9.632 20.524-37.97c.952-1.765 3.157-2.424 4.923-1.469s2.422 3.159 1.469 4.923l-20.525 37.971c-.657 1.216-1.908 1.906-3.199 1.906-.583 0-1.174-.141-1.724-.438-1.765-.954-2.422-3.158-1.468-4.923zm-2.653-21.219c-5.145 0-9.329-4.185-9.329-9.328 0-5.145 4.185-9.329 9.329-9.329s9.328 4.186 9.328 9.329-4.185 9.328-9.328 9.328zm31.324 7.922c5.145 0 9.329 4.185 9.329 9.328 0 5.145-4.186 9.329-9.329 9.329s-9.328-4.185-9.328-9.329c0-5.143 4.185-9.328 9.328-9.328z" fill="url(#d)"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="124" viewBox="194 194 124 124" width="124"><linearGradient id="a"><stop offset="0" stop-color="#f18a34"></stop><stop offset=".5029" stop-color="#ed7441"></stop><stop offset="1" stop-color="#e5274c"></stop></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="222.457" x2="245.8652" xlink:href="#a" y1="274.5527" y2="274.5527"></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="265.6494" x2="289.0586" xlink:href="#a" y1="256.2432" y2="256.2432"></linearGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="194" x2="318" xlink:href="#a" y1="256" y2="256"></linearGradient><path d="m234.161 259.719c-6.264 0-10.206 1.631-11.428 2.627 1.223.997 5.166 2.628 11.428 2.628.903 0 1.806-.036 2.684-.106 2-.16 3.751 1.331 3.911 3.331.161 2-1.331 3.751-3.331 3.911-1.069.086-2.167.13-3.264.13-4.325 0-8.454-.675-11.704-1.904v3.795c.766.938 4.843 2.894 11.704 2.894.903 0 1.806-.035 2.683-.106 1.998-.159 3.752 1.331 3.912 3.331.161 2-1.331 3.751-3.331 3.912-1.069.085-2.167.129-3.264.129-4.325 0-8.454-.675-11.704-1.903v4.109c.77.938 4.847 2.892 11.704 2.892 6.858 0 10.934-1.954 11.705-2.892v-23.884c-.771-.939-4.847-2.894-11.705-2.894z" fill="url(#b)"></path><path d="m289.058 225.991c-.771-.937-4.847-2.891-11.704-2.891-5.945 0-9.799 1.468-11.221 2.47 1.421 1.002 5.274 2.471 11.221 2.471.903 0 1.807-.036 2.683-.106 2.001-.161 3.752 1.33 3.912 3.33.161 2.001-1.331 3.752-3.331 3.912-1.069.086-2.168.129-3.263.129-4.326 0-8.455-.674-11.705-1.903v4.265c.768.937 4.843 2.892 11.705 2.892.903 0 1.806-.035 2.683-.106 2-.159 3.751 1.331 3.911 3.331.161 2-1.331 3.751-3.33 3.912-1.07.085-2.168.128-3.264.128-4.326 0-8.454-.673-11.705-1.902v3.794c.768.937 4.843 2.893 11.705 2.893.903 0 1.806-.036 2.683-.106 2-.162 3.751 1.331 3.912 3.33.16 2-1.331 3.752-3.331 3.912-1.069.086-2.168.13-3.264.13-4.326 0-8.454-.675-11.705-1.903v4.107c.768.937 4.844 2.894 11.705 2.894.903 0 1.806-.036 2.683-.106 2-.16 3.751 1.331 3.912 3.331.16 2-1.331 3.751-3.331 3.911-1.069.086-2.168.13-3.264.13-4.325 0-8.454-.675-11.704-1.904v3.795c.767.938 4.843 2.894 11.704 2.894.903 0 1.807-.035 2.684-.106 2-.159 3.751 1.331 3.911 3.331s-1.331 3.751-3.331 3.912c-1.069.085-2.168.129-3.263.129-4.326 0-8.455-.675-11.705-1.903v4.109c.771.938 4.847 2.892 11.705 2.892 6.856 0 10.933-1.954 11.703-2.892h-.001z" fill="url(#c)"></path><path d="m256 194c-34.242 0-62 27.758-62 62s27.758 62 62 62 62-27.758 62-62-27.758-62-62-62zm-33.935 47.916 4.94-4.862c1.87-1.878 4.354-2.911 6.999-2.911s5.128 1.033 6.999 2.911l4.939 4.862c1.43 1.408 1.448 3.708.041 5.137-.711.723-1.65 1.084-2.589 1.084-.92 0-1.841-.347-2.549-1.043l-4.954-4.876c-.01-.011-.021-.02-.03-.03-.499-.502-1.158-.779-1.857-.779-.7 0-1.359.276-1.857.779-.011.01-.02.02-.03.03l-4.954 4.876c-1.43 1.407-3.73 1.39-5.138-.041-1.407-1.429-1.389-3.729.04-5.137zm31.066 44.844c0 1.88-.877 5.392-6.752 7.79-3.323 1.355-7.661 2.103-12.218 2.103-4.556 0-8.895-.747-12.217-2.103-5.875-2.398-6.752-5.91-6.752-7.79v-24.415c0-1.88.877-5.392 6.752-7.79 3.322-1.355 7.661-2.102 12.217-2.102 4.557 0 8.896.747 12.218 2.102 5.875 2.398 6.752 5.91 6.752 7.79zm43.193 0c0 1.88-.877 5.392-6.752 7.79-3.322 1.355-7.662 2.103-12.218 2.103-4.557 0-8.896-.747-12.218-2.103-5.875-2.398-6.752-5.91-6.752-7.79v-61.035c0-.051.001-.103.002-.156-.001-.053-.002-.106-.002-.157 0-.338.051-.664.137-.975.408-1.94 1.904-4.579 6.615-6.502 3.322-1.356 7.661-2.103 12.219-2.103 4.556 0 8.895.747 12.217 2.103 5.875 2.398 6.752 5.91 6.752 7.79z" fill="url(#d)"></path></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

@@ -0,0 +1,114 @@
######## Como publicar angular work ############
npm install -> se necessário
ng serve -> se necessário
ng build -> gerar o dist
ssh teu_user@IP_DO_SERVIDOR -> comando para entrar no serviro (meter user@ip)
##Criar pasta do site no servidor eu usei um docker##
mkdir -p /docker/angular/metacaseform
### Copiar os ficheiros do PC para o servidor onde coloquei a pagina da metacase do Docker ###
scp -r dist/form_metacase/browser/* USERNAME@IP:/docker/angular/metacaseform/
#### criar o composse.yml e as configurações para o Nginx para criar o Docker ####
nano /docker/angular/metacaseform/nginx.conf
nano /docker/angular/metacaseform/docker-compose.yml
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location = /favicon.ico {
log_not_found off;
access_log off;
try_files $uri =204;
}
}
version: "3.9"
services:
angular-metacase:
image: nginx:latest
container_name: angular-metacase
restart: unless-stopped
ports:
- "8090:80"
volumes:
- /docker/angular/metacaseform/app:/usr/share/nginx/html:ro
- /docker/angular/metacaseform/nginx.conf:/etc/nginx/conf.d/default.conf:ro
##### arrancar o container #####
cd /docker/angular/metacaseform
docker compose up -d
#### IR ao Ngnix configurar o acesso http…. blabla e escolher uma porta livre no caso especifico escolhi a 8090 ####
Block Common Exploits ✓
Websockets Support ✓
Request a new certificate
Force SSL ✓
HTTP/2 Support ✓
##### Fui ao meu provaider de domínio registar o metacasehub ####
https://www.ovh.com/manager
2️⃣ Ir para a gestão do domínio
No menu da esquerda, Web Cloud → Domains
Selecionar o domínio:
Clica em DNS Zone
Criar ou editar o registo
Clica em Add an entry Escolher A CNAE:
Subdomain: metacasehub (NESTE cASO)
Target: IP_PUBLICO
TTL: Default
Exemplo:
metacasehub → 85.xxx.xxx.xxx
Saber o IP publico no meu caso eu não usei isto porque estou a usar o duck DNS mas pode ser feito por aqui:
👉 https://whatismyipaddress.com/
ou correr no terminal:
curl ifconfig.me
e deve estar
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="124" viewBox="194 194 124 124" width="124"><linearGradient id="a"><stop offset="0" stop-color="#f18a34"></stop><stop offset=".5029" stop-color="#ed7441"></stop><stop offset="1" stop-color="#e5274c"></stop></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="276.8213" x2="292.749" xlink:href="#a" y1="271.2646" y2="271.2646"></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="219.251" x2="235.1787" xlink:href="#a" y1="271.2646" y2="271.2646"></linearGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="194" x2="318" xlink:href="#a" y1="256" y2="256"></linearGradient><path d="m284.785 273.825c3.531 0 6.582-2.102 7.964-5.121h-15.928c1.383 3.02 4.433 5.121 7.964 5.121z" fill="url(#b)"></path><path d="m227.215 273.825c3.531 0 6.581-2.102 7.964-5.121h-15.928c1.382 3.02 4.432 5.121 7.964 5.121z" fill="url(#c)"></path><path d="m256 194c-34.242 0-62 27.758-62 62s27.758 62 62 62 62-27.758 62-62-27.758-62-62-62zm28.785 87.091c-8.833 0-16.02-7.186-16.02-16.02 0-.514.108-1.002.3-1.445.003-.007.005-.014.008-.021l10.945-28.753c-1.847-.919-3.562-2.236-5.243-3.53-5.008-3.848-8.701-6.68-15.143-1.718v58.902c9.218.988 16.697 5.326 17.051 5.533 1.729 1.019 2.307 3.244 1.288 4.974-.676 1.15-1.889 1.792-3.134 1.792-.627 0-1.261-.162-1.839-.502-.075-.045-8.213-4.732-16.999-4.732-8.852 0-16.919 4.685-17 4.732-1.729 1.017-3.957.439-4.973-1.29-1.017-1.729-.44-3.955 1.289-4.974.353-.207 7.833-4.545 17.051-5.533v-59.039c-6.335-4.779-10.005-1.96-14.968 1.854-1.345 1.034-2.71 2.083-4.147 2.932l6.231 15.506c.748 1.861-.155 3.977-2.016 4.725-.444.179-.903.264-1.354.264-1.44 0-2.802-.862-3.372-2.28l-4.774-11.878-7.937 20.848h19.57c2.006 0 3.633 1.627 3.633 3.633 0 8.834-7.186 16.02-16.019 16.02s-16.02-7.186-16.02-16.02c0-.013.002-.025.002-.037-.018-.475.057-.959.235-1.429l10.956-28.781c-3.336-1.791-5.61-5.311-5.61-9.354 0-2.006 1.626-3.633 3.633-3.633s3.633 1.626 3.633 3.633c0 1.837 1.49 3.334 3.326 3.345 1.384-.011 3.528-1.659 5.602-3.253 2.904-2.232 6.195-4.762 10.391-5.604 3.02-.606 6.001-.25 9.005 1.086v-6.215c0-2.006 1.626-3.633 3.633-3.633s3.633 1.626 3.633 3.633v6.295c3.062-1.401 6.101-1.783 9.18-1.165 4.195.842 7.486 3.372 10.391 5.604 2.073 1.594 4.218 3.242 5.602 3.253 1.837-.01 3.326-1.507 3.326-3.345 0-2.006 1.627-3.633 3.633-3.633s3.633 1.626 3.633 3.633c0 4.078-2.313 7.623-5.695 9.399l5.959 14.897c.745 1.863-.161 3.976-2.024 4.722-.442.177-.899.261-1.348.261-1.441 0-2.806-.865-3.374-2.285l-4.01-10.025-7.232 18.999h19.5c2.007 0 3.633 1.627 3.633 3.633-.001 8.833-7.188 16.019-16.021 16.019z" fill="url(#d)"></path></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="124" viewBox="194 194 124 124" width="124"><linearGradient id="a"><stop offset="0" stop-color="#f18a34"></stop><stop offset=".5029" stop-color="#ed7441"></stop><stop offset="1" stop-color="#e5274c"></stop></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="194" x2="318" xlink:href="#a" y1="256" y2="256"></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="223.4248" x2="272.5908" xlink:href="#a" y1="255.7578" y2="255.7578"></linearGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="236.1021" x2="258.7275" xlink:href="#a" y1="237.5254" y2="237.5254"></linearGradient><path d="m256 194c-34.242 0-62 27.758-62 62s27.758 62 62 62 62-27.758 62-62-27.758-62-62-62zm41.293 73.651c0 2.007-1.626 3.633-3.633 3.633s-3.633-1.626-3.633-3.633v-4.191h-11.514v19.841c0 3.174 2.582 5.757 5.756 5.757h.001c1.537 0 2.983-.598 4.071-1.686 1.087-1.087 1.686-2.533 1.686-4.071 0-2.007 1.627-3.633 3.633-3.633 2.007 0 3.633 1.626 3.633 3.633 0 3.479-1.354 6.749-3.814 9.209-2.439 2.439-5.677 3.792-9.124 3.813-.028.001-.057.001-.085.001l-55.087-.001c-7.181 0-13.023-5.842-13.023-13.022l.001-64.477c0-2.006 1.626-3.633 3.633-3.633h55.087c2.007 0 3.633 1.626 3.633 3.633v37.37h15.147c2.007 0 3.633 1.626 3.633 3.633v7.824z" fill="url(#b)"></path><path d="m271.247 222.457h-47.821l-.001 60.844c0 3.175 2.583 5.757 5.758 5.757l43.408.001c-.86-1.737-1.344-3.691-1.344-5.758zm-29.231 61.348h-9.546c-2.006 0-3.633-1.627-3.633-3.633 0-2.007 1.627-3.633 3.633-3.633h9.546c2.006 0 3.633 1.626 3.633 3.633-.001 2.006-1.627 3.633-3.633 3.633zm0-12.05h-9.546c-2.006 0-3.633-1.628-3.633-3.633 0-2.007 1.627-3.633 3.633-3.633h9.546c2.006 0 3.633 1.626 3.633 3.633-.001 2.005-1.627 3.633-3.633 3.633zm0-12.208h-9.546c-2.006 0-3.633-1.626-3.633-3.633 0-2.006 1.627-3.633 3.633-3.633h9.546c2.006 0 3.633 1.627 3.633 3.633-.001 2.007-1.627 3.633-3.633 3.633zm20.344 24.258h-9.547c-2.005 0-3.633-1.627-3.633-3.633 0-2.007 1.627-3.633 3.633-3.633h9.547c2.007 0 3.633 1.626 3.633 3.633 0 2.006-1.626 3.633-3.633 3.633zm0-12.05h-9.547c-2.005 0-3.633-1.628-3.633-3.633 0-2.007 1.627-3.633 3.633-3.633h9.547c2.007 0 3.633 1.626 3.633 3.633 0 2.005-1.626 3.633-3.633 3.633zm0-12.208h-9.547c-2.005 0-3.633-1.626-3.633-3.633 0-2.006 1.627-3.633 3.633-3.633h9.547c2.007 0 3.633 1.627 3.633 3.633 0 2.007-1.626 3.633-3.633 3.633zm3.633-15.84c0 2.007-1.626 3.633-3.633 3.633h-29.891c-2.006 0-3.633-1.626-3.633-3.633v-12.363c0-2.007 1.627-3.633 3.633-3.633h29.891c2.007 0 3.633 1.626 3.633 3.633z" fill="url(#c)"></path><path d="m236.102 234.977h22.625v5.097h-22.625z" fill="url(#d)"></path></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="124" viewBox="194 194 124 124" width="124"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="194" x2="318" y1="256" y2="256"><stop offset="0" stop-color="#f18a34"></stop><stop offset=".5029" stop-color="#ed7441"></stop><stop offset="1" stop-color="#e5274c"></stop></linearGradient><path d="m256 194c-34.242 0-62 27.758-62 62s27.758 62 62 62 62-27.758 62-62-27.758-62-62-62zm-32.645 23.591 5.297-5.325c1.414-1.423 3.714-1.428 5.137-.013s1.429 3.714.014 5.137l-5.31 5.338c-.01.009-.021.02-.031.03-.599.588-.929 1.365-.929 2.188 0 .822.33 1.6.929 2.187.011.01.021.021.031.031l5.31 5.337c1.415 1.423 1.409 3.723-.014 5.138-.708.705-1.636 1.057-2.562 1.057-.933 0-1.866-.356-2.575-1.071l-5.297-5.324c-1.992-1.964-3.088-4.575-3.088-7.355 0-2.779 1.096-5.391 3.088-7.355zm65.116 76.818-5.297 5.324c-.709.714-1.642 1.071-2.575 1.071-.927 0-1.853-.352-2.562-1.058-1.422-1.414-1.428-3.714-.013-5.137l5.31-5.339c.011-.01.02-.02.03-.03.6-.589.93-1.365.93-2.188 0-.822-.33-1.6-.93-2.188-.011-.01-.02-.021-.03-.03l-5.31-5.338c-1.415-1.423-1.409-3.723.013-5.138 1.423-1.415 3.723-1.409 5.138.014l5.297 5.324c1.992 1.963 3.088 4.575 3.088 7.355-.001 2.783-1.097 5.394-3.089 7.358zm-7.983-26.055c-.369.118-.743.175-1.111.175-1.536 0-2.963-.981-3.459-2.522-.613-1.91.438-3.956 2.348-4.57 9.136-2.935 15.273-11.358 15.273-20.962 0-12.138-9.875-22.013-22.013-22.013s-22.013 9.875-22.013 22.013c0 1.102.083 2.202.245 3.286 3.872 1.298 7.386 3.384 10.349 6.068.198-.521.511-.984.91-1.358.645-.692 1.553-1.135 2.566-1.16.387-.113 1.325-.938 1.49-3.377h-.177c-2.007 0-3.633-1.626-3.633-3.633 0-1.768 1.264-3.24 2.938-3.564-.087-.841-.146-1.744-.146-2.715 0-4.519 3.689-8.196 8.225-8.196h4.311c2.007 0 3.633 1.627 3.633 3.633s-1.626 3.633-3.633 3.633h-4.311c-.547 0-.959.4-.959.931 0 .944.077 1.816.189 2.647h2.109c2.007 0 3.633 1.627 3.633 3.633 0 2.007-1.626 3.633-3.633 3.633h-1.267c-.052 1.324-.241 2.502-.533 3.551h6.336c2.006 0 3.633 1.627 3.633 3.633s-1.627 3.633-3.633 3.633h-13.703c3.337 4.755 5.298 10.542 5.298 16.779 0 4.126-.842 8.116-2.504 11.861-.812 1.834-2.959 2.66-4.793 1.848-1.834-.813-2.661-2.96-1.848-4.793 1.248-2.812 1.88-5.811 1.88-8.916 0-12.138-9.875-22.013-22.013-22.013s-22.013 9.875-22.013 22.013 9.875 22.013 22.013 22.013c3.593 0 7.158-.884 10.307-2.557 1.772-.941 3.971-.268 4.912 1.505.941 1.771.268 3.972-1.504 4.911-4.195 2.229-8.938 3.406-13.715 3.406-16.145 0-29.278-13.134-29.278-29.278s13.134-29.278 29.278-29.277c.616 0 1.228.021 1.834.059-.038-.61-.06-1.223-.06-1.834 0-16.145 13.134-29.278 29.278-29.278 16.145 0 29.278 13.134 29.278 29.278.003 12.766-8.161 23.97-20.314 27.874zm-45.597 16.924c-2.007 0-3.633-1.627-3.633-3.633 0-2.007 1.626-3.633 3.633-3.633h7.082c.554 0 .973-.396.973-.921v-1.398c0-.516-.419-.903-.973-.903h-2.322c-4.535 0-8.225-3.733-8.225-8.323v-.146c0-3.536 2.259-6.556 5.415-7.702v-.874c0-2.006 1.626-3.632 3.633-3.632 2.005 0 3.633 1.626 3.633 3.632v.388c1.938.075 3.489 1.667 3.489 3.625 0 2.007-1.627 3.633-3.633 3.633h-4.311c-.546 0-.958.4-.958.931v.146c0 .564.448 1.058.958 1.058h2.322c4.543 0 8.239 3.664 8.239 8.169v1.398c0 3.78-2.594 6.97-6.105 7.906v.485c0 2.006-1.626 3.633-3.633 3.633s-3.633-1.627-3.633-3.633v-.205h-1.951z" fill="url(#a)"></path></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="124" viewBox="194 194 124 124" width="124"><linearGradient id="a"><stop offset="0" stop-color="#f18a34"></stop><stop offset=".5029" stop-color="#ed7441"></stop><stop offset="1" stop-color="#e5274c"></stop></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="269.5049" x2="273.6309" xlink:href="#a" y1="269.2891" y2="269.2891"></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="238.1802" x2="242.3052" xlink:href="#a" y1="242.7104" y2="242.7104"></linearGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="194" x2="318" xlink:href="#a" y1="256" y2="256"></linearGradient><circle cx="271.567" cy="269.29" fill="url(#b)" r="2.063"></circle><circle cx="240.243" cy="242.71" fill="url(#c)" r="2.063"></circle><path d="m256 194c-34.242 0-62 27.758-62 62s27.758 62 62 62 62-27.758 62-62-27.758-62-62-62zm40.23 88.89c-1.114 1.668-3.371 2.115-5.038 1.001-1.668-1.115-2.117-3.371-1.001-5.039 4.566-6.831 6.98-14.733 6.98-22.852 0-22.702-18.47-41.172-41.172-41.172s-41.172 18.47-41.172 41.172 18.47 41.172 41.172 41.172c7.529 0 14.895-2.05 21.298-5.93 1.717-1.04 3.95-.49 4.99 1.225 1.039 1.717.49 3.951-1.226 4.991-7.539 4.565-16.206 6.979-25.062 6.979-26.709 0-48.438-21.729-48.438-48.438s21.729-48.438 48.438-48.438 48.438 21.729 48.438 48.438c.001 9.562-2.837 18.859-8.207 26.891zm-53.334-9.632 20.524-37.97c.952-1.765 3.157-2.424 4.923-1.469s2.422 3.159 1.469 4.923l-20.525 37.971c-.657 1.216-1.908 1.906-3.199 1.906-.583 0-1.174-.141-1.724-.438-1.765-.954-2.422-3.158-1.468-4.923zm-2.653-21.219c-5.145 0-9.329-4.185-9.329-9.328 0-5.145 4.185-9.329 9.329-9.329s9.328 4.186 9.328 9.329-4.185 9.328-9.328 9.328zm31.324 7.922c5.145 0 9.329 4.185 9.329 9.328 0 5.145-4.186 9.329-9.329 9.329s-9.328-4.185-9.328-9.329c0-5.143 4.185-9.328 9.328-9.328z" fill="url(#d)"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="124" viewBox="194 194 124 124" width="124"><linearGradient id="a"><stop offset="0" stop-color="#f18a34"></stop><stop offset=".5029" stop-color="#ed7441"></stop><stop offset="1" stop-color="#e5274c"></stop></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="222.457" x2="245.8652" xlink:href="#a" y1="274.5527" y2="274.5527"></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="265.6494" x2="289.0586" xlink:href="#a" y1="256.2432" y2="256.2432"></linearGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="194" x2="318" xlink:href="#a" y1="256" y2="256"></linearGradient><path d="m234.161 259.719c-6.264 0-10.206 1.631-11.428 2.627 1.223.997 5.166 2.628 11.428 2.628.903 0 1.806-.036 2.684-.106 2-.16 3.751 1.331 3.911 3.331.161 2-1.331 3.751-3.331 3.911-1.069.086-2.167.13-3.264.13-4.325 0-8.454-.675-11.704-1.904v3.795c.766.938 4.843 2.894 11.704 2.894.903 0 1.806-.035 2.683-.106 1.998-.159 3.752 1.331 3.912 3.331.161 2-1.331 3.751-3.331 3.912-1.069.085-2.167.129-3.264.129-4.325 0-8.454-.675-11.704-1.903v4.109c.77.938 4.847 2.892 11.704 2.892 6.858 0 10.934-1.954 11.705-2.892v-23.884c-.771-.939-4.847-2.894-11.705-2.894z" fill="url(#b)"></path><path d="m289.058 225.991c-.771-.937-4.847-2.891-11.704-2.891-5.945 0-9.799 1.468-11.221 2.47 1.421 1.002 5.274 2.471 11.221 2.471.903 0 1.807-.036 2.683-.106 2.001-.161 3.752 1.33 3.912 3.33.161 2.001-1.331 3.752-3.331 3.912-1.069.086-2.168.129-3.263.129-4.326 0-8.455-.674-11.705-1.903v4.265c.768.937 4.843 2.892 11.705 2.892.903 0 1.806-.035 2.683-.106 2-.159 3.751 1.331 3.911 3.331.161 2-1.331 3.751-3.33 3.912-1.07.085-2.168.128-3.264.128-4.326 0-8.454-.673-11.705-1.902v3.794c.768.937 4.843 2.893 11.705 2.893.903 0 1.806-.036 2.683-.106 2-.162 3.751 1.331 3.912 3.33.16 2-1.331 3.752-3.331 3.912-1.069.086-2.168.13-3.264.13-4.326 0-8.454-.675-11.705-1.903v4.107c.768.937 4.844 2.894 11.705 2.894.903 0 1.806-.036 2.683-.106 2-.16 3.751 1.331 3.912 3.331.16 2-1.331 3.751-3.331 3.911-1.069.086-2.168.13-3.264.13-4.325 0-8.454-.675-11.704-1.904v3.795c.767.938 4.843 2.894 11.704 2.894.903 0 1.807-.035 2.684-.106 2-.159 3.751 1.331 3.911 3.331s-1.331 3.751-3.331 3.912c-1.069.085-2.168.129-3.263.129-4.326 0-8.455-.675-11.705-1.903v4.109c.771.938 4.847 2.892 11.705 2.892 6.856 0 10.933-1.954 11.703-2.892h-.001z" fill="url(#c)"></path><path d="m256 194c-34.242 0-62 27.758-62 62s27.758 62 62 62 62-27.758 62-62-27.758-62-62-62zm-33.935 47.916 4.94-4.862c1.87-1.878 4.354-2.911 6.999-2.911s5.128 1.033 6.999 2.911l4.939 4.862c1.43 1.408 1.448 3.708.041 5.137-.711.723-1.65 1.084-2.589 1.084-.92 0-1.841-.347-2.549-1.043l-4.954-4.876c-.01-.011-.021-.02-.03-.03-.499-.502-1.158-.779-1.857-.779-.7 0-1.359.276-1.857.779-.011.01-.02.02-.03.03l-4.954 4.876c-1.43 1.407-3.73 1.39-5.138-.041-1.407-1.429-1.389-3.729.04-5.137zm31.066 44.844c0 1.88-.877 5.392-6.752 7.79-3.323 1.355-7.661 2.103-12.218 2.103-4.556 0-8.895-.747-12.217-2.103-5.875-2.398-6.752-5.91-6.752-7.79v-24.415c0-1.88.877-5.392 6.752-7.79 3.322-1.355 7.661-2.102 12.217-2.102 4.557 0 8.896.747 12.218 2.102 5.875 2.398 6.752 5.91 6.752 7.79zm43.193 0c0 1.88-.877 5.392-6.752 7.79-3.322 1.355-7.662 2.103-12.218 2.103-4.557 0-8.896-.747-12.218-2.103-5.875-2.398-6.752-5.91-6.752-7.79v-61.035c0-.051.001-.103.002-.156-.001-.053-.002-.106-.002-.157 0-.338.051-.664.137-.975.408-1.94 1.904-4.579 6.615-6.502 3.322-1.356 7.661-2.103 12.219-2.103 4.556 0 8.895.747 12.217 2.103 5.875 2.398 6.752 5.91 6.752 7.79z" fill="url(#d)"></path></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.
+21 -1
View File
@@ -1,3 +1,23 @@
import { Routes } from '@angular/router';
import { Formacaometacase } from './formacaometacase/formacaometacase';
import { CourseDetailPageComponent } from './course-detail-page/course-detail-page';
import { CourseLearningPageComponent } from './course-learning-page/course-learning-page';
export const routes: Routes = [];
export const routes: Routes = [
{
path: '',
component: Formacaometacase
},
{
path: 'cursos/:slug',
component: CourseDetailPageComponent
},
{
path: 'cursos/:slug/percurso',
component: CourseLearningPageComponent
},
{
path: '**',
redirectTo: ''
}
];
+3 -7
View File
@@ -1,10 +1,13 @@
import { TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { App } from './app';
import { routes } from './app.routes';
describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
providers: [provideRouter(routes)]
}).compileComponents();
});
@@ -13,11 +16,4 @@ describe('App', () => {
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it('should render title', async () => {
const fixture = TestBed.createComponent(App);
await fixture.whenStable();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, form_metacase');
});
});
+3 -4
View File
@@ -1,12 +1,11 @@
import { Component, signal } from '@angular/core';
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
templateUrl: './app.html',
styleUrl: './app.css'
template: `<router-outlet></router-outlet>`
})
export class App {
protected readonly title = signal('form_metacase');
}
@@ -0,0 +1,527 @@
:host {
display: block;
min-height: 100vh;
background: #f8fafc;
color: #0f172a;
}
* {
box-sizing: border-box;
}
.course-detail-page {
min-height: 100vh;
}
.course-intro {
background:
radial-gradient(circle at top left, rgba(37, 99, 235, 0.12), transparent 24%),
linear-gradient(180deg, #ffffff 0%, #f6f9fd 100%);
border-bottom: 1px solid #e2e8f0;
}
.course-intro__inner {
display: grid;
grid-template-columns: minmax(0, 1.55fr) minmax(320px, 420px);
gap: 34px;
max-width: 1240px;
margin: 0 auto;
padding: 56px 24px 40px;
}
.course-intro__main {
padding-top: 12px;
}
.back-link {
display: inline-flex;
align-items: center;
margin-bottom: 20px;
color: #2563eb;
text-decoration: none;
font-weight: 700;
}
.back-link:hover {
color: #1d4ed8;
}
.intro-pills {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 18px;
}
.intro-pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 34px;
padding: 0 14px;
border-radius: 999px;
background: #dbeafe;
color: #1d4ed8;
font-size: 0.86rem;
font-weight: 800;
}
.intro-pill--muted {
background: #eff6ff;
color: #334155;
}
.course-intro h1 {
max-width: 760px;
margin: 0 0 16px;
font-size: clamp(2.4rem, 5vw, 4rem);
line-height: 1.04;
letter-spacing: -0.03em;
}
.lesson-image {
display: block;
margin: 30px auto;
max-width: 600px;
width: 100%;
}
.intro-summary {
max-width: 760px;
margin: 0 0 24px;
color: #475569;
font-size: 1.06rem;
line-height: 1.75;
text-align: justify;
}
.intro-stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
max-width: 720px;
margin-bottom: 20px;
}
.intro-stat {
padding: 18px 18px 16px;
border: 1px solid #dbe4f0;
border-radius: 22px;
background: #ffffff;
}
.intro-stat span {
display: block;
margin-bottom: 8px;
color: #64748b;
font-size: 0.8rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.intro-stat strong {
font-size: 1rem;
}
.intro-icons {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.intro-icons__item {
width: auto;
height: 28px;
}
.enroll-card {
overflow: hidden;
border: 1px solid #dbe4f0;
border-radius: 28px;
background: #ffffff;
box-shadow: 0 18px 42px rgba(15, 23, 42, 0.08);
}
.enroll-card__media {
aspect-ratio: 16 / 10;
overflow: hidden;
background: #0f172a;
}
.enroll-card__media img {
width: 100%;
height: 100%;
object-fit: cover;
}
.enroll-card__body {
padding: 22px;
}
.enroll-card__topline {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 14px;
}
.enroll-card__topline span {
display: inline-flex;
padding: 7px 11px;
border-radius: 999px;
background: #eff6ff;
color: #1d4ed8;
font-size: 0.8rem;
font-weight: 800;
}
.enroll-card h2 {
margin: 0 0 10px;
font-size: 1.55rem;
}
.enroll-card p {
margin: 0 0 18px;
color: #64748b;
line-height: 1.65;
}
.detail-tabs {
position: sticky;
top: 0;
z-index: 20;
display: flex;
gap: 28px;
max-width: 1240px;
margin: 0 auto;
padding: 0 24px;
background: rgba(248, 250, 252, 0.94);
backdrop-filter: blur(12px);
border-bottom: 1px solid #e2e8f0;
}
.detail-tabs a {
display: inline-flex;
align-items: center;
min-height: 62px;
color: #334155;
text-decoration: none;
font-weight: 800;
border-bottom: 3px solid transparent;
}
.detail-tabs a:hover {
color: #0f172a;
border-bottom-color: #2563eb;
}
.detail-layout {
display: grid;
grid-template-columns: minmax(0, 1.55fr) minmax(280px, 0.7fr);
gap: 30px;
max-width: 1240px;
margin: 0 auto;
padding: 28px 24px 72px;
}
.detail-main {
display: flex;
flex-direction: column;
gap: 22px;
}
.detail-panel,
.sidebar-card,
.course-not-found__card {
border: 1px solid #dbe4f0;
border-radius: 28px;
background: #ffffff;
box-shadow: 0 14px 34px rgba(15, 23, 42, 0.05);
}
.detail-panel {
padding: 28px;
}
.panel-header {
margin-bottom: 18px;
}
.panel-header--split {
display: flex;
align-items: end;
justify-content: space-between;
gap: 16px;
}
.panel-header__eyebrow,
.sidebar-card__eyebrow {
display: inline-flex;
margin-bottom: 10px;
color: #2563eb;
font-size: 1.2rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.panel-header h2 {
margin: 0;
font-size: 1.7rem;
line-height: 1.2;
}
.panel-count {
display: inline-flex;
padding: 8px 12px;
border-radius: 999px;
background: #eff6ff;
color: #1d4ed8;
font-size: 0.82rem;
font-weight: 800;
white-space: nowrap;
}
.detail-lead {
margin: 0 0 18px;
color: #475569;
line-height: 1.75;
}
.detail-facts {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
margin-bottom: 18px;
}
.detail-facts article,
.detail-box {
padding: 18px;
border: 1px solid #e5edf6;
border-radius: 22px;
background: #fbfdff;
}
.detail-facts span {
display: block;
margin-bottom: 8px;
color: #64748b;
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.detail-columns {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.detail-box h3,
.learning-card h3,
.module-card__content h3 {
margin: 0 0 10px;
font-size: 1.08rem;
}
.bullet-list,
.sidebar-list {
margin: 0;
padding-left: 18px;
color: #475569;
line-height: 1.75;
}
.bullet-list li + li,
.sidebar-list li + li {
margin-top: 8px;
}
.learning-grid {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
gap: 16px;
}
.learning-card {
padding: 20px;
border: 1px solid #e5edf6;
border-radius: 22px;
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
}
.learning-card p,
.module-card__content p {
margin: 0;
color: #64748b;
line-height: 1.65;
}
.module-list {
display: flex;
flex-direction: column;
gap: 14px;
}
.module-card {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 16px;
align-items: start;
padding: 20px;
border: 1px solid #e5edf6;
border-radius: 24px;
background: #ffffff;
}
.module-card__index {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 14px;
background: #eff6ff;
color: #1d4ed8;
font-weight: 800;
}
.module-card__duration {
display: inline-flex;
padding: 8px 11px;
border-radius: 999px;
background: #f1f5f9;
color: #334155;
font-size: 0.82rem;
font-weight: 800;
white-space: nowrap;
}
.sidebar-card {
position: sticky;
top: 92px;
padding: 24px;
}
.sidebar-list {
margin-bottom: 18px;
}
.sidebar-tags {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.sidebar-tags span {
display: inline-flex;
padding: 8px 12px;
border-radius: 999px;
background: #eff6ff;
color: #1d4ed8;
font-size: 0.84rem;
font-weight: 700;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 50px;
border: 0;
border-radius: 18px;
background: #1d4ed8;
color: #ffffff;
text-decoration: none;
font-size: 0.96rem;
font-weight: 800;
cursor: pointer;
}
.button + .button {
margin-top: 12px;
}
.button--secondary {
background: #eff6ff;
color: #1d4ed8;
}
.course-not-found {
min-height: 100vh;
display: grid;
place-items: center;
padding: 24px;
}
.course-not-found__card {
max-width: 520px;
padding: 32px;
text-align: center;
}
.course-not-found__card h1 {
margin: 0 0 12px;
}
.course-not-found__card p {
margin: 0 0 20px;
color: #64748b;
}
@media (max-width: 1024px) {
.course-intro__inner,
.detail-layout {
grid-template-columns: 1fr;
}
.sidebar-card {
position: static;
}
}
@media (max-width: 768px) {
.course-intro__inner {
padding: 40px 20px 28px;
}
.detail-tabs {
gap: 18px;
overflow-x: auto;
padding: 0 20px;
}
.detail-layout {
padding: 22px 20px 56px;
}
.intro-stats,
.detail-facts,
.detail-columns,
.learning-grid {
grid-template-columns: 1fr;
}
.detail-panel,
.sidebar-card,
.course-not-found__card {
border-radius: 22px;
}
.detail-panel {
padding: 22px;
}
.panel-header--split,
.module-card {
grid-template-columns: 1fr;
}
.module-card__duration {
justify-self: start;
}
}
@@ -0,0 +1,121 @@
<ng-container *ngIf="course() as course; else notFound">
<div class="course-detail-page">
<section class="course-intro">
<div class="course-intro__inner">
<div class="course-intro__main">
<a routerLink="/" class="back-link">Voltar ao menu das formações</a>
<h1>{{ course.title }}</h1>
<p class="intro-summary">{{ course.summary }}</p>
<section id="overview" class="detail-panel">
<div class="panel-header">
<span class="panel-header__eyebrow">Informação geral da formação</span>
<p class="detail-lead">{{ course.description }}</p>
</div>
<div class="detail-facts">
<article>
<span>Idioma</span>
<strong>{{ course.language }}</strong>
</article>
<article>
<span>Formato</span>
<strong>{{ course.format }}</strong>
</article>
<article class="intro-stat">
<span>Duração</span>
<strong>{{ course.duration }}</strong>
</article>
<article class="intro-stat">
<span>Modú los</span>
<strong>{{ course.modules.length }}</strong>
</article>
<div class="detail-box">
<span>Pré-requisitos</span>
<ul class="bullet-list">
<li *ngFor="let item of course.prerequisites">{{ item }}</li>
</ul>
</div>
<div class="detail-box">
<span>Público-Alvo</span>
<ul class="bullet-list">
<li *ngFor="let item of course.audience">{{ item }}</li>
</ul>
</div>
</div>
</section>
</div>
<aside class="enroll-card">
<div class="enroll-card__media">
<img
[src]="course.image"
[alt]="course.title"
/>
</div>
<div class="enroll-card__body">
<h2>Começar esta formação</h2>
<p>{{ course.description }}</p>
<a [routerLink]="['/cursos', course.slug, 'percurso']" class="button">Iniciar formação</a>
<a routerLink="/" class="button button--secondary">Voltar ao menu das formações</a>
</div>
</aside>
</div>
</section>
<main class="detail-layout">
<section class="detail-main">
<section id="structure" class="detail-panel">
<div class="panel-header panel-header--split">
<div>
<span class="panel-header__eyebrow">Estrutura da formação</span>
</div>
<span class="panel-count">{{ course.modules.length }} lições</span>
</div>
<div class="module-list">
<article
*ngFor="let module of course.modules; let index = index"
class="module-card"
>
<div class="module-card__index">{{ index + 1 }}</div>
<div class="module-card__content">
<h3>{{ module.title }}</h3>
<p>{{ module.description }}</p>
</div>
<span class="module-card__duration">{{ module.duration }}</span>
</article>
</div>
</section>
</section>
<aside class="detail-sidebar">
<div class="sidebar-card">
<span class="sidebar-card__eyebrow">Competências da Formação</span>
<div class="learning-grid">
<article *ngFor="let item of course.objectives" class="topic-card">
<h3>{{ item }}</h3>
</article>
</div>
</div>
</aside>
</main>
</div>
</ng-container>
<ng-template #notFound>
<div class="course-not-found">
<div class="course-not-found__card">
<h1>Formação não encontrada</h1>
<p>A formação selecionada não está disponível neste momento.</p>
<a routerLink="/" class="button">Voltar aos cursos</a>
</div>
</div>
</ng-template>
@@ -0,0 +1,22 @@
import { CommonModule } from '@angular/common';
import { Component, computed, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { getCourseBySlug } from '../data/courses';
@Component({
selector: 'app-course-detail-page',
standalone: true,
imports: [CommonModule, RouterLink],
templateUrl: './course-detail-page.html',
styleUrl: './course-detail-page.css'
})
export class CourseDetailPageComponent {
private readonly route = inject(ActivatedRoute);
private readonly paramMap = toSignal(this.route.paramMap);
readonly course = computed(() => {
const slug = this.paramMap()?.get('slug') ?? null;
return getCourseBySlug(slug);
});
}
@@ -0,0 +1,740 @@
:host {
display: block;
min-height: 100vh;
background: #f8fafc;
color: #0f172a;
}
* {
box-sizing: border-box;
}
.learning-page {
min-height: 100vh;
}
.learning-header {
border-bottom: 1px solid #e2e8f0;
background:
radial-gradient(circle at top left, rgba(37, 99, 235, 0.12), transparent 28%),
linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
}
.learning-header__inner {
display: flex;
align-items: end;
justify-content: space-between;
gap: 24px;
max-width: 1280px;
margin: 0 auto;
padding: 40px 24px 28px;
}
.back-link {
display: inline-flex;
margin-bottom: 18px;
color: #2563eb;
text-decoration: none;
font-weight: 700;
}
.course-kicker {
margin: 0 0 10px;
color: #2563eb;
font-size: 0.82rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.learning-header h1 {
margin: 0;
font-size: clamp(2.2rem, 4vw, 3.6rem);
line-height: 1.06;
letter-spacing: -0.03em;
}
.progress-card {
min-width: 260px;
padding: 18px 20px;
border: 1px solid #dbe4f0;
border-radius: 24px;
background: #ffffff;
box-shadow: 0 16px 30px rgba(15, 23, 42, 0.05);
}
.progress-card__label {
display: block;
margin-bottom: 8px;
color: #64748b;
font-size: 0.8rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.progress-card strong {
display: block;
margin-bottom: 14px;
font-size: 1rem;
}
.progress-bar {
height: 8px;
border-radius: 999px;
background: #e2e8f0;
overflow: hidden;
}
.progress-bar span {
display: block;
width: 24%;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #1d4ed8 0%, #3b82f6 100%);
}
.learning-layout {
display: grid;
grid-template-columns: 320px minmax(0, 1fr);
gap: 28px;
max-width: 1280px;
margin: 0 auto;
padding: 28px 24px 64px;
}
.learning-sidebar {
position: relative;
}
.sidebar-panel,
.content-panel,
.course-not-found__card {
border: 1px solid #dbe4f0;
border-radius: 28px;
background: #ffffff;
box-shadow: 0 14px 34px rgba(15, 23, 42, 0.05);
}
.sidebar-panel {
position: sticky;
top: 24px;
padding: 22px;
}
.sidebar-panel__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 18px;
}
.sidebar-panel__eyebrow {
color: #2563eb;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.module-nav {
display: flex;
flex-direction: column;
gap: 12px;
}
.module-nav__item {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
padding: 0;
border: 1px solid #e5edf6;
border-radius: 20px;
background: #f8fafc;
}
.module-nav__trigger {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 12px;
width: 100%;
padding: 14px;
border: 0;
border-radius: inherit;
background: transparent;
text-align: left;
cursor: pointer;
}
.module-nav__item--active {
border-color: #bfdbfe;
background: #eff6ff;
}
.module-nav__index {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: 12px;
background: #ffffff;
color: #1d4ed8;
font-weight: 800;
}
.module-nav__content h3 {
margin: 0 0 6px;
font-size: 0.98rem;
}
.module-nav__content p {
margin: 0;
color: #64748b;
font-size: 0.9rem;
}
.learning-main {
display: flex;
flex-direction: column;
gap: 22px;
}
.content-panel {
padding: 28px;
}
.content-panel__header {
display: flex;
align-items: end;
justify-content: space-between;
gap: 18px;
margin-bottom: 18px;
}
.content-panel__eyebrow {
display: inline-flex;
margin-bottom: 10px;
color: #2563eb;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.content-panel__header h2 {
margin: 0;
font-size: 1.8rem;
line-height: 1.15;
}
.lesson-duration {
display: inline-flex;
padding: 8px 12px;
border-radius: 999px;
background: #eff6ff;
color: #1d4ed8;
font-size: 0.84rem;
font-weight: 800;
white-space: nowrap;
}
.content-intro {
margin: 0 0 18px;
color: #475569;
line-height: 1.75;
white-space: pre-line;
text-align: justify
}
.resource-download-card {
display: flex;
align-items: center;
gap: 16px;
margin: 0 0 22px;
padding: 18px 20px;
border: 1px solid #bfdbfe;
border-radius: 22px;
background: linear-gradient(135deg, #eff6ff 0%, #ffffff 100%);
color: inherit;
text-decoration: none;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.resource-download-card:hover {
transform: translateY(-1px);
border-color: #60a5fa;
box-shadow: 0 16px 32px rgba(37, 99, 235, 0.12);
}
.resource-download-card__icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 56px;
width: 56px;
height: 56px;
border-radius: 18px;
background: #dbeafe;
color: #1d4ed8;
}
.resource-download-card__icon svg {
width: 28px;
height: 28px;
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 1.8;
}
.resource-download-card__content {
display: flex;
flex: 1;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.resource-download-card__eyebrow {
color: #2563eb;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.resource-download-card__content strong {
font-size: 1rem;
}
.resource-download-card__content span:last-child {
color: #64748b;
line-height: 1.5;
}
.resource-download-card__action {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 42px;
padding: 0 16px;
border-radius: 999px;
background: #1d4ed8;
color: #ffffff;
font-size: 0.88rem;
font-weight: 800;
white-space: nowrap;
}
.course-topics {
margin: 0 0 18px;
color: #475569;
line-height: 1.75;
text-align: justify;
padding-left: 20px;
}
.course-topics li {
margin-bottom: 6px;
}
.course-topics li::before {
content: "✓";
color: #475569;
margin: 0 0 18px;
}
.content-icons {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 22px;
}
.content-icons__item {
width: auto;
height: 26px;
}
.lesson-section + .lesson-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #e5edf6;
}
.module-sequence {
display: flex;
flex-direction: column;
gap: 18px;
}
.learning-module {
scroll-margin-top: 28px;
padding: 20px;
border: 1px solid #e5edf6;
border-radius: 24px;
background: #ffffff;
}
.learning-module__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.module-description {
margin: 0 0 14px;
color: #475569;
line-height: 1.7;
}
.lesson-section h3,
.question-card h3 {
margin: 0 0 10px;
font-size: 1.16rem;
}
.lesson-section p,
.question-note {
margin: 0;
color: #64748b;
line-height: 1.75;
text-align: justify;
white-space: pre-line;
}
.lesson-section p + p {
margin-top: 12px;
}
.lesson-image {
display: block;
width: min(100%, 860px);
margin: 18px auto;
border-radius: 18px;
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.12);
cursor: zoom-in;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.lesson-image:hover {
transform: scale(1.01);
box-shadow: 0 22px 48px rgba(15, 23, 42, 0.16);
}
.presentation-viewer {
margin-top: 24px;
padding: 22px;
border: 1px solid #dbe4f0;
border-radius: 24px;
background: linear-gradient(180deg, #eff6ff 0%, #ffffff 100%);
}
.presentation-viewer__header {
display: flex;
align-items: end;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
}
.presentation-viewer__header h3 {
margin: 0;
font-size: 1.12rem;
}
.presentation-viewer__counter,
.image-lightbox__counter {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 40px;
padding: 0 14px;
border-radius: 999px;
background: #dbeafe;
color: #1d4ed8;
font-size: 0.82rem;
font-weight: 800;
white-space: nowrap;
}
.presentation-viewer__stage {
overflow: hidden;
border-radius: 20px;
background: #0f172a;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
.presentation-viewer__slide {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
object-fit: contain;
background: #0f172a;
cursor: zoom-in;
}
.presentation-viewer__actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 16px;
}
.presentation-viewer__button {
min-height: 44px;
padding: 0 18px;
border: 0;
border-radius: 14px;
background: #1d4ed8;
color: #ffffff;
font-size: 0.9rem;
font-weight: 800;
cursor: pointer;
}
.presentation-viewer__button:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.image-lightbox {
position: fixed;
inset: 0;
z-index: 1000;
display: grid;
place-items: center;
padding: 24px;
background: rgba(15, 23, 42, 0.82);
backdrop-filter: blur(6px);
}
.image-lightbox__dialog {
position: relative;
max-width: min(1200px, 100%);
max-height: 100%;
}
.image-lightbox__toolbar {
position: absolute;
top: -12px;
right: -12px;
z-index: 1;
display: flex;
align-items: center;
gap: 8px;
}
.image-lightbox__action,
.image-lightbox__close {
min-width: 48px;
min-height: 40px;
padding: 0 14px;
border: 0;
border-radius: 999px;
background: #ffffff;
color: #0f172a;
font-size: 0.86rem;
font-weight: 800;
cursor: pointer;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.2);
}
.image-lightbox__close {
min-width: 96px;
}
.image-lightbox__viewport {
overflow: auto;
max-width: min(1200px, calc(100vw - 48px));
max-height: calc(100vh - 48px);
border-radius: 20px;
cursor: zoom-in;
}
.image-lightbox__image {
display: block;
max-width: min(1200px, calc(100vw - 48px));
max-height: calc(100vh - 48px);
border-radius: 20px;
box-shadow: 0 28px 70px rgba(15, 23, 42, 0.35);
transition: transform 0.18s ease;
user-select: none;
}
.question-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.question-card {
padding: 20px;
border: 1px solid #e5edf6;
border-radius: 24px;
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
}
.question-card__number {
display: inline-flex;
margin-bottom: 12px;
color: #2563eb;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.question-options {
display: grid;
gap: 10px;
margin: 16px 0 14px;
}
.question-option {
min-height: 50px;
border: 1px solid #dbe4f0;
border-radius: 16px;
background: #ffffff;
color: #0f172a;
font-size: 0.95rem;
font-weight: 700;
text-align: left;
padding: 0 16px;
cursor: pointer;
}
.question-option:hover {
border-color: #93c5fd;
background: #f8fbff;
}
.question-option:disabled {
opacity: 1;
cursor: default;
}
.question-option.question-option--correct {
border-color: #16a34a;
background: #dcfce7;
color: #166534;
}
.question-option.question-option--incorrect {
border-color: #dc2626;
background: #fee2e2;
color: #991b1b;
}
.lesson-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 180px;
min-height: 50px;
padding: 0 20px;
border: 0;
border-radius: 18px;
background: #1d4ed8;
color: #ffffff;
text-decoration: none;
font-size: 0.96rem;
font-weight: 800;
cursor: pointer;
}
.button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.button--secondary {
background: #eff6ff;
color: #1d4ed8;
}
.course-not-found {
min-height: 100vh;
display: grid;
place-items: center;
padding: 24px;
}
.course-not-found__card {
max-width: 520px;
padding: 32px;
text-align: center;
}
.course-not-found__card h1 {
margin: 0 0 12px;
}
.course-not-found__card p {
margin: 0 0 20px;
color: #64748b;
}
@media (max-width: 1024px) {
.learning-header__inner,
.learning-layout {
grid-template-columns: 1fr;
display: grid;
}
.progress-card,
.sidebar-panel {
position: static;
}
}
@media (max-width: 768px) {
.learning-header__inner,
.learning-layout {
padding-left: 20px;
padding-right: 20px;
}
.learning-header__inner {
padding-top: 32px;
padding-bottom: 24px;
}
.content-panel,
.sidebar-panel,
.course-not-found__card {
border-radius: 22px;
}
.content-panel {
padding: 22px;
}
.content-panel__header,
.lesson-actions,
.presentation-viewer__header,
.presentation-viewer__actions {
flex-direction: column;
align-items: stretch;
}
.resource-download-card {
align-items: flex-start;
flex-direction: column;
}
.resource-download-card__action {
width: 100%;
}
}
@@ -0,0 +1,289 @@
<ng-container *ngIf="course() as course; else notFound">
<div class="learning-page">
<header class="learning-header">
<div class="learning-header__inner">
<div>
<a [routerLink]="['/cursos', course.slug]" class="back-link">Voltar aos detalhes do curso</a>
<p class="course-kicker" style="display:none">{{ course.category }}</p>
<h1>{{ course.title }}</h1>
</div>
</div>
</header>
<main class="learning-layout">
<aside class="learning-sidebar">
<div class="sidebar-panel">
<div class="sidebar-panel__top">
<span class="sidebar-panel__eyebrow">Conteúdo da Formação</span>
<strong>{{ course.modules.length }} módulos</strong>
</div>
<div class="module-nav">
<article
*ngFor="let module of course.modules; let index = index"
class="module-nav__item"
[class.module-nav__item--active]="index === currentModuleIndex()"
>
<button type="button" class="module-nav__trigger" (click)="scrollToModule(index)">
<span class="module-nav__index">{{ index + 1 }}</span>
<div class="module-nav__content">
<h3>{{ module.title }}</h3>
<p>{{ module.duration }}</p>
</div>
</button>
</article>
<div class="progress-card">
<span class="progress-card__label">Progressão atual</span>
<strong>{{ progressLabel() }}</strong>
<div class="progress-bar">
<span [style.width.%]="progressPercentage()"></span>
</div>
</div>
</div>
</div>
</aside>
<section class="learning-main">
<div class="content-panel">
<div class="content-panel__header">
<div>
<span class="content-panel__eyebrow">Percurso da Formação</span>
<h2>Contextualização</h2>
</div>
</div>
<p class="content-intro">{{ course.summary }}</p>
<ul class="course-topics">
<li *ngFor="let topic of course.topics">{{ topic }}</li>
</ul>
<section class="module-sequence">
<article
*ngFor="let item of modulesWithSections(); let index = index"
class="learning-module"
[attr.data-module-index]="index"
>
<div class="learning-module__header">
<span class="question-card__number">Módulo {{ index + 1 + " " + item.module.title }}</span>
<span class="lesson-duration">{{ item.module.duration }}</span>
</div>
<section class="lesson-section" *ngIf="item.section as section">
<p *ngFor="let paragraph of section.paragraphs" [innerHTML]="paragraph"> </p>
<ul class="course-topics" *ngIf="section.bullets">
<li *ngFor="let bullet of section.bullets">
{{ bullet }}
</li>
</ul>
<p *ngFor="let paragraph of section.paragraphs2" [innerHTML]="paragraph"> </p>
<img
*ngIf="section.imagem && section.imagem.trim() !== ''"
[src]="section.imagem"
class="lesson-image"
alt="Imagem da secao"
(click)="openZoomedImage(section.imagem)"
>
<p *ngFor="let paragraph of section.paragraphs3" [innerHTML]="paragraph"> </p>
<ul class="course-topics" *ngIf="section.bullets2">
<li *ngFor="let bullet of section.bullets2">
{{ bullet }}
</li>
</ul>
<p *ngFor="let paragraph of section.paragraphs4" [innerHTML]="paragraph"> </p>
<ul class="course-topics" *ngIf="section.bullets3">
<li *ngFor="let bullet of section.bullets3">
{{ bullet }}
</li>
</ul>
<p *ngFor="let paragraph of section.paragraphs5" [innerHTML]="paragraph"> </p>
<img
*ngIf="section.imagem2 && section.imagem2.trim() !== ''"
[src]="section.imagem2"
class="lesson-image"
alt="Imagem da secao"
(click)="openZoomedImage(section.imagem2)"
>
<p *ngFor="let paragraph of section.paragraphs6" [innerHTML]="paragraph"> </p>
<ul class="course-topics" *ngIf="section.bullets4">
<li *ngFor="let bullet of section.bullets4">
{{ bullet }}
</li>
</ul>
<img
*ngIf="section.imagem3 && section.imagem3.trim() !== ''"
[src]="section.imagem3"
class="lesson-image"
alt="Imagem da secao"
(click)="openZoomedImage(section.imagem3)"
>
<section class="presentation-viewer" *ngIf="section.presentationImages?.length">
<div class="presentation-viewer__header">
<div>
<span class="content-panel__eyebrow">Apresentação interativa</span>
</div>
<span class="presentation-viewer__counter">
Imagem {{ getPresentationImageIndex(index) + 1 }} / {{ section.presentationImages!.length }}
</span>
</div>
<div class="presentation-viewer__stage">
<img
[src]="getPresentationImage(index, section.presentationImages!)"
class="presentation-viewer__slide"
alt="Imagem da apresentacao"
(click)="openZoomedImageGallery(section.presentationImages!, getPresentationImageIndex(index))"
>
</div>
<div class="presentation-viewer__actions">
<button
type="button"
class="presentation-viewer__button"
[disabled]="getPresentationImageIndex(index) === 0"
(click)="previousPresentationImage(index)"
>
Anterior
</button>
<button
type="button"
class="presentation-viewer__button"
[disabled]="getPresentationImageIndex(index) >= section.presentationImages!.length - 1"
(click)="nextPresentationImage(index, section.presentationImages!)"
>
Seguinte
</button>
</div>
</section>
</section>
</article>
</section>
</div>
<a
*ngIf="course.pdfPath"
class="resource-download-card"
[href]="course.pdfPath"
[attr.download]="course.pdfDownloadName ?? ''"
[attr.aria-label]="'Descarregar ' + (course.pdfTitle ?? 'documento PDF')"
>
<span class="resource-download-card__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" focusable="false">
<path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" />
<path d="M14 3v5h5" />
<path d="M12 11v6" />
<path d="m9.5 14.5 2.5 2.5 2.5-2.5" />
</svg>
</span>
<span class="resource-download-card__content">
<strong>{{ course.pdfTitle ?? 'Documento PDF' }}</strong>
</span>
<span class="resource-download-card__action">Download</span>
</a>
<div class="content-panel">
<div class="content-panel__header">
<div>
<span class="content-panel__eyebrow">Validação de Conhecimentos</span>
</div>
</div>
<div class="question-list">
<article *ngFor="let question of visibleQuestions(); let index = index" class="question-card">
<span class="question-card__number">Pergunta {{ index + 1 }}</span>
<h3>{{ question.prompt }}</h3>
<div class="question-options">
<button
*ngFor="let option of question.options; let optionIndex = index"
type="button"
class="question-option"
[class.question-option--correct]="isCorrectOption(index, optionIndex)"
[class.question-option--incorrect]="isWrongSelectedOption(index, optionIndex)"
[disabled]="isQuestionAnswered(index)"
(click)="selectAnswer(index, optionIndex)"
>
{{ option }}
</button>
</div>
<p class="question-note">{{ question.note }}</p>
</article>
</div>
</div>
<div class="lesson-actions">
<a [routerLink]="['/cursos', course.slug]" class="button button--secondary">Voltar ao curso</a>
<button type="button" class="button" (click)="scrollToModule(0)">Voltar ao primeiro modulo</button>
</div>
</section>
</main>
</div>
</ng-container>
<div *ngIf="zoomedImageSrc() as zoomedImage" class="image-lightbox" (click)="closeZoomedImage()">
<div class="image-lightbox__dialog" (click)="$event.stopPropagation()">
<div class="image-lightbox__toolbar">
<button
*ngIf="zoomedImageSet().length > 1"
type="button"
class="image-lightbox__action"
[disabled]="zoomedImageIndex() === 0"
(click)="showPreviousZoomedImage()"
>
Anterior
</button>
<span *ngIf="zoomedImageSet().length > 1" class="image-lightbox__counter">
{{ zoomedImageIndex() + 1 }} / {{ zoomedImageSet().length }}
</span>
<button type="button" class="image-lightbox__action" (click)="zoomOutImage()">-</button>
<button type="button" class="image-lightbox__action" (click)="resetImageZoom()">100%</button>
<button type="button" class="image-lightbox__action" (click)="zoomInImage()">+</button>
<button
*ngIf="zoomedImageSet().length > 1"
type="button"
class="image-lightbox__action"
[disabled]="zoomedImageIndex() >= zoomedImageSet().length - 1"
(click)="showNextZoomedImage()"
>
Seguinte
</button>
<button type="button" class="image-lightbox__close" (click)="closeZoomedImage()">Fechar</button>
</div>
<div class="image-lightbox__viewport" (wheel)="handleImageZoomWheel($event, zoomedImageElement)">
<img
#zoomedImageElement
[src]="zoomedImage"
class="image-lightbox__image"
[style.transform]="'scale(' + zoomScale() + ')'"
[style.transform-origin]="zoomOrigin()"
alt="Imagem ampliada"
(click)="handleImageZoomClick($event, zoomedImageElement)"
>
</div>
</div>
</div>
<ng-template #notFound>
<div class="course-not-found">
<div class="course-not-found__card">
<h1>Percurso nao encontrado</h1>
<p>O percurso pedido nao esta disponivel neste momento.</p>
<a routerLink="/" class="button">Voltar aos cursos</a>
</div>
</div>
</ng-template>
@@ -0,0 +1,298 @@
import { CommonModule } from '@angular/common';
import { Component, HostListener, computed, effect, inject, signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { CourseQuestion, getCourseBySlug } from '../data/courses';
@Component({
selector: 'app-course-learning-page',
standalone: true,
imports: [CommonModule, RouterLink],
templateUrl: './course-learning-page.html',
styleUrl: './course-learning-page.css'
})
export class CourseLearningPageComponent {
private static readonly QUESTIONS_PER_SESSION = 5;
private static readonly MIN_ZOOM_SCALE = 1;
private static readonly MAX_ZOOM_SCALE = 3;
private static readonly ZOOM_STEP = 0.25;
private readonly route = inject(ActivatedRoute);
private readonly paramMap = toSignal(this.route.paramMap);
private readonly selectedAnswers = signal<Record<number, number>>({});
private readonly presentationImageIndexes = signal<Record<number, number>>({});
readonly visibleQuestions = signal<CourseQuestion[]>([]);
readonly currentModuleIndex = signal(0);
readonly zoomedImageSet = signal<string[]>([]);
readonly zoomedImageIndex = signal(0);
readonly zoomedImageSrc = computed(() => this.zoomedImageSet()[this.zoomedImageIndex()] ?? null);
readonly zoomScale = signal(1);
readonly zoomOrigin = signal('50% 50%');
private readonly resetAnswersOnCourseChange = effect(() => {
const slug = this.paramMap()?.get('slug') ?? null;
const course = getCourseBySlug(slug);
this.selectedAnswers.set({});
this.presentationImageIndexes.set({});
this.currentModuleIndex.set(0);
this.zoomedImageSet.set([]);
this.zoomedImageIndex.set(0);
this.zoomScale.set(1);
this.zoomOrigin.set('50% 50%');
this.visibleQuestions.set(this.pickRandomQuestions(course?.questions ?? []));
setTimeout(() => this.updateCurrentModuleFromScroll(), 0);
});
readonly course = computed(() => {
const slug = this.paramMap()?.get('slug') ?? null;
return getCourseBySlug(slug);
});
readonly modulesWithSections = computed(() => {
const course = this.course();
if (!course) {
return [];
}
const fallbackSection = course.lessonSections[course.lessonSections.length - 1] ?? null;
return course.modules.map((module, index) => ({
module,
section: course.lessonSections[index] ?? fallbackSection
}));
});
readonly progressLabel = computed(() => {
const course = this.course();
if (!course) {
return '0% complete';
}
return `${this.currentModuleIndex() + 1} of ${course.modules.length} módulos`;
});
readonly progressPercentage = computed(() => {
const course = this.course();
if (!course || course.modules.length === 0) {
return 0;
}
return ((this.currentModuleIndex() + 1) / course.modules.length) * 100;
});
scrollToModule(index: number): void {
const section = document.querySelector<HTMLElement>(`[data-module-index="${index}"]`);
if (!section) {
return;
}
section.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
@HostListener('window:scroll')
onWindowScroll(): void {
this.updateCurrentModuleFromScroll();
}
@HostListener('window:keydown.escape')
onEscapeKey(): void {
this.closeZoomedImage();
}
@HostListener('window:keydown.arrowleft')
onArrowLeftKey(): void {
this.showPreviousZoomedImage();
}
@HostListener('window:keydown.arrowright')
onArrowRightKey(): void {
this.showNextZoomedImage();
}
private updateCurrentModuleFromScroll(): void {
const sections = Array.from(document.querySelectorAll<HTMLElement>('[data-module-index]'));
if (sections.length === 0) {
return;
}
const scrollProbe = window.scrollY + window.innerHeight * 0.35;
let activeIndex = 0;
sections.forEach((section, index) => {
if (section.offsetTop <= scrollProbe) {
activeIndex = index;
}
});
if (activeIndex !== this.currentModuleIndex()) {
this.currentModuleIndex.set(activeIndex);
}
}
selectAnswer(questionIndex: number, optionIndex: number): void {
const currentAnswers = this.selectedAnswers();
if (currentAnswers[questionIndex] !== undefined) {
return;
}
this.selectedAnswers.set({
...currentAnswers,
[questionIndex]: optionIndex
});
}
isQuestionAnswered(questionIndex: number): boolean {
return this.selectedAnswers()[questionIndex] !== undefined;
}
isCorrectOption(questionIndex: number, optionIndex: number): boolean {
const question = this.visibleQuestions()[questionIndex];
if (!question || !this.isQuestionAnswered(questionIndex)) {
return false;
}
return question.correctOptionIndex === optionIndex;
}
isWrongSelectedOption(questionIndex: number, optionIndex: number): boolean {
const selectedOption = this.selectedAnswers()[questionIndex];
if (selectedOption === undefined) {
return false;
}
const question = this.visibleQuestions()[questionIndex];
if (!question) {
return false;
}
const correctOption = question.correctOptionIndex;
return selectedOption === optionIndex && selectedOption !== correctOption;
}
openZoomedImage(imageSrc: string): void {
this.openZoomedImageGallery([imageSrc], 0);
}
openZoomedImageGallery(imageSet: string[], imageIndex: number): void {
this.zoomedImageSet.set(imageSet);
this.zoomedImageIndex.set(imageIndex);
this.zoomScale.set(1);
this.zoomOrigin.set('50% 50%');
}
closeZoomedImage(): void {
this.zoomedImageSet.set([]);
this.zoomedImageIndex.set(0);
this.zoomScale.set(1);
this.zoomOrigin.set('50% 50%');
}
showPreviousZoomedImage(): void {
const imageSet = this.zoomedImageSet();
const imageIndex = this.zoomedImageIndex();
if (imageSet.length <= 1 || imageIndex === 0) {
return;
}
this.zoomedImageIndex.set(imageIndex - 1);
this.resetImageZoom();
}
showNextZoomedImage(): void {
const imageSet = this.zoomedImageSet();
const imageIndex = this.zoomedImageIndex();
if (imageSet.length <= 1 || imageIndex >= imageSet.length - 1) {
return;
}
this.zoomedImageIndex.set(imageIndex + 1);
this.resetImageZoom();
}
previousPresentationImage(moduleIndex: number): void {
const currentImage = this.getPresentationImageIndex(moduleIndex);
if (currentImage === 0) {
return;
}
this.presentationImageIndexes.update((indexes) => ({
...indexes,
[moduleIndex]: currentImage - 1
}));
}
nextPresentationImage(moduleIndex: number, images: string[]): void {
const currentImage = this.getPresentationImageIndex(moduleIndex);
if (currentImage >= images.length - 1) {
return;
}
this.presentationImageIndexes.update((indexes) => ({
...indexes,
[moduleIndex]: currentImage + 1
}));
}
getPresentationImage(moduleIndex: number, images: string[]): string {
return images[this.getPresentationImageIndex(moduleIndex)] ?? images[0] ?? '';
}
getPresentationImageIndex(moduleIndex: number): number {
return this.presentationImageIndexes()[moduleIndex] ?? 0;
}
zoomInImage(): void {
this.zoomScale.update((scale) => Math.min(scale + CourseLearningPageComponent.ZOOM_STEP, CourseLearningPageComponent.MAX_ZOOM_SCALE));
}
zoomOutImage(): void {
this.zoomScale.update((scale) => Math.max(scale - CourseLearningPageComponent.ZOOM_STEP, CourseLearningPageComponent.MIN_ZOOM_SCALE));
}
resetImageZoom(): void {
this.zoomScale.set(1);
this.zoomOrigin.set('50% 50%');
}
handleImageZoomWheel(event: WheelEvent, imageElement: HTMLImageElement): void {
event.preventDefault();
this.updateZoomOriginFromPointer(event, imageElement);
if (event.deltaY < 0) {
this.zoomInImage();
return;
}
this.zoomOutImage();
}
handleImageZoomClick(event: MouseEvent, imageElement: HTMLImageElement): void {
this.updateZoomOriginFromPointer(event, imageElement);
this.zoomInImage();
}
private updateZoomOriginFromPointer(event: MouseEvent | WheelEvent, imageElement: HTMLImageElement): void {
const bounds = imageElement.getBoundingClientRect();
if (bounds.width === 0 || bounds.height === 0) {
return;
}
const relativeX = ((event.clientX - bounds.left) / bounds.width) * 100;
const relativeY = ((event.clientY - bounds.top) / bounds.height) * 100;
const clampedX = Math.min(Math.max(relativeX, 0), 100);
const clampedY = Math.min(Math.max(relativeY, 0), 100);
this.zoomOrigin.set(`${clampedX}% ${clampedY}%`);
}
private pickRandomQuestions(questions: CourseQuestion[]): CourseQuestion[] {
const shuffledQuestions = [...questions];
for (let index = shuffledQuestions.length - 1; index > 0; index -= 1) {
const randomIndex = Math.floor(Math.random() * (index + 1));
[shuffledQuestions[index], shuffledQuestions[randomIndex]] = [shuffledQuestions[randomIndex], shuffledQuestions[index]];
}
return shuffledQuestions.slice(0, CourseLearningPageComponent.QUESTIONS_PER_SESSION);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,57 @@
.hero-logo-badge {
position: absolute;
top: 24px;
right: 24px;
z-index: 1;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 16px;
border: 1px solid rgba(191, 219, 254, 0.55);
border-radius: 999px;
background: rgba(239, 246, 255, 0.88);
backdrop-filter: blur(8px);
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.12);
}
.hero-logo {
width: clamp(120px, 16vw, 190px);
height: auto;
display: block;
object-fit: contain;
}
.course-card__badge {
position: absolute;
top: 16px;
left: 16px;
z-index: 1;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
max-width: calc(100% - 32px);
overflow-x: auto;
}
.course-card__badge-image {
width: auto;
max-width: 88px;
flex: 0 0 auto;
height: 24px;
display: block;
object-fit: contain;
}
@media (max-width: 768px) {
.hero-logo-badge {
top: 16px;
right: 16px;
padding: 8px 14px;
}
.hero-logo {
width: 120px;
}
}
@@ -0,0 +1,98 @@
<div class="min-h-screen bg-slate-50 text-slate-900">
<section class="relative overflow-hidden bg-slate-950">
<div class="absolute inset-0 bg-gradient-to-r from-indigo-700/20 via-cyan-500/10 to-purple-700/20"></div>
<div class="hero-logo-badge">
<a href="https://www.metacase.pt" target="_blank" rel="noopener">
<img
src="/logo-metacase-cor.svg"
alt="Metacase"
class="hero-logo"
/>
</a>
</div>
<div class="relative mx-auto max-w-7xl px-6 py-20 lg:px-8">
<div class="max-w-3xl">
<span class="inline-flex rounded-full border border-white/20 bg-white/10 px-4 py-1 text-sm font-medium text-white">
Learning Hub
</span>
<h1 class="mt-6 text-4xl font-bold tracking-tight text-white sm:text-5xl lg:text-6xl">
Formações de TargetOne
</h1>
<p class="mt-6 text-lg leading-8 text-slate-300">
Formações práticas e atualizadas para a utilização da plataforma TargetOne <br>Aprendizagem orientada para a ferramenta com formações teóricas e práticas
</p>
</div>
</div>
</section>
<section class="mx-auto max-w-7xl px-6 py-10 lg:px-8">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h2 class="text-3xl font-bold tracking-tight text-slate-900">Formações disponíveis</h2>
<p class="mt-2 text-slate-600">
Atualmente encontram-se disponíveis {{ courses.length }} {{ courses.length === 1 ? 'formação' : 'formações' }} em destaque.
</p>
</div>
<div class="inline-flex w-fit items-center rounded-2xl border border-slate-200 bg-white px-4 py-3 shadow-sm">
<span class="text-sm font-semibold text-slate-700">
{{ courses.length }} {{ courses.length === 1 ? 'curso' : 'cursos' }}
</span>
</div>
</div>
</section>
<section class="mx-auto max-w-7xl px-6 pb-16 lg:px-8">
<div class="grid gap-8 md:grid-cols-2 xl:grid-cols-3">
<article
*ngFor="let course of courses"
class="group overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-sm transition duration-300 hover:-translate-y-1 hover:shadow-xl"
>
<div class="relative h-56 overflow-hidden">
<img
[src]="course.image"
[alt]="course.title"
class="h-full w-full object-cover transition duration-500 group-hover:scale-105"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent"></div>
<div class="course-card__badge">
<img
*ngFor="let icon of course.categoryIcons"
[alt]="course.category"
class="course-card__badge-image"
/>
</div>
</div>
<div class="p-6">
<h3 class="mt-2 text-xl font-bold text-slate-900">
{{ course.title }}
</h3>
<p class="mt-3 text-sm leading-6 text-slate-600">
{{ course.description }}
</p>
<div class="mt-5 flex flex-wrap gap-2">
<span class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-700">
{{ course.duration }}
</span>
</div>
<a
[routerLink]="['/cursos', course.slug]"
class="mt-6 block w-full rounded-2xl bg-gradient-to-r from-indigo-600 to-purple-600 px-4 py-3 text-center text-sm font-semibold text-white transition hover:opacity-95"
>
Entrar
</a>
</div>
</article>
</div>
</section>
</div>
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { Formacaometacase } from './formacaometacase';
describe('Formacaometacase', () => {
let component: Formacaometacase;
let fixture: ComponentFixture<Formacaometacase>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Formacaometacase],
providers: [provideRouter([])]
}).compileComponents();
fixture = TestBed.createComponent(Formacaometacase);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,15 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import { COURSES, type Course } from '../data/courses';
@Component({
selector: 'app-formacaometacase',
standalone: true,
imports: [CommonModule, RouterLink],
templateUrl: './formacaometacase.html',
styleUrl: './formacaometacase.css'
})
export class Formacaometacase {
readonly courses: Course[] = COURSES;
}