From ad94655967c49261eec4ba7a81e2dc3ae9fc288f Mon Sep 17 00:00:00 2001 From: Tim Kainz Date: Sun, 5 Apr 2026 00:09:58 +0200 Subject: [PATCH] Added dummy content and deployment things --- Dockerfile | 12 + angular.json | 5 +- nginx.conf | 9 + src/app/app.config.ts | 12 +- src/app/app.css | 81 +++++++ src/app/app.html | 359 ++--------------------------- src/app/app.routes.ts | 10 +- src/app/app.spec.ts | 9 +- src/app/app.ts | 10 +- src/components/card/card.css | 11 + src/components/card/card.html | 2 + src/components/card/card.ts | 19 ++ src/pages/about/about.component.ts | 36 +++ src/pages/about/about.css | 92 ++++++++ src/pages/about/about.html | 36 +++ src/pages/about/about.spec.ts | 29 +++ src/pages/about/about.ts | 41 ++++ src/pages/contact/contact.css | 88 +++++++ src/pages/contact/contact.html | 35 +++ src/pages/contact/contact.spec.ts | 30 +++ src/pages/contact/contact.ts | 32 +++ src/pages/home/home.css | 182 +++++++++++++++ src/pages/home/home.html | 68 ++++++ src/pages/home/home.spec.ts | 37 +++ src/pages/home/home.ts | 92 ++++++++ src/styles.css | 20 ++ 26 files changed, 1000 insertions(+), 357 deletions(-) create mode 100644 Dockerfile create mode 100644 nginx.conf create mode 100644 src/components/card/card.css create mode 100644 src/components/card/card.html create mode 100644 src/components/card/card.ts create mode 100644 src/pages/about/about.component.ts create mode 100644 src/pages/about/about.css create mode 100644 src/pages/about/about.html create mode 100644 src/pages/about/about.spec.ts create mode 100644 src/pages/about/about.ts create mode 100644 src/pages/contact/contact.css create mode 100644 src/pages/contact/contact.html create mode 100644 src/pages/contact/contact.spec.ts create mode 100644 src/pages/contact/contact.ts create mode 100644 src/pages/home/home.css create mode 100644 src/pages/home/home.html create mode 100644 src/pages/home/home.spec.ts create mode 100644 src/pages/home/home.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..565cbc1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM node:lts-alpine AS build +WORKDIR /app +COPY . . +RUN npm install +RUN npm run build + +FROM nginx:alpine-slim +EXPOSE 80 +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist/website/browser /usr/share/nginx/html/web + +ENTRYPOINT ["nginx", "-g", "daemon off;"] diff --git a/angular.json b/angular.json index ff436d0..3d2852b 100644 --- a/angular.json +++ b/angular.json @@ -16,6 +16,7 @@ "build": { "builder": "@angular/build:application", "options": { + "baseHref": "/web/", "browser": "src/main.ts", "tsConfig": "tsconfig.app.json", "assets": [ @@ -24,9 +25,7 @@ "input": "public" } ], - "styles": [ - "src/styles.css" - ] + "styles": ["src/styles.css"] }, "configurations": { "production": { diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..ddedc66 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,9 @@ +server { + listen 80; + + root /usr/share/nginx/html; + + location /web/ { + try_files $uri $uri/ /web/index.html; + } +} diff --git a/src/app/app.config.ts b/src/app/app.config.ts index cb1270e..8944662 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,11 +1,17 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; -import { provideRouter } from '@angular/router'; +import { provideRouter, withInMemoryScrolling } from '@angular/router'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), - provideRouter(routes) - ] + provideRouter( + routes, + withInMemoryScrolling({ + anchorScrolling: 'enabled', + scrollPositionRestoration: 'enabled', + }), + ), + ], }; diff --git a/src/app/app.css b/src/app/app.css index e69de29..eaa0275 100644 --- a/src/app/app.css +++ b/src/app/app.css @@ -0,0 +1,81 @@ +.shell { + min-height: 100vh; + position: relative; +} + +.content { + max-width: 1500px; + margin: 0 auto; + padding: 1.5rem 1.25rem 4rem; +} + +.sidebar { + position: fixed; + top: 1.5rem; + right: 1.5rem; + width: 280px; + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1.2rem; + border: 1px solid #30363d; + border-radius: 20px; + background: rgba(22, 27, 34, 0.92); + backdrop-filter: blur(10px); + z-index: 20; +} + +.brand { + color: #e6edf3; + font-size: 1.05rem; + font-weight: 700; + letter-spacing: 0.02em; + text-decoration: none; +} + +.subtitle { + margin: 0; + color: #8b949e; + font-size: 0.92rem; +} + +.nav { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.nav a { + padding: 0.7rem 0.8rem; + border: 1px solid transparent; + border-radius: 12px; + color: #c9d1d9; + text-decoration: none; + transition: border-color 120ms ease, background-color 120ms ease, color 120ms ease; +} + +.nav a:hover, +.nav a:focus-visible, +.nav a.active { + border-color: #2f81f7; + background: rgba(47, 129, 247, 0.14); + color: #ffffff; + outline: none; +} + +@media (max-width: 1320px) { + .content { + max-width: 1040px; + } + + .sidebar { + position: static; + width: auto; + margin: 0 1.25rem 1.5rem; + } + + .nav { + flex-direction: row; + flex-wrap: wrap; + } +} diff --git a/src/app/app.html b/src/app/app.html index a1c4296..df74492 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -1,344 +1,19 @@ - - - - - - - - +
+
+ +
- - -
-
-
- -

Hello, {{ title() }}

-

Congratulations! Your app is running. 🎉

-
- -
-
- @for (item of [ - { title: 'Explore the Docs', link: 'https://angular.dev' }, - { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' }, - { title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'}, - { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' }, - { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' }, - { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' }, - ]; track item.title) { - - {{ item.title }} - - - - - } -
- -
-
-
- - - - - - - - - - - + + +
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index dc39edb..3cb520f 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,3 +1,11 @@ import { Routes } from '@angular/router'; +import { AboutComponent } from '../pages/about/about.component'; +import { Contact } from '../pages/contact/contact'; +import { Home } from '../pages/home/home'; -export const routes: Routes = []; +export const routes: Routes = [ + { path: '', redirectTo: 'home', pathMatch: 'full' }, + { path: 'home', component: Home }, + { path: 'about', component: AboutComponent }, + { path: 'contact', component: Contact }, +]; diff --git a/src/app/app.spec.ts b/src/app/app.spec.ts index d72d883..096b967 100644 --- a/src/app/app.spec.ts +++ b/src/app/app.spec.ts @@ -1,10 +1,12 @@ import { TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; import { App } from './app'; describe('App', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [App], + providers: [provideRouter([])], }).compileComponents(); }); @@ -14,10 +16,13 @@ describe('App', () => { expect(app).toBeTruthy(); }); - it('should render title', async () => { + it('should render the side navigation shell', async () => { const fixture = TestBed.createComponent(App); await fixture.whenStable(); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('h1')?.textContent).toContain('Hello, website'); + expect(compiled.querySelector('.sidebar')).toBeTruthy(); + expect(compiled.querySelectorAll('.nav a').length).toBeGreaterThanOrEqual(4); }); }); diff --git a/src/app/app.ts b/src/app/app.ts index b365ecc..2675a3a 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,12 +1,10 @@ -import { Component, signal } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; +import { Component } from '@angular/core'; +import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; @Component({ selector: 'app-root', - imports: [RouterOutlet], + imports: [RouterLink, RouterLinkActive, RouterOutlet], templateUrl: './app.html', styleUrl: './app.css' }) -export class App { - protected readonly title = signal('website'); -} +export class App {} diff --git a/src/components/card/card.css b/src/components/card/card.css new file mode 100644 index 0000000..b44d6b8 --- /dev/null +++ b/src/components/card/card.css @@ -0,0 +1,11 @@ +:host { + display: block; +} + +:host(.card) { + display: block; + padding: 1.2rem; + border: 1px solid #30363d; + border-radius: 14px; + background: #161b22; +} diff --git a/src/components/card/card.html b/src/components/card/card.html new file mode 100644 index 0000000..9af4366 --- /dev/null +++ b/src/components/card/card.html @@ -0,0 +1,2 @@ + + diff --git a/src/components/card/card.ts b/src/components/card/card.ts new file mode 100644 index 0000000..3da19d0 --- /dev/null +++ b/src/components/card/card.ts @@ -0,0 +1,19 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-card', + imports: [], + templateUrl: './card.html', + styleUrl: './card.css', + host: { + '[class]': 'hostClassName', + }, +}) +export class CardComponent { + @Input() cardClass = ''; + + get hostClassName(): string { + return this.cardClass ? `card ${this.cardClass}` : 'card'; + } +} + diff --git a/src/pages/about/about.component.ts b/src/pages/about/about.component.ts new file mode 100644 index 0000000..80f1f19 --- /dev/null +++ b/src/pages/about/about.component.ts @@ -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.', + ]; +} diff --git a/src/pages/about/about.css b/src/pages/about/about.css new file mode 100644 index 0000000..51e421d --- /dev/null +++ b/src/pages/about/about.css @@ -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; + } +} diff --git a/src/pages/about/about.html b/src/pages/about/about.html new file mode 100644 index 0000000..06f3202 --- /dev/null +++ b/src/pages/about/about.html @@ -0,0 +1,36 @@ +
+
+

About

+

Fullstack developer crafting modern web, backend, and mobile products.

+

+ I am Alex Carter, and I enjoy building software that feels clean, fast, and reliable from + the first screen to the last API call. +

+
+ +
+
+

What I bring

+

Core strengths across the stack.

+
+
+ @for (pillar of pillars; track pillar.title) { + +

{{ pillar.title }}

+

{{ pillar.description }}

+
+ } +
+
+ +
+ +

How I work

+
    + @for (value of values; track value) { +
  • {{ value }}
  • + } +
+
+
+
diff --git a/src/pages/about/about.spec.ts b/src/pages/about/about.spec.ts new file mode 100644 index 0000000..d21e6d0 --- /dev/null +++ b/src/pages/about/about.spec.ts @@ -0,0 +1,29 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AboutComponent } from './about.component'; + +describe('About', () => { + let component: AboutComponent; + let fixture: ComponentFixture; + + 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); + }); +}); diff --git a/src/pages/about/about.ts b/src/pages/about/about.ts new file mode 100644 index 0000000..08e0021 --- /dev/null +++ b/src/pages/about/about.ts @@ -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 {}; + diff --git a/src/pages/contact/contact.css b/src/pages/contact/contact.css new file mode 100644 index 0000000..64e7b89 --- /dev/null +++ b/src/pages/contact/contact.css @@ -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; + } +} diff --git a/src/pages/contact/contact.html b/src/pages/contact/contact.html new file mode 100644 index 0000000..bb06296 --- /dev/null +++ b/src/pages/contact/contact.html @@ -0,0 +1,35 @@ +
+
+

