Added dummy content and deployment things
This commit is contained in:
36
src/pages/about/about.component.ts
Normal file
36
src/pages/about/about.component.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CardComponent } from '../../components/card/card';
|
||||
|
||||
export interface Pillar {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
imports: [CardComponent],
|
||||
templateUrl: './about.html',
|
||||
styleUrl: './about.css',
|
||||
})
|
||||
export class AboutComponent {
|
||||
readonly pillars: Pillar[] = [
|
||||
{
|
||||
title: 'Angular UI',
|
||||
description: 'Component-driven interfaces with a strong focus on clarity, speed, and accessibility.',
|
||||
},
|
||||
{
|
||||
title: 'C# Backend',
|
||||
description: 'Reliable APIs and services shaped with clean architecture and maintainable domain logic.',
|
||||
},
|
||||
{
|
||||
title: 'Flutter Mobile',
|
||||
description: 'Polished mobile apps with shared design systems and a smooth native feel.',
|
||||
},
|
||||
];
|
||||
|
||||
readonly values: string[] = [
|
||||
'Ship practical features with a thoughtful product mindset.',
|
||||
'Keep the codebase simple, testable, and easy to evolve.',
|
||||
'Balance visual polish with performance and real-world usability.',
|
||||
];
|
||||
}
|
||||
92
src/pages/about/about.css
Normal file
92
src/pages/about/about.css
Normal file
@@ -0,0 +1,92 @@
|
||||
.page {
|
||||
margin: 0 auto;
|
||||
padding: 1rem 1.25rem 4rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 2.5rem;
|
||||
border: 1px solid rgba(110, 118, 129, 0.28);
|
||||
border-radius: 24px;
|
||||
background: radial-gradient(circle at top right, rgba(88, 166, 255, 0.16), rgba(13, 17, 23, 0.9));
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
width: fit-content;
|
||||
margin: 0 0 1rem;
|
||||
padding: 0.3rem 0.7rem;
|
||||
border: 1px solid rgba(88, 166, 255, 0.5);
|
||||
border-radius: 999px;
|
||||
color: #9ecbff;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
p,
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2rem, 5vw, 3.2rem);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.lede {
|
||||
max-width: 65ch;
|
||||
margin-top: 1rem;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.section-title-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: baseline;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-title-row p,
|
||||
.card p,
|
||||
.story li {
|
||||
color: #b6bec8;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* base .card container styles moved to src/components/card/card.css */
|
||||
|
||||
.card h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.story ul {
|
||||
padding-left: 1.2rem;
|
||||
}
|
||||
|
||||
.story li + li {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.page {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 2rem 1.2rem;
|
||||
}
|
||||
}
|
||||
36
src/pages/about/about.html
Normal file
36
src/pages/about/about.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<div class="page">
|
||||
<section class="hero section">
|
||||
<p class="eyebrow">About</p>
|
||||
<h1>Fullstack developer crafting modern web, backend, and mobile products.</h1>
|
||||
<p class="lede">
|
||||
I am Alex Carter, and I enjoy building software that feels clean, fast, and reliable from
|
||||
the first screen to the last API call.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-title-row">
|
||||
<h2>What I bring</h2>
|
||||
<p>Core strengths across the stack.</p>
|
||||
</div>
|
||||
<div class="card-grid">
|
||||
@for (pillar of pillars; track pillar.title) {
|
||||
<app-card>
|
||||
<h3>{{ pillar.title }}</h3>
|
||||
<p>{{ pillar.description }}</p>
|
||||
</app-card>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section story">
|
||||
<app-card>
|
||||
<h2>How I work</h2>
|
||||
<ul>
|
||||
@for (value of values; track value) {
|
||||
<li>{{ value }}</li>
|
||||
}
|
||||
</ul>
|
||||
</app-card>
|
||||
</section>
|
||||
</div>
|
||||
29
src/pages/about/about.spec.ts
Normal file
29
src/pages/about/about.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AboutComponent } from './about.component';
|
||||
|
||||
describe('About', () => {
|
||||
let component: AboutComponent;
|
||||
let fixture: ComponentFixture<AboutComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AboutComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AboutComponent);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render about content', () => {
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Fullstack developer');
|
||||
expect(compiled.querySelectorAll('.card').length).toBe(component.pillars.length + 1);
|
||||
});
|
||||
});
|
||||
41
src/pages/about/about.ts
Normal file
41
src/pages/about/about.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CardComponent } from '../../components/card/card';
|
||||
|
||||
export interface Pillar {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
imports: [CardComponent],
|
||||
templateUrl: './about.html',
|
||||
styleUrl: './about.css',
|
||||
})
|
||||
export class About {
|
||||
readonly pillars: Pillar[] = [
|
||||
{
|
||||
title: 'Angular UI',
|
||||
description:
|
||||
'Component-driven interfaces with a strong focus on clarity, speed, and accessibility.',
|
||||
},
|
||||
{
|
||||
title: 'C# Backend',
|
||||
description:
|
||||
'Reliable APIs and services shaped with clean architecture and maintainable domain logic.',
|
||||
},
|
||||
{
|
||||
title: 'Flutter Mobile',
|
||||
description: 'Polished mobile apps with shared design systems and a smooth native feel.',
|
||||
},
|
||||
];
|
||||
|
||||
readonly values: string[] = [
|
||||
'Ship practical features with a thoughtful product mindset.',
|
||||
'Keep the codebase simple, testable, and easy to evolve.',
|
||||
'Balance visual polish with performance and real-world usability.',
|
||||
];
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
88
src/pages/contact/contact.css
Normal file
88
src/pages/contact/contact.css
Normal file
@@ -0,0 +1,88 @@
|
||||
.page {
|
||||
margin: 0 auto;
|
||||
padding: 1rem 1.25rem 4rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 2.5rem;
|
||||
border: 1px solid rgba(110, 118, 129, 0.28);
|
||||
border-radius: 24px;
|
||||
background: radial-gradient(circle at top right, rgba(47, 129, 247, 0.18), rgba(13, 17, 23, 0.9));
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
width: fit-content;
|
||||
margin: 0 0 1rem;
|
||||
padding: 0.3rem 0.7rem;
|
||||
border: 1px solid rgba(88, 166, 255, 0.5);
|
||||
border-radius: 999px;
|
||||
color: #9ecbff;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2rem, 5vw, 3.2rem);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.lede {
|
||||
max-width: 65ch;
|
||||
margin-top: 1rem;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* base .card container styles moved to src/components/card/card.css */
|
||||
|
||||
.contact-card .label {
|
||||
margin-bottom: 0.5rem;
|
||||
color: #8b949e;
|
||||
font-size: 0.92rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.contact-card a,
|
||||
.value {
|
||||
color: #e6edf3;
|
||||
font-size: 1.02rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.note .card {
|
||||
border-color: rgba(47, 129, 247, 0.45);
|
||||
background: linear-gradient(120deg, rgba(31, 111, 235, 0.16), rgba(22, 27, 34, 0.92));
|
||||
}
|
||||
|
||||
.note p {
|
||||
color: #c9d1d9;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.page {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 2rem 1.2rem;
|
||||
}
|
||||
}
|
||||
35
src/pages/contact/contact.html
Normal file
35
src/pages/contact/contact.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<div class="page">
|
||||
<section class="hero section">
|
||||
<p class="eyebrow">Contact</p>
|
||||
<h1>Let’s build something polished, useful, and fast.</h1>
|
||||
<p class="lede">
|
||||
If you have a product idea, an open role, or an app that needs a reliable fullstack hand,
|
||||
I’d be happy to talk.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="card-grid">
|
||||
@for (channel of channels; track channel.label) {
|
||||
<app-card cardClass="contact-card">
|
||||
<p class="label">{{ channel.label }}</p>
|
||||
@if (channel.href) {
|
||||
<a [href]="channel.href">{{ channel.value }}</a>
|
||||
} @else {
|
||||
<p class="value">{{ channel.value }}</p>
|
||||
}
|
||||
</app-card>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section note">
|
||||
<app-card>
|
||||
<h2>Preferred first step</h2>
|
||||
<p>
|
||||
Send a short message with your goals, timeline, and the stack you’re using. I’ll reply
|
||||
with a clear next step.
|
||||
</p>
|
||||
</app-card>
|
||||
</section>
|
||||
</div>
|
||||
30
src/pages/contact/contact.spec.ts
Normal file
30
src/pages/contact/contact.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { Contact } from './contact';
|
||||
|
||||
describe('Contact', () => {
|
||||
let component: Contact;
|
||||
let fixture: ComponentFixture<Contact>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [Contact],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Contact);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render contact cards', () => {
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('build something');
|
||||
expect(compiled.querySelectorAll('.contact-card').length).toBe(component.channels.length);
|
||||
});
|
||||
});
|
||||
|
||||
32
src/pages/contact/contact.ts
Normal file
32
src/pages/contact/contact.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CardComponent } from '../../components/card/card';
|
||||
|
||||
interface Channel {
|
||||
label: string;
|
||||
value: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-contact',
|
||||
imports: [CardComponent],
|
||||
templateUrl: './contact.html',
|
||||
styleUrl: './contact.css',
|
||||
})
|
||||
export class Contact {
|
||||
readonly channels: Channel[] = [
|
||||
{
|
||||
label: 'Email',
|
||||
value: 'alex.carter.dev@mail.com',
|
||||
href: 'mailto:alex.carter.dev@mail.com',
|
||||
},
|
||||
{
|
||||
label: 'Location',
|
||||
value: 'Remote / UTC+2',
|
||||
},
|
||||
{
|
||||
label: 'Availability',
|
||||
value: 'Open to freelance and full-time roles',
|
||||
},
|
||||
];
|
||||
}
|
||||
182
src/pages/home/home.css
Normal file
182
src/pages/home/home.css
Normal file
@@ -0,0 +1,182 @@
|
||||
.page-wrap {
|
||||
margin: 0 auto;
|
||||
padding: 1rem 1.25rem 4rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 4rem;
|
||||
scroll-margin-top: 5.5rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 2.5rem;
|
||||
border: 1px solid rgba(110, 118, 129, 0.28);
|
||||
border-radius: 24px;
|
||||
background: radial-gradient(circle at top right, rgba(88, 166, 255, 0.2), rgba(13, 17, 23, 0.8));
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
width: fit-content;
|
||||
margin: 0 0 1rem;
|
||||
padding: 0.3rem 0.7rem;
|
||||
border: 1px solid rgba(88, 166, 255, 0.5);
|
||||
border-radius: 999px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
color: #9ecbff;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(2rem, 5vw, 3.4rem);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.hero-role {
|
||||
margin: 0.75rem 0 0;
|
||||
font-size: 1.35rem;
|
||||
color: #79c0ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hero-intro,
|
||||
.hero-focus {
|
||||
max-width: 60ch;
|
||||
margin: 1rem 0 0;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.hero-cta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.72rem 1.2rem;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #1f6feb, #2f81f7);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 10px 24px rgba(31, 111, 235, 0.35);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
border: 1px solid #3d444d;
|
||||
color: #e6edf3;
|
||||
background: rgba(33, 38, 45, 0.55);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
border-color: #58a6ff;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: clamp(1.5rem, 2.2vw, 2rem);
|
||||
}
|
||||
|
||||
.section > p {
|
||||
max-width: 70ch;
|
||||
margin: 0.9rem 0 0;
|
||||
color: #b6bec8;
|
||||
}
|
||||
|
||||
.section-title-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.3rem;
|
||||
}
|
||||
|
||||
.section-title-row p {
|
||||
margin: 0;
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
margin: 0;
|
||||
font-size: 1.08rem;
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: #8b949e;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.project-card p {
|
||||
margin: 0.75rem 0 0;
|
||||
}
|
||||
|
||||
.impact {
|
||||
color: #9ecbff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
display: grid;
|
||||
grid-template-columns: 260px 1fr;
|
||||
gap: 1rem;
|
||||
padding: 1.1rem;
|
||||
border-left: 3px solid #2f81f7;
|
||||
border-radius: 12px;
|
||||
background: rgba(22, 27, 34, 0.85);
|
||||
}
|
||||
|
||||
.timeline-item ul {
|
||||
margin: 0;
|
||||
padding-left: 1rem;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.timeline-item li + li {
|
||||
margin-top: 0.45rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-bottom: 0;
|
||||
text-align: center;
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.page-wrap {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 2rem 1.2rem;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
68
src/pages/home/home.html
Normal file
68
src/pages/home/home.html
Normal file
@@ -0,0 +1,68 @@
|
||||
<div class="page-wrap">
|
||||
<header class="hero section" id="top">
|
||||
<p class="eyebrow">Available for full-time and freelance work</p>
|
||||
<h1>{{ hero.name }}</h1>
|
||||
<p class="hero-role">{{ hero.role }}</p>
|
||||
<p class="hero-intro">{{ hero.intro }}</p>
|
||||
<p class="hero-focus">{{ hero.focus }}</p>
|
||||
<div class="hero-cta">
|
||||
<a class="btn btn-primary" routerLink="/about">About Me</a>
|
||||
<a class="btn btn-ghost" routerLink="/contact">Contact</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="section" id="stack">
|
||||
<div class="section-title-row">
|
||||
<h2>Tech Stack</h2>
|
||||
<p>Primary technologies I use to deliver production-ready software.</p>
|
||||
</div>
|
||||
<div class="card-grid">
|
||||
@for (skill of skills; track skill.name) {
|
||||
<app-card cardClass="skill-card">
|
||||
<h3>{{ skill.name }}</h3>
|
||||
<p class="meta">{{ skill.category }} - {{ skill.level }}</p>
|
||||
</app-card>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="projects">
|
||||
<div class="section-title-row">
|
||||
<h2>Featured Projects</h2>
|
||||
<p>Recent work across web platforms, APIs, and mobile apps.</p>
|
||||
</div>
|
||||
<div class="card-grid projects-grid">
|
||||
@for (project of projects; track project.title) {
|
||||
<app-card cardClass="project-card">
|
||||
<h3>{{ project.title }}</h3>
|
||||
<p>{{ project.description }}</p>
|
||||
<p class="meta">{{ project.stack.join(' - ') }}</p>
|
||||
<p class="impact">{{ project.impact }}</p>
|
||||
</app-card>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="experience">
|
||||
<h2>Experience</h2>
|
||||
<div class="timeline">
|
||||
@for (item of timeline; track item.role + item.company) {
|
||||
<article class="timeline-item">
|
||||
<div>
|
||||
<h3>{{ item.role }}</h3>
|
||||
<p class="meta">{{ item.company }} - {{ item.period }}</p>
|
||||
</div>
|
||||
<ul>
|
||||
@for (highlight of item.highlights; track highlight) {
|
||||
<li>{{ highlight }}</li>
|
||||
}
|
||||
</ul>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="section footer">
|
||||
<p>(c) {{ currentYear }} {{ hero.name }} - Fullstack Developer (Angular - C# - Flutter)</p>
|
||||
</footer>
|
||||
</div>
|
||||
37
src/pages/home/home.spec.ts
Normal file
37
src/pages/home/home.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { Home } from './home';
|
||||
|
||||
describe('Home', () => {
|
||||
let component: Home;
|
||||
let fixture: ComponentFixture<Home>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [Home],
|
||||
providers: [provideRouter([])],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Home);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render hero title and project section', () => {
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Alex Carter');
|
||||
expect(compiled.querySelector('#projects h2')?.textContent).toContain('Featured Projects');
|
||||
expect(compiled.querySelector('.hero-cta a[routerlink="/about"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render all project cards', () => {
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelectorAll('.project-card').length).toBe(component.projects.length);
|
||||
});
|
||||
});
|
||||
92
src/pages/home/home.ts
Normal file
92
src/pages/home/home.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { CardComponent } from '../../components/card/card';
|
||||
|
||||
interface Skill {
|
||||
name: string;
|
||||
category: 'Frontend' | 'Backend' | 'Mobile' | 'Cloud';
|
||||
level: string;
|
||||
}
|
||||
|
||||
interface Project {
|
||||
title: string;
|
||||
description: string;
|
||||
stack: string[];
|
||||
impact: string;
|
||||
}
|
||||
|
||||
interface TimelineItem {
|
||||
role: string;
|
||||
company: string;
|
||||
period: string;
|
||||
highlights: string[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
imports: [RouterLink, CardComponent],
|
||||
templateUrl: './home.html',
|
||||
styleUrl: './home.css',
|
||||
})
|
||||
export class Home {
|
||||
readonly currentYear = new Date().getFullYear();
|
||||
|
||||
readonly hero = {
|
||||
name: 'Alex Carter',
|
||||
role: 'Fullstack Developer',
|
||||
intro:
|
||||
'I build polished web, backend, and mobile products with Angular, C#, and Flutter.',
|
||||
focus: 'Focused on performance, clean architecture, and product-minded delivery.',
|
||||
};
|
||||
|
||||
readonly skills: Skill[] = [
|
||||
{ name: 'Angular', category: 'Frontend', level: 'Advanced' },
|
||||
{ name: 'TypeScript', category: 'Frontend', level: 'Advanced' },
|
||||
{ name: 'C# / .NET', category: 'Backend', level: 'Advanced' },
|
||||
{ name: 'REST APIs', category: 'Backend', level: 'Advanced' },
|
||||
{ name: 'Flutter', category: 'Mobile', level: 'Advanced' },
|
||||
{ name: 'Firebase', category: 'Cloud', level: 'Intermediate' },
|
||||
];
|
||||
|
||||
readonly projects: Project[] = [
|
||||
{
|
||||
title: 'ClinicFlow Platform',
|
||||
description: 'Patient scheduling and billing dashboard for multi-location clinics.',
|
||||
stack: ['Angular', 'C#', '.NET API', 'PostgreSQL'],
|
||||
impact: 'Reduced booking mistakes by 38% and improved team response speed.',
|
||||
},
|
||||
{
|
||||
title: 'FieldOps Mobile App',
|
||||
description: 'Offline-first mobile app for technicians to manage service tasks.',
|
||||
stack: ['Flutter', 'C#', 'SQLite', 'Azure Functions'],
|
||||
impact: 'Enabled same-day job updates even in low-connectivity zones.',
|
||||
},
|
||||
{
|
||||
title: 'Insights Portal',
|
||||
description: 'Real-time analytics workspace with modular report widgets.',
|
||||
stack: ['Angular', 'SignalR', 'C#', '.NET'],
|
||||
impact: 'Cut reporting time from hours to minutes for operations teams.',
|
||||
},
|
||||
];
|
||||
|
||||
readonly timeline: TimelineItem[] = [
|
||||
{
|
||||
role: 'Senior Fullstack Developer',
|
||||
company: 'Northline Digital',
|
||||
period: '2023 - Present',
|
||||
highlights: [
|
||||
'Led Angular frontend redesign for enterprise dashboard products.',
|
||||
'Built C# microservices and optimized API response times by 30%.',
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'Software Engineer',
|
||||
company: 'CloudMotion Labs',
|
||||
period: '2020 - 2023',
|
||||
highlights: [
|
||||
'Delivered Flutter apps with shared design system and CI pipelines.',
|
||||
'Developed secure backend services with clean architecture patterns.',
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user