Added dummy content and deployment things

This commit is contained in:
2026-04-05 00:09:58 +02:00
parent bebe21446d
commit ad94655967
26 changed files with 1000 additions and 357 deletions

View 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
View 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;
}
}

View 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>

View 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
View 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 {};

View 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;
}
}

View File

@@ -0,0 +1,35 @@
<div class="page">
<section class="hero section">
<p class="eyebrow">Contact</p>
<h1>Lets 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,
Id 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 youre using. Ill reply
with a clear next step.
</p>
</app-card>
</section>
</div>

View 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);
});
});

View 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
View 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
View 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>

View 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
View 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.',
],
},
];
}