Contact

+

Let’s build something polished, useful, and fast.

+

+ If you have a product idea, an open role, or an app that needs a reliable fullstack hand, + I’d be happy to talk. +

+
+ +
+
+ @for (channel of channels; track channel.label) { + +

{{ channel.label }}

+ @if (channel.href) { + {{ channel.value }} + } @else { +

{{ channel.value }}

+ } +
+ } +
+
+ +
+ +

Preferred first step

+

+ Send a short message with your goals, timeline, and the stack you’re using. I’ll reply + with a clear next step. +

+
+
+
diff --git a/src/pages/contact/contact.spec.ts b/src/pages/contact/contact.spec.ts new file mode 100644 index 0000000..bc47e17 --- /dev/null +++ b/src/pages/contact/contact.spec.ts @@ -0,0 +1,30 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Contact } from './contact'; + +describe('Contact', () => { + let component: Contact; + let fixture: ComponentFixture; + + 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); + }); +}); + diff --git a/src/pages/contact/contact.ts b/src/pages/contact/contact.ts new file mode 100644 index 0000000..5478277 --- /dev/null +++ b/src/pages/contact/contact.ts @@ -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', + }, + ]; +} diff --git a/src/pages/home/home.css b/src/pages/home/home.css new file mode 100644 index 0000000..74782e0 --- /dev/null +++ b/src/pages/home/home.css @@ -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; + } +} diff --git a/src/pages/home/home.html b/src/pages/home/home.html new file mode 100644 index 0000000..4dc2cc7 --- /dev/null +++ b/src/pages/home/home.html @@ -0,0 +1,68 @@ +
+
+

