feat: Refactor technical profile component to enhance user experience and add digital rain effect
- Replaced static HTML content with dynamic rendering using Angular's template syntax. - Introduced a new section for "About Me" with ASCII art and descriptive paragraphs. - Implemented a digital rain effect on a canvas element, enhancing visual appeal. - Added responsive design considerations and improved accessibility features. - Updated TypeScript component to manage new functionalities and lifecycle hooks. - Included a new text asset for ASCII art representation. - Created a new skill review guideline for code quality assessment.
This commit is contained in:
parent
2192734b2f
commit
5f143d5fac
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
name: ponytail-review
|
||||
description: >
|
||||
Code review focused exclusively on over-engineering. Finds what to delete:
|
||||
reinvented standard library, unneeded dependencies, speculative abstractions,
|
||||
and dead flexibility.
|
||||
---
|
||||
|
||||
# Ponytail Review
|
||||
|
||||
Review the current diff for unnecessary complexity only. Do not review
|
||||
correctness, security, performance, or style.
|
||||
|
||||
Report one line per finding using:
|
||||
|
||||
`<file>:L<line>: <tag> <what to cut>. <replacement>.`
|
||||
|
||||
Allowed tags:
|
||||
|
||||
- `delete`: dead code or speculative feature; replacement is nothing.
|
||||
- `stdlib`: hand-rolled functionality provided by the standard library.
|
||||
- `native`: code or dependency replaceable by a platform feature.
|
||||
- `yagni`: abstraction with one implementation or one caller.
|
||||
- `shrink`: same logic expressed with fewer lines.
|
||||
|
||||
End with `net: -<N> lines possible.` If there is nothing to cut, report
|
||||
`Lean already. Ship.`
|
||||
|
||||
The review does not apply fixes. A smoke test is the minimum acceptable
|
||||
validation and must not be flagged for deletion.
|
||||
|
|
@ -50,7 +50,11 @@ export const EN_TRANSLATIONS: TranslationDictionary = {
|
|||
p3RightBadge2: 'Live .NET API',
|
||||
|
||||
// Technical Profile
|
||||
tpEyebrow: '01 // COMPUTER ENGINEERING & EXPERTISE',
|
||||
tpAboutGreeting: 'Hello, I am',
|
||||
tpAboutName: 'Bryan Baño',
|
||||
tpAboutDescription: 'I am a software engineer focused on building thoughtful, accessible, and technically solid digital experiences. I combine clean architecture, modern frontend development, and scalable backend solutions. I work with .NET, Angular, Signals, REST APIs, and relational databases.\n\nMy approach combines engineering and design. First I understand the problem, then simplify the experience, and finally build a maintainable solution. I prioritize performance, accessibility, separation of concerns, and code quality.\n\nI also explore interfaces inspired by Unix terminals, retro systems, Cyber-Zen aesthetics, and new ways to present technical information without sacrificing readability. Every project must be functional, expressive, and easy to maintain.\n\nI currently develop solutions that connect frontend, backend, visual design, and digital interaction. I am interested in creating products that not only work reliably, but also communicate a clear and memorable identity.',
|
||||
tpAboutAria: 'About Bryan Baño',
|
||||
tpEyebrow: 'INGENIERÍA EN COMPUTACIÓN & EXPERTISE',
|
||||
tpTitle: 'Technical Skills & Areas of Interest',
|
||||
tpLeadStudent: '9th semester Computer Engineering student at',
|
||||
tpLeadSpecialization: 'Specialized in clean architectures with',
|
||||
|
|
|
|||
|
|
@ -50,6 +50,10 @@ export const ES_TRANSLATIONS: TranslationDictionary = {
|
|||
p3RightBadge2: 'Live .NET API',
|
||||
|
||||
// Technical Profile
|
||||
tpAboutGreeting: 'Hola, soy',
|
||||
tpAboutName: 'Bryan Baño',
|
||||
tpAboutDescription: 'Soy ingeniero de software enfocado en construir experiencias digitales claras, accesibles y técnicamente sólidas. Me interesa combinar arquitectura limpia, desarrollo frontend moderno y soluciones backend escalables. Trabajo con .NET, Angular, Signals, APIs REST y bases de datos relacionales.\n\nMi proceso combina ingeniería y diseño. Primero entiendo el problema, después simplifico la experiencia y finalmente construyo una solución mantenible. Priorizo el rendimiento, la accesibilidad, la separación de responsabilidades y la calidad del código.\n\nTambién exploro interfaces inspiradas en terminales Unix, sistemas retro, estética Cyber-Zen y nuevas formas de presentar información técnica sin sacrificar legibilidad. Cada proyecto debe ser funcional, expresivo y fácil de mantener.\n\nActualmente desarrollo soluciones que conectan frontend, backend, diseño visual e interacción digital. Me interesa crear productos que no solo funcionen correctamente, sino que también comuniquen una identidad clara y memorable.',
|
||||
tpAboutAria: 'Sobre Bryan Baño',
|
||||
tpEyebrow: 'INGENIERÍA EN COMPUTACIÓN & EXPERTISE',
|
||||
tpTitle: 'Habilidades Técnicas & Áreas de Interés',
|
||||
tpLeadStudent: 'Estudiante de 9no semestre de Ingeniería en Computación en la',
|
||||
|
|
|
|||
|
|
@ -50,6 +50,10 @@ export interface TranslationDictionary {
|
|||
readonly p3RightBadge2: string;
|
||||
|
||||
// Technical Profile
|
||||
readonly tpAboutGreeting: string;
|
||||
readonly tpAboutName: string;
|
||||
readonly tpAboutDescription: string;
|
||||
readonly tpAboutAria: string;
|
||||
readonly tpEyebrow: string;
|
||||
readonly tpTitle: string;
|
||||
readonly tpLeadStudent: string;
|
||||
|
|
|
|||
|
|
@ -140,6 +140,23 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.profile-module-card app-technical-profile {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.matrix-stage {
|
||||
width: 100%;
|
||||
height: clamp(64px, 10vh, 104px);
|
||||
background: #121517;
|
||||
}
|
||||
|
||||
.matrix-stage app-technical-profile {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ==============================================================================
|
||||
SECCIÓN DE PROYECTOS DESTACADOS (CASOS REALES A ANCHO COMPLETO)
|
||||
============================================================================== */
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
<div class="stage-grid">
|
||||
<!-- Slot 1: Perfil Técnico & Filosofía de Ingeniería -->
|
||||
<article class="module-card profile-module-card">
|
||||
<app-technical-profile></app-technical-profile>
|
||||
<app-technical-profile [showMatrix]="false"></app-technical-profile>
|
||||
</article>
|
||||
|
||||
<!-- Slot 2: Retro OS en Monitor CRT Antiguo (50% Horizontal) -->
|
||||
|
|
@ -38,6 +38,10 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="matrix-stage" aria-label="Lluvia de codigo Matrix">
|
||||
<app-technical-profile [showProfile]="false"></app-technical-profile>
|
||||
</section>
|
||||
|
||||
<!-- Sección de Proyectos Destacados (Casos Reales del CV a Ancho Completo) -->
|
||||
<section class="projects-stage">
|
||||
<div class="projects-stage-inner">
|
||||
|
|
|
|||
|
|
@ -7,11 +7,12 @@
|
|||
.profile-editorial-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-2) 0;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
|
|
@ -19,118 +20,208 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.about-me {
|
||||
display: flow-root;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.about-copy {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
/* Eyebrow */
|
||||
.editorial-eyebrow {
|
||||
display: block;
|
||||
margin: 0 0 var(--space-1) 0;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.75rem;
|
||||
font-size: clamp(0.85rem, 2cqw, 1.1rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
letter-spacing: 0.16em;
|
||||
color: var(--zen-accent-kintsugi);
|
||||
text-transform: uppercase;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.about-greeting {
|
||||
margin: 0;
|
||||
color: var(--zen-text-secondary);
|
||||
font-size: clamp(0.95rem, 1.8cqw, 1.125rem);
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
/* Título Principal H2 */
|
||||
.editorial-title {
|
||||
margin: 0 0 var(--space-4) 0;
|
||||
font-family: var(--font-sans);
|
||||
font-size: clamp(1.85rem, 3vw, 2.35rem);
|
||||
font-weight: 800;
|
||||
font-size: clamp(2rem, 5cqw, 3rem);
|
||||
font-weight: 900;
|
||||
color: #ffffff;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
/* Párrafo Lead */
|
||||
.editorial-lead {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.9375rem;
|
||||
.about-description {
|
||||
margin: 0 0 var(--space-4) 0;
|
||||
color: var(--zen-text-secondary);
|
||||
line-height: 1.6;
|
||||
font-size: clamp(0.875rem, 1.55cqw, 1rem);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.about-link {
|
||||
display: inline-block;
|
||||
margin-top: var(--space-2);
|
||||
color: var(--zen-text-gold);
|
||||
font-family: var(--font-sans);
|
||||
font-size: clamp(1rem, 2cqw, 1.25rem);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ascii-portrait {
|
||||
float: left;
|
||||
width: 58%;
|
||||
min-width: 0;
|
||||
margin: var(--space-2) 0 0 0;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
color: var(--zen-text-gold);
|
||||
font-family: var(--font-mono);
|
||||
font-size: clamp(1.8px, calc(58cqw / 108), 4.2px);
|
||||
line-height: 0.84;
|
||||
text-align: left;
|
||||
shape-outside: polygon(
|
||||
0% 0%,
|
||||
49.4% 0%,
|
||||
52.2% 4.3%,
|
||||
59.4% 8.6%,
|
||||
62.8% 16.1%,
|
||||
65.0% 32.3%,
|
||||
63.3% 36.6%,
|
||||
58.9% 39.8%,
|
||||
57.2% 44.1%,
|
||||
58.3% 49.5%,
|
||||
63.3% 53.8%,
|
||||
70.6% 57.0%,
|
||||
80.6% 60.2%,
|
||||
90.0% 63.4%,
|
||||
91.7% 66.7%,
|
||||
93.3% 73.1%,
|
||||
95.0% 82.8%,
|
||||
97.8% 92.5%,
|
||||
100% 100%,
|
||||
0% 100%
|
||||
);
|
||||
shape-margin: 20px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ascii-portrait pre {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
white-space: pre;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.editorial-lead strong {
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
.ascii-status {
|
||||
display: block;
|
||||
max-width: 120px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.65rem;
|
||||
line-height: 1.4;
|
||||
color: var(--zen-text-muted);
|
||||
}
|
||||
|
||||
/* Secciones de Contenido */
|
||||
.editorial-section {
|
||||
.hacker-screen {
|
||||
flex: 0 0 clamp(64px, 10vh, 104px);
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: none;
|
||||
background-color: transparent;
|
||||
font-family: var(--font-mono);
|
||||
isolation: isolate;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.hacker-rain {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
display: block;
|
||||
background: transparent;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.ascii-portrait {
|
||||
width: 56%;
|
||||
shape-margin: 16px;
|
||||
font-size: clamp(1.8px, calc(56cqw / 108), 3.6px);
|
||||
}
|
||||
|
||||
.editorial-eyebrow {
|
||||
font-size: clamp(0.8rem, 1.8cqw, 1rem);
|
||||
}
|
||||
|
||||
.editorial-title {
|
||||
font-size: clamp(1.8rem, 4.5cqw, 2.6rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.profile-editorial-content {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.about-me {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.section-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: var(--space-4) 0;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.section-bullet {
|
||||
color: var(--zen-accent-kintsugi);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
.about-copy {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.editorial-subtitle {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
letter-spacing: -0.01em;
|
||||
.editorial-eyebrow {
|
||||
font-size: clamp(0.75rem, 3.5vw, 0.95rem);
|
||||
}
|
||||
|
||||
/* Grid de Categorías Técnicas */
|
||||
.skills-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding-left: 14px;
|
||||
.editorial-title {
|
||||
font-size: clamp(1.6rem, 7vw, 2.2rem);
|
||||
}
|
||||
|
||||
.skill-category {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
color: var(--zen-text-secondary);
|
||||
.ascii-portrait {
|
||||
float: none;
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
margin: var(--space-6) auto 0 auto;
|
||||
shape-outside: none;
|
||||
font-size: clamp(1.6px, calc(95cqw / 108), 3.2px);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cat-label {
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 700;
|
||||
color: #7dcfff;
|
||||
font-size: 0.8125rem;
|
||||
margin-right: 6px;
|
||||
.ascii-portrait pre {
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.cat-techs {
|
||||
color: #c0caf5;
|
||||
}
|
||||
|
||||
/* Lista de Áreas de Interés */
|
||||
.interests-list {
|
||||
list-style: none;
|
||||
padding: 0 0 0 14px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.interest-item {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
color: var(--zen-text-secondary);
|
||||
}
|
||||
|
||||
.interest-key {
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 700;
|
||||
color: #bb9af7;
|
||||
font-size: 0.8125rem;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,74 +1,32 @@
|
|||
<div class="profile-editorial-content">
|
||||
<!-- Eyebrow en Dorado Kintsugi -->
|
||||
<span class="editorial-eyebrow">
|
||||
{{ i18n.t().tpEyebrow }}
|
||||
</span>
|
||||
@if (showProfile()) {
|
||||
<section class="about-me" [attr.aria-label]="i18n.t().tpAboutAria">
|
||||
<div class="about-copy">
|
||||
<span class="editorial-eyebrow">{{ i18n.t().tpEyebrow }}</span>
|
||||
<p class="about-greeting">{{ i18n.t().tpAboutGreeting }}</p>
|
||||
|
||||
<!-- Título Principal H2 -->
|
||||
<h2 class="editorial-title">
|
||||
{{ i18n.t().tpTitle }}
|
||||
</h2>
|
||||
<figure class="ascii-portrait">
|
||||
@if (asciiArt()) {
|
||||
<pre aria-label="Retrato ASCII de Bryan">{{ asciiArt() }}</pre>
|
||||
} @else if (asciiLoadError()) {
|
||||
<figcaption class="ascii-status" role="status">No se pudo cargar el retrato.</figcaption>
|
||||
} @else {
|
||||
<figcaption class="ascii-status" role="status">Cargando retrato...</figcaption>
|
||||
}
|
||||
</figure>
|
||||
|
||||
<!-- Breve Perfil Extraído del CV -->
|
||||
<p class="editorial-lead">
|
||||
{{ i18n.t().tpLeadStudent }} <strong>Escuela Politécnica Nacional (EPN)</strong>.
|
||||
{{ i18n.t().tpLeadSpecialization }} <strong>{{ i18n.t().tpLeadStack }}</strong>{{ i18n.t().tpLeadEnd }}
|
||||
</p>
|
||||
<h2 class="editorial-title">{{ i18n.t().tpAboutName }}</h2>
|
||||
@for (paragraph of aboutParagraphs(); track $index) {
|
||||
<p class="about-description">{{ paragraph }}</p>
|
||||
}
|
||||
<span class="about-link">About me <span aria-hidden="true">→</span></span>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- 1. Bloque: Habilidades Técnicas Principales -->
|
||||
<div class="editorial-section">
|
||||
<div class="section-title-row">
|
||||
<span class="section-bullet">▹</span>
|
||||
<h3 class="editorial-subtitle">{{ i18n.t().tpSkillsSubtitle }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="skills-grid">
|
||||
<!-- Backend -->
|
||||
<div class="skill-category">
|
||||
<strong class="cat-label">{{ i18n.t().tpCatBackend }}</strong>
|
||||
<span class="cat-techs">.NET 8/9, C#, Clean Architecture, CQRS (MediatR), FluentValidation, REST APIs.</span>
|
||||
</div>
|
||||
|
||||
<!-- Frontend -->
|
||||
<div class="skill-category">
|
||||
<strong class="cat-label">{{ i18n.t().tpCatFrontend }}</strong>
|
||||
<span class="cat-techs">Angular 16+, Signals reactivos, ChangeDetection OnPush, TypeScript, Canvas 2D.</span>
|
||||
</div>
|
||||
|
||||
<!-- Datos & Cloud -->
|
||||
<div class="skill-category">
|
||||
<strong class="cat-label">{{ i18n.t().tpCatData }}</strong>
|
||||
<span class="cat-techs">MySQL, PostgreSQL, SQLite, Docker, AWS (Academy Graduate), Git/GitHub CI/CD.</span>
|
||||
</div>
|
||||
|
||||
<!-- Ciberseguridad & Redes -->
|
||||
<div class="skill-category">
|
||||
<strong class="cat-label">{{ i18n.t().tpCatSecurity }}</strong>
|
||||
<span class="cat-techs">{{ i18n.t().tpCatSecurityDesc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Bloque: Áreas de Interés & Cultura Dev -->
|
||||
<div class="editorial-section">
|
||||
<div class="section-title-row">
|
||||
<span class="section-bullet">▹</span>
|
||||
<h3 class="editorial-subtitle">{{ i18n.t().tpInterestsSubtitle }}</h3>
|
||||
</div>
|
||||
|
||||
<ul class="interests-list">
|
||||
<li class="interest-item">
|
||||
<strong class="interest-key">{{ i18n.t().tpInterest1Key }}</strong>
|
||||
<span>{{ i18n.t().tpInterest1Desc }}</span>
|
||||
</li>
|
||||
<li class="interest-item">
|
||||
<strong class="interest-key">{{ i18n.t().tpInterest2Key }}</strong>
|
||||
<span>{{ i18n.t().tpInterest2Desc }}</span>
|
||||
</li>
|
||||
<li class="interest-item">
|
||||
<strong class="interest-key">{{ i18n.t().tpInterest3Key }}</strong>
|
||||
<span>{{ i18n.t().tpInterest3Desc }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@if (showMatrix()) {
|
||||
<section class="hacker-screen" aria-label="Lluvia de codigo Matrix">
|
||||
<canvas #rainCanvas class="hacker-rain" aria-hidden="true"></canvas>
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,32 @@
|
|||
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
ElementRef,
|
||||
inject,
|
||||
input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
signal,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { I18nService } from '../../../application/services/i18n.service';
|
||||
|
||||
interface DigitalRainColumn {
|
||||
x: number;
|
||||
y: number;
|
||||
speed: number;
|
||||
length: number;
|
||||
opacity: number;
|
||||
characters: string[];
|
||||
}
|
||||
|
||||
const BINARY_CHARACTERS = '01';
|
||||
const HEX_CHARACTERS = '0123456789ABCDEF';
|
||||
const CODE_CHARACTERS = '{}[]()<>&|/\\:;=+-_*#$%@?!~^';
|
||||
const TERMINAL_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
@Component({
|
||||
selector: 'app-technical-profile',
|
||||
standalone: true,
|
||||
|
|
@ -8,6 +34,235 @@ import { I18nService } from '../../../application/services/i18n.service';
|
|||
styleUrl: './technical-profile.component.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class TechnicalProfileComponent {
|
||||
export class TechnicalProfileComponent implements AfterViewInit, OnDestroy, OnInit {
|
||||
@ViewChild('rainCanvas') private readonly rainCanvas?: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
protected readonly i18n = inject(I18nService);
|
||||
readonly showProfile = input(true);
|
||||
readonly showMatrix = input(true);
|
||||
protected readonly asciiArt = signal('');
|
||||
protected readonly asciiLoadError = signal(false);
|
||||
protected readonly aboutParagraphs = computed(() =>
|
||||
this.i18n.t().tpAboutDescription.split('\n\n').filter((p) => p.trim().length > 0)
|
||||
);
|
||||
private readonly columns: DigitalRainColumn[] = [];
|
||||
private context?: CanvasRenderingContext2D;
|
||||
private animationFrameId?: number;
|
||||
private intersectionObserver?: IntersectionObserver;
|
||||
private resizeObserver?: ResizeObserver;
|
||||
private motionQuery?: MediaQueryList;
|
||||
private isVisible = true;
|
||||
private isReducedMotion = false;
|
||||
private lastFrameTime = 0;
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.showProfile()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadTextAsset('assets/portfolio.txt')
|
||||
.then((portrait) => this.asciiArt.set(portrait))
|
||||
.catch(() => this.asciiLoadError.set(true));
|
||||
}
|
||||
|
||||
private loadTextAsset(path: string): Promise<string> {
|
||||
return fetch(path).then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`ASCII asset request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.text();
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (!this.showMatrix() || !this.rainCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = this.rainCanvas.nativeElement;
|
||||
this.context = canvas.getContext('2d') ?? undefined;
|
||||
if (!this.context) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
this.isReducedMotion = this.motionQuery.matches;
|
||||
this.motionQuery.addEventListener('change', this.onMotionPreferenceChange);
|
||||
|
||||
this.resizeObserver = new ResizeObserver(() => this.resizeCanvas());
|
||||
this.resizeObserver.observe(canvas.parentElement ?? canvas);
|
||||
|
||||
this.intersectionObserver = new IntersectionObserver(([entry]) => {
|
||||
this.isVisible = entry.isIntersecting;
|
||||
if (this.isVisible && !this.isReducedMotion && this.animationFrameId === undefined) {
|
||||
this.lastFrameTime = 0;
|
||||
this.animationFrameId = requestAnimationFrame(this.animate);
|
||||
}
|
||||
}, { threshold: 0.05 });
|
||||
this.intersectionObserver.observe(canvas);
|
||||
|
||||
this.resizeCanvas();
|
||||
if (!this.isReducedMotion) {
|
||||
this.animationFrameId = requestAnimationFrame(this.animate);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.animationFrameId !== undefined) {
|
||||
cancelAnimationFrame(this.animationFrameId);
|
||||
}
|
||||
this.motionQuery?.removeEventListener('change', this.onMotionPreferenceChange);
|
||||
this.intersectionObserver?.disconnect();
|
||||
this.resizeObserver?.disconnect();
|
||||
}
|
||||
|
||||
private readonly onMotionPreferenceChange = (event: MediaQueryListEvent): void => {
|
||||
this.isReducedMotion = event.matches;
|
||||
if (this.isReducedMotion) {
|
||||
if (this.animationFrameId !== undefined) {
|
||||
cancelAnimationFrame(this.animationFrameId);
|
||||
this.animationFrameId = undefined;
|
||||
}
|
||||
this.draw();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isVisible && this.animationFrameId === undefined) {
|
||||
this.lastFrameTime = 0;
|
||||
this.animationFrameId = requestAnimationFrame(this.animate);
|
||||
}
|
||||
};
|
||||
|
||||
private readonly animate = (timestamp: number): void => {
|
||||
this.animationFrameId = undefined;
|
||||
if (!this.isVisible || this.isReducedMotion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elapsed = this.lastFrameTime === 0 ? 0 : Math.min(timestamp - this.lastFrameTime, 50);
|
||||
this.lastFrameTime = timestamp;
|
||||
this.update(elapsed / 1000);
|
||||
this.draw();
|
||||
this.animationFrameId = requestAnimationFrame(this.animate);
|
||||
};
|
||||
|
||||
private resizeCanvas(): void {
|
||||
if (!this.rainCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = this.rainCanvas.nativeElement;
|
||||
const bounds = canvas.getBoundingClientRect();
|
||||
const pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
|
||||
canvas.width = Math.max(1, Math.floor(bounds.width * pixelRatio));
|
||||
canvas.height = Math.max(1, Math.floor(bounds.height * pixelRatio));
|
||||
this.context?.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||
this.createColumns(bounds.width, bounds.height);
|
||||
this.draw();
|
||||
}
|
||||
|
||||
private createColumns(width: number, height: number): void {
|
||||
const fontSize = Math.max(14, Math.min(20, width / 58));
|
||||
const columnCount = Math.max(24, Math.floor(width / (fontSize * 0.72)));
|
||||
this.columns.length = 0;
|
||||
|
||||
for (let index = 0; index < columnCount; index += 1) {
|
||||
const length = this.randomInteger(14, 38);
|
||||
this.columns.push({
|
||||
x: (index / columnCount) * width + Math.random() * fontSize * 0.6,
|
||||
y: Math.random() * height * 1.4 - height,
|
||||
speed: this.randomInteger(150, 280),
|
||||
length,
|
||||
opacity: 0.16 + Math.random() * 0.68,
|
||||
characters: Array.from({ length }, () => this.randomCharacter())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private update(deltaSeconds: number): void {
|
||||
if (!this.rainCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const height = this.rainCanvas.nativeElement.clientHeight;
|
||||
const fontSize = Math.max(14, Math.min(20, this.rainCanvas.nativeElement.clientWidth / 58));
|
||||
|
||||
for (const column of this.columns) {
|
||||
column.y += column.speed * deltaSeconds;
|
||||
if (column.y - column.length * fontSize > height) {
|
||||
column.y = -Math.random() * height * 0.75 - column.length * fontSize;
|
||||
column.speed = this.randomInteger(150, 280);
|
||||
column.length = this.randomInteger(14, 38);
|
||||
column.opacity = 0.16 + Math.random() * 0.68;
|
||||
column.characters = Array.from({ length: column.length }, () => this.randomCharacter());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private draw(): void {
|
||||
if (!this.rainCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = this.rainCanvas.nativeElement;
|
||||
const context = this.context;
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
const width = canvas.clientWidth;
|
||||
const height = canvas.clientHeight;
|
||||
const fontSize = Math.max(14, Math.min(20, width / 58));
|
||||
context.clearRect(0, 0, width, height);
|
||||
context.font = `${fontSize}px ${getComputedStyle(canvas).fontFamily}`;
|
||||
context.textAlign = 'center';
|
||||
context.textBaseline = 'top';
|
||||
context.shadowBlur = 0;
|
||||
|
||||
for (const column of this.columns) {
|
||||
for (let index = 0; index < column.characters.length; index += 1) {
|
||||
const characterY = column.y + index * fontSize;
|
||||
if (characterY < -fontSize || characterY > height) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isHead = index === 0;
|
||||
const isHighlight = index === Math.floor(column.length * 0.35) && column.opacity > 0.5;
|
||||
const trailAlpha = column.opacity * (1 - index / (column.length * 1.15));
|
||||
const edgeFade = Math.min(1, Math.max(0, (height - characterY) / (fontSize * 5)));
|
||||
const alpha = Math.max(0, trailAlpha * edgeFade);
|
||||
if (alpha === 0) {
|
||||
continue;
|
||||
}
|
||||
context.fillStyle = isHead
|
||||
? `rgba(220, 255, 226, ${Math.min(1, alpha + 0.3)})`
|
||||
: isHighlight
|
||||
? `rgba(80, 255, 126, ${alpha})`
|
||||
: `rgba(18, 174, 78, ${alpha})`;
|
||||
if (isHead || isHighlight) {
|
||||
context.shadowColor = 'rgba(37, 255, 104, 0.8)';
|
||||
context.shadowBlur = isHead ? 9 : 5;
|
||||
}
|
||||
context.fillText(column.characters[index], column.x, characterY);
|
||||
context.shadowBlur = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private randomCharacter(): string {
|
||||
const characterSet = Math.random() < 0.48
|
||||
? BINARY_CHARACTERS
|
||||
: Math.random() < 0.68
|
||||
? HEX_CHARACTERS
|
||||
: Math.random() < 0.72
|
||||
? CODE_CHARACTERS
|
||||
: TERMINAL_CHARACTERS;
|
||||
|
||||
return characterSet[Math.floor(Math.random() * characterSet.length)];
|
||||
}
|
||||
|
||||
private randomInteger(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
|
||||
ilI,; l!;;I;:l
|
||||
i>:;!,,:;'.``^`'`^^";:,
|
||||
,^",` ^".."` . ."'^' ."'.^!:,
|
||||
:I::,^":,``' .. .'.'.. ..''.
|
||||
<i:' ^^:;^^`'.. . ' '.... . . ."
|
||||
;^`. .":'^l^.. '`.'':'.''. ,` `
|
||||
!^....,^''..l^.'^"`''^`.^^.. ' .' .
|
||||
,l^ .`':`'^":,'";!ii>~<I;!"``..`""^'''. .:,`^. '
|
||||
:.;^. ..^:lii~[)/tfjftf/)]<<!>~>+_~i!i!I:^`^ ... ... .`
|
||||
~l,`"`::;+/vzXUQ00LCJUYYXzvur/|1}}{[[[[]?-+<I". .' ..
|
||||
"l!;`^!l+/QmwwwwwwmO0CJJYYXXXcvnxrjf/||(){{}?+i:^ '
|
||||
'``,^;l,i]zZmmmqqwwwmO0LCJUUUYXXzvunrjftt\/\(){[?+>:^ .
|
||||
"',,'':~i}CmmwqppwwwmZO0QCJUYYUUUUYYzvxjf/\|(||1[?_+~i"
|
||||
"":``^":i_X0OmwpdqwwwqqpppmOQCLLQQQQCXcunxf/|(11{]-+~<>I^.
|
||||
"^^i>i>i<~)YLQOpdqwpdbkkkkbdqwZZO0O0CXvnxxrft\\|){]-+~<<i:`.. .. .
|
||||
``:~+><+_+)XLQO0XvYUcrt\\\rvJ0ZZZQUcj({-~>!ll!i<>i!i>><>>:^.. .
|
||||
''^li:`;:"<)YQJnt1{]<;,::;!i?1fQdpCn)?>l"^`'''`''`^"",^;>>I'... ...
|
||||
''!` ``^li[UY/rvxucxrrt|111(|fQhhLf]<!l!ll>+?[{}?~i!l;,;il' . .
|
||||
.^;l'''`,;l_YnzQOwwppOYnt|))\rJwbpLr?<i!ll<])ff/({?___~>Iii' .
|
||||
`<^'.";!i~+cYQQCzXct{<!!~<_\JZqbmz(-~!l!;;!?+l;,;ll!<++~>i^ . .
|
||||
iI"'.,i"I~_YQ0Yt[<}(;":![~<nbwdkZu}__<!i!l~)~,::!I^";i~_+~I
|
||||
;;I'^II!>/Oww0LUY0OUvrffxUwqpkkZu1-___++?[[)(1[?-+<><+--+<^ .
|
||||
il:":i;!_JZqpko*hdmOO0Owbakdbad0Xf{-+_--[{)(|\||()1}[?__+~!.
|
||||
!,;!:!i\Omdkooahkkbkkka*hbh#MpQJz\[-++-[1(/fxnnnxj/(}]_+~~, .
|
||||
"^!,!+!umpkho*#*oaahkkdkhhoMowCct}]?--_?{(tjnvcvux/(1}]_++l
|
||||
?[~:<>lUpbkh*#MW#ohdZUYwqJnCQx|{-<li-?~~?}|txnnnrf\(1}]?-+i' ^:;
|
||||
cz1+>~l-Opbdho##*akmUt/Cdpn~]f{_~il:,lii+--?}|/tt\()1}]??-+>" ,;I<
|
||||
/U_<fu}!1Owppkkkkbq0u/nQbaahwCXf]<!l!!!>~+_---]}1)11}[]]--_~>l' ll:,~
|
||||
u\~|k&Z~[QZwwppqwZQcuJJOdpbbdbqv)(//{__~<~~+~~~?][}[]]--__+<>i^."I_<!:l
|
||||
rv-uadL{+COmwwwmZ0QYCrjObhhkpm0JJCvt)[?---__+<i~+-[]?--___+~<i;;"<-]~,I
|
||||
XxCdnt\1cOZqpwwZZOCx+|zLXr\|1))1{i!!!lIII!ii!l>-][]?-----+~<ii<:l!>_:i
|
||||
vwpcvQpJCZwpqmmmmQn{)txfuXLOQJXvcnjj\]~il;IIl+[{[]?--?-_+~<<iiil;!<!
|
||||
vpZZbomUOmqwmmmZ0CJZhaahhhd0vft/t\)[?-----]]][}?-----__~~<i>~_~<>i+
|
||||
rZboahQLOmwmmZOOZwdkkbddpOJuf|))\|1[]??-?]]][]------_~<<ii++++>i
|
||||
tzJQJU0OmmmZZmwwqdkkkkkbdqZQCYzvj\1}[?-??]]?---___+~~<I:l!<+
|
||||
[CQOZZZmmwpdbbkhoooakpmLJYcxf\)}]]]]]?--___+~<<<!
|
||||
)YC00OOmwqdkkkhaooahp0CUYvf\(1[]]]]]?-___+<>!!l^
|
||||
1nXJQOZmwpdbkhaddpm0UXcut)[[[]?---___++<<i!I;^
|
||||
-|fcUJL0ZmmqwmmQJXn/||1]?_++++<<<>><<>!I;:,"
|
||||
f\)|/rvvzCCXuxf/(}-__<~>i!!!!i!!!l!l;:,:,;;
|
||||
rCj/{{{1(|\({[-_++_<~~<<~~<i!lIllII::::Ill
|
||||
(0Ycr/|)[?------?---___+__~<!llllII;;;l!!I
|
||||
0OCXnjft({]+<<~++++++~+~>>il!lII;;Ill!iil
|
||||
0mOQUYzczzuf([-+<iii!i>ii!lIIlI;Il!!iiiI-/
|
||||
C0OO0QLCQQLJJJXnf({?+~<<>ii!!l!!!><>i!!+)uz|
|
||||
*&UOOZZO000QQLCCJXvx([_~~~<<<>>>iiii~?(rczvur
|
||||
f$@aOOmmmmZOOQQLLCCUj}?_+~+_~~>i<+])fuzXXzvvnn(l
|
||||
Z@@$%hwmwwmZO00QLJYx1[[[[]-++])jvXYUYXzccvvvnxr< ^i
|
||||
)8$@@$$B*dwmOZZO0LUvt()1{1\xUOwwOCUYYXzzccvvunnx)`'."
|
||||
[-C$@$$$@$$$8aqZOOO0CzurrvCd#8&#kqZ0QCYXccccvvvuxr-'''.',i
|
||||
}}}M$@$$$$$@@$$$B#dZCCLZbW$$$$8MokpwZLYzccccvvvunj),'''''..^l
|
||||
-]{+r$@$$$$@@$$@%WodOwbOQJXXYJ0wddkhkq0JXzzzccvvxxrf+`^`'''''...':i
|
||||
--_?}]~L$B$@@$$$8a#dLYYJk#dmCXcccvu+~|nYLO0CUXzzccvvvu|I"^```````'`'...'^":li
|
||||
]]??--]]]>w$B@$$Bbc(1dW8@$$@B$$BMdJxf)(f)?_]|xUQOZ0QQLJQj!;,"^^^^^^`'...........''^,;!
|
||||
_+~<<~_??-]]?->w$$$#0r|{{tzmbkkoM%@$BWbCr)[[][}}[-<<_{tvU0wOc~~l,""^^""^^",^`'`^^^^^^^^``'''^":Ii
|
||||
_+~~~<~~<i<-?-?]]__|QBbYvrjxvUQCUJ0Qmh#B@MZvt1}1{-~~~+-[)\||/rt)\]_<I,,""",,"":IlI;,,""",",,,,","""^^^^":I!>
|
||||
+++~~~~++__---?]]???]?<1zm%phB%*oW%BB&k0XJYb$$*X\1[11_~]{|rcJ0wd*Wkz(tc_iI:,,",:::,:;l!lI;:^.'":::::;;:;:;:::,,""",:;l!>
|
||||
+++~~~+++++--__----?]????]_itJo@$$$B&M&$$$@BWk0xm&aCf)}{1{)\fjuU0mdoW&hJjX*Y>>l;:,",,:;IlIlii!;,. ^:;;IIIIIII;II;;;;;:::,,,,:,,:;;!
|
||||
~~~~~++___++_++?---_---]]????-~<fCW@@@$@8WW%@@@BB@BZmou]><}|1fttftvQwk#&WkCuc*#[<!I::,,,:;Ill!i<>!:' ^;IIIllllllllllIIlIII;I;;;;;:::":ll;":
|
||||
~<<~~+__++++_____+_]__-_-??]????]->-rL8$$$$@B%8B@B%8&%$*d$L1_i!{tj/\\nLwk#&8h0YXO&z><l;::,,:;II!ii>>iI^ ':Illl!!!!!l!!!!l!!lllllllIII;;:!>l:`'..!
|
||||
<<~<~+++_-________-__++_]__-_-?-?]??]?-i{xJ&$@$$$$@B@%8MW&B$ka%Y)-?+?r//xUOph#&WawCJZhb-<!lI;:,,;;Il!><<il"..,ll!i!!i>iii!!iii!!!!!!!!!llll:i>I:^'''..
|
||||
~><<><~+_-_+-_____---_-__+_~_]_-?--?-?]??]-+!)fL8$@$$$$@BB&WMW8$Bd%&Y(?_]?|tcZdkoMWMapOLOdWr>>lI;::,:;II!i<<>l:. ^Iliiii>>>>ii!ii!iiii!!ii!llll;i<;,^....'."
|
||||
+~;i++___---_-??_-_---_--__++~_?--?-?--]]?]?-~!)tO%$$$$@@B%%8%WW%@*M@hY/[---]f0dh*WW#*km0Qq*w~~iI;:,,:;;II!><<iI` ':l!ii><<>>>i!!i!!iiiii!ill!l!I!<;,`'....''.
|
||||
_-i:------_----??--_-_-___-__+_-?--?????]]]??_<i{/m@$$$@@B%8&WMWMB%8$%pXf1]-?]1Ua#MWM**hmQOd*)<<il;:,:;III!i>>i!" .:liii>>>><ii!llll!!iii!!ll!l!ll~;"^'......'."
|
||||
_--i,?----__---_?-?----_++___-__-]---?]?-??]]-_<i1/d$@$$@B%&#*a*&B@%@BMmXr|{}[[}\dW&W#*od00w*Y>~ilI;::::Ill!ii>!:. "!i><><<>>>i!lIIIll!!ii!!lII!l:~l"^''.......'.
|
||||
___~"+-____---__-??---__~_-__?---]---?-??]]]?-_><)j*$@$@B&Woka#8@$@@@8aOXnf\)){{{JM8&#obOQmhk]~>!I;:,::;I!!!i>>I' `li>><<<<ii!lll;;Illll!!!!;Ill,i>;,^'........'.:
|
||||
-__+,l?______-_???-?-_+__------__]?--???]]]]?-_i+)c%$@@B8#ahaWB@$@@@%Wk0Ycxf\\1{{xo%8MhmC0dWn~<!I;::::;II!iii>!" 'I>><>><<i!!!lI;::;Illl!!l;:;;":~I,`'...........,
|
||||
+-+++!^____++---????-__+__----_--_]?---?????]]?_i~{0$@@@%#hkaMB@@$@B%&*dQYXvrft(}}fkB8#pLCm#w+~il;:,,;;;Il!i!>i: 'I>>>>>>iii!lIII;:,:;IllllI:"""`<>:`''.......'...'
|
||||
_?_++<^!-+_+_---??---__---???-?___?]--?]]??]]]?_!>1p$@@BWabk#%@@@@B%W*apQCYcuxr|[{tkB8hZLZb#)<>lI;:::;IIl!iiii;..;>>i>>>!!i!I;;lI::,,;IIll;:,^^.;+l,`'''''.....''. ^
|
||||
_-_~~<I"+~_+_?--?----__------?-_-_?]-_]????]]]?_!>)qBBB8*kk*8@@B%%&M*aodOLCYcux|[[/hB&bZ0qWX><lI;:::;I;l!!>>>l' ,!ii>>!ii!llI;,:;;;,,:;I;::,"^'.>i;,^^`''''.....''. ;
|
||||
_-_<~>!^l____?---_--___-_?-----_-_-]?-???]]]??--<~(0&BBWhko&@@B%&M*hhaadmO0JXcv|?[r*%#bZ0hd_~!I;:,::IIII!>ii!` ^l!!i!l!!lllI;:::,::::;;;::"^^' "~l;,^``'''.......''.'
|
||||
__+~<>iI,~+_---__-_-___??---?---___]]-?]]???????~~|za8%Mkk#B$B%W*kkddkhdmwZQUXc)?[uW%#bZZo|<iI;:,,:;IIll!>il^ `:li!IIll;llIIl;"`^",:;;;:"^^`'' l~I;,`''''.........''
|
||||
~__~<><>!"l+__-__--____-??_--_----__]??-??-???]??>;)fmM8#ka&@B%MaadppkaakpqqZLCz1?[c&%opZhu<>I;:,,:;IIIlli>l` ^!!i!lIl;::;:::""^^^"""::"^'`'''..ii;:,`'''...........' ,
|
||||
~~~<>i>>i:^<+__+__-__-_---?-_---_-__-]?----???]?_i"]|Jo&*a*&@B&ddbbpb*##hdbdqm0z[]}X&&kwaY<<l;;,",:;IIl!iil` ':!!llI;;::,,"^````^^^^`'..''''.. ^il;,^''.............'.^
|
||||
~~<<>iiiil^l_+++_+__---?----_--_-_-__]?----??---+>,<(ck##*#8@%amwOppd#&Whbbddqwc]}}C%#wkQ<>!I:,"":;IIIliil` ',ll!!llII;;:;:,"^"""""^'..''..... ,I;"`'''...............^
|
||||
~~<<<ii!ii!:"~~~__++-?-??--+-__---_-__-?----?---__~l;{xwoM*M8B&kwOLmZq#8&*hhhdppv]{}0%awd[>!l;:""":;Il!i!I` .":;;I;;;;;;;:,,"^^"^""^`'..''..... ,;:^^''''...............
|
||||
<<<<ii!l!i!;^;~~-___-_--__+_-__??_---_~??__------_+>:+\Ch#W&B&#admmmOdW8&MM&Wokkn?}1wWbbr>>lI;,,,,:;I!!!;' '":;;;II;;;I:::,""^^^```''...''.... .":"^`''''`'''.........'.`
|
||||
><<<iii!!iil,^i++~____++____-_--___---+_]-__-_----+~l;{xq*W&%o#hdZwppaW&WMW&Moaar-[\pahY<<lI;:,:::;Il!l:' '^:;I;;;;II;:::,",,"^```''....'..... .^"^`````````''.........'.:
|
||||
<<>ii!!!!!il;^,+~~~+++~~_-__-_-_-__-___+-?_-__-__-_~i"_\Jh#WMbWdpwOqbaW8&MW&#*ohf_[fmh0+>!l;:,,::;IIlI,..'",:;;;;;;:::::,""""^^``''.......... '`^^^^^^^^````'.'.........`
|
||||
<<<>!!!!ll!!!l:'I~<~++++_+++_+----_-___--_?-______-+<>;;1rq*#hd&bqwmpaaW8WMMM*oak\~[jwp]>ilI::,,:;;III"..'":::;;::::::,,""""^^^`''''......... .`^^^^^^``````'............
|
||||
>><i!!!!lllll!:^'i~<<_++++~+_---____+-_-_~~?__++___+~>!^_/Chopb&ahkbh*M&WW###ohab)>-nd1i>lI;:::::;II;^ .'^,,::::;:::,:::,",""`'''''.......... .'^^""^^````^^`'...........^
|
||||
i><ii!!lllllIl:"'^~~<++~~_+__-_-__++++_+~<<_?_+~+___~<!:;)nwkwkW*#**#MWWWM*ooahob)i<nti<lI;::;;;;II:' .'^",:::;;;;:;;:;::,,^`'''............ .'`^"^`````^^^``'...........
|
||||
><>i!!llllll;I;,"."+<<~+~~+_+____+_~_+__~++<--++~++_~<il^+/UwQkM**#MWWWWW#aahkhhb)<_[<<il;:,;;;;II,' .'^,::;;;;III;III;,""^`'''............ .'^^^^^^`^^^^```''..........`
|
||||
><i!l!!IlIllIlI,^' ^~<<~~~++~___-_++_++++~<<<__+~~~~+~>l:,{rJzkM####MMM##ohhbddkb1!]~>ilI;:;IlIII". .'^,;I;;;Il;lII;;,,^^'''.............. .'`^^`^``^`'^`^`''...........
|
||||
<<!ll!lIlllll!l;^'. ^<<<~~~+~+++__+_++++~+<><>__~<~~~<<!I^i\uvk**#*#*****ahkppwpb}!->!l;;;;;IlI;^...'"::;II;lIIII;;,""^''................. ..'```'''````````''...'......`
|
||||
>>!ll!lllIlIIII:,^.. ^<~~<~+~++~~+++++~++<++<>>-_~<<<>>il;^]/jpaaaaoao*oahakmZO0w)~<ill;;IllllI^ ..`",:;IlllllII;,"``'................... .'````'`````````'''..'.......
|
||||
>illIllll;;lI;;:"`. '<~<~+<~~<~~<~~~+~~+<~~<i<-+<ii>ii!I"I}(LqdbkkkhhhhkhkmQJJY_>i!IIIIll!!;' ..^,;;Ill!!lI;:,^`''..................... .''```^^^```^^``'''..''......,
|
||||
>!Illl!lII;I;;;,^`' ^+~~~~<~<<<<<<<+~~<~~~~<<>+~i!!ii!lI"!]uYLOOmpkhhhhkpOJYX?ii!!;l!!!!l,. ..^,;Il!lllI:,^^'''....................... .'````^^^^```^'`'''....'.....'
|
||||
illllllIIII;;I;"^`' ;_+~<<~<<<<<<~<~~<<+<>~<i>_+i!iii!l;^,1xcYJQmkhhkkbp0UY]ii!!lli>i!l^ .'^,;;llII;:,"^``'''...................... ..'```^^^`^^^^```'''....'.....
|
||||
illlll;;II;;;;;,^`'. !_<>~~~+><<<<<~<<<><<><>>>~+<<+~~~>;`l|uvvcCwbhkbdw0L1iil!!!i>i!;' ..'^,;;;I;;;,""``''......................... ..'``^^^^^^^^```''''....''....^
|
||||
lIlllII;I;;;:,:"^`'. .<_<<<~+<<<<<+~<>><<<<>><il<_+~<<~iI"^~tvvnvCqkkdqZm\i<!!!ii>i!:. ..'^,;:;;;:""^`'''.......................... .''``^^^^""^``'''''.....'.....
|
||||
illllI;;;;;:;:,:"^`'. `_~>><<>><>~~<<><<<<~<><>i!i+<il;;:"^,[nXcvYObkdwpni>!iii>>il^ ..`":;;:;,,^^`'''............................ .''``^^^^""^``'''''......'... ,
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"rootDir": "./src",
|
||||
"types": []
|
||||
},
|
||||
"include": [
|
||||
|
|
|
|||
Loading…
Reference in New Issue