Available for full-time and freelance work

+

{{ hero.name }}

+

{{ hero.role }}

+

{{ hero.intro }}

+

{{ hero.focus }}

+ +
+ +
+
+

Tech Stack

+

Primary technologies I use to deliver production-ready software.

+
+
+ @for (skill of skills; track skill.name) { + +

{{ skill.name }}

+

{{ skill.category }} - {{ skill.level }}

+
+ } +
+
+ +
+
+

Featured Projects

+

Recent work across web platforms, APIs, and mobile apps.

+
+
+ @for (project of projects; track project.title) { + +

{{ project.title }}

+

{{ project.description }}

+

{{ project.stack.join(' - ') }}

+

{{ project.impact }}

+
+ } +
+
+ +
+

Experience

+
+ @for (item of timeline; track item.role + item.company) { +
+
+

{{ item.role }}

+

{{ item.company }} - {{ item.period }}

+
+
    + @for (highlight of item.highlights; track highlight) { +
  • {{ highlight }}
  • + } +
+
+ } +
+
+ +
+

(c) {{ currentYear }} {{ hero.name }} - Fullstack Developer (Angular - C# - Flutter)

+
+
diff --git a/src/pages/home/home.spec.ts b/src/pages/home/home.spec.ts new file mode 100644 index 0000000..566bdf5 --- /dev/null +++ b/src/pages/home/home.spec.ts @@ -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; + + 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); + }); +}); diff --git a/src/pages/home/home.ts b/src/pages/home/home.ts new file mode 100644 index 0000000..0d38646 --- /dev/null +++ b/src/pages/home/home.ts @@ -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.', + ], + }, + ]; +} diff --git a/src/styles.css b/src/styles.css index 90d4ee0..6b8179b 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1 +1,21 @@ /* You can add global styles to this file, and also import other style files */ + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + font-family: Inter, Segoe UI, Roboto, Helvetica, Arial, sans-serif; + background: #0d1117; + color: #e6edf3; + line-height: 1.6; +} + +a { + color: inherit; +}