Osobisty CRM do śledzenia aplikacji o pracę — zastępuje Google Sheets, dodaje automatyczne przypomnienia o follow-upach, integracje z job boardami i powiadomienia email.
Projekt portfolio: Angular 21 + Java 21 / Spring Boot 3, własny design system, multi-user z JWT, deployowany na Render/Vercel. Zgodny z WCAG 2.1 AA — dostępność cyfrowa zaprojektowana od pierwszego commita, nie doklejana w trakcie audytu (wymóg European Accessibility Act 2025 / polskiej ustawy o dostępności cyfrowej 2019).
- Landing: https://frontend-beige-omega-33.vercel.app/landing
- Login / Register: https://frontend-beige-omega-33.vercel.app/login
- App (po zalogowaniu): https://frontend-beige-omega-33.vercel.app/dashboard
- Backend (Render): https://job-crm-backend.onrender.com
- Swagger UI: https://frontend-beige-omega-33.vercel.app/swagger-ui.html
⚠️ Backend hostowany na Render free tier — usypia po 15 min bezruchu, więc pierwsze zapytanie po przerwie może zająć 30-50 s (cold start). Zarejestruj nowe konto na/registerżeby przetestować pełny flow.
| Ścieżka | Strona | Status |
|---|---|---|
/ |
przekierowuje na /landing |
public |
/landing |
Marketing — hero z preview dashboardu, feature grid, pricing, testimonials | public |
/login, /register |
Auth split-screen (formularz + visual column z preview cards) | public |
/dashboard |
KPI z sparkline, kanban / lejek / lista, A/B CV, najbliższe wydarzenia, follow-upy, job boardy | authed |
/applications |
Pełna tabela aplikacji z filtrami | authed |
/job-board |
Crawler JJIT / NFJ + match score | authed |
/follow-up |
Skrzynka follow-upów do wysłania | authed |
/settings |
Ustawienia użytkownika (i18n, powiadomienia) | authed |
- Motywacja
- Stack technologiczny
- Struktura repo
- Model domeny
- REST API
- Dostępność cyfrowa (WCAG 2.1 AA)
- Fazowanie
- Uruchomienie lokalnie
- Deployment
- Konwencje
Trzymanie aplikacji w Google Sheets ma problemy:
- brak przypomnień o follow-upach → o aplikacjach się zapomina
- statusy się nie zgadzają, bo trzeba je ręcznie aktualizować
- brak integracji z mailami i ofertami pracy
- brak metryk (ile aplikacji / tydzień, response rate, średni czas do odpowiedzi)
CRM rozwiązuje to przez:
- centralizację w jednej aplikacji webowej (PWA-ready)
- scheduled jobs które przypominają o zaległych follow-upach
- auto-import ofert z JustJoinIT / NoFluffJobs (LinkedIn pomijamy — agresywnie blokuje scrapery i to ToS violation)
- email notifications (Spring Mail + SMTP)
- dashboard ze statystykami
| Komponent | Wybór | Uzasadnienie |
|---|---|---|
| Język | Java 21 (LTS) | Najnowszy LTS, pattern matching, records, virtual threads |
| Framework | Spring Boot 3.x | Standard branżowy, najwięcej ogłoszeń o pracę |
| Build | Maven | Częstsze w ogłoszeniach niż Gradle |
| ORM | Spring Data JPA + Hibernate | Standard Spring |
| Migracje | Flyway | Wersjonowanie schematu, baseline-friendly |
| Auth | Spring Security + JWT (jjwt) | Stateless, łatwy do skalowania |
| Validation | jakarta.validation | Wbudowane w Spring Boot 3 |
| API docs | springdoc-openapi | Auto-generowany Swagger UI z adnotacji |
| Spring Mail (JavaMailSender) | Dev: Mailtrap, prod: SMTP (Resend / SendGrid free tier) | |
| Scheduling | Spring @Scheduled |
Follow-up reminders, crawlery job boardów |
| HTTP client | Spring WebClient / RestClient |
Wywołania API job boardów |
| Testy | JUnit 5, Mockito, AssertJ, Testcontainers | Postgres w Dockerze do testów integracyjnych |
| Komponent | Wybór | Uzasadnienie |
|---|---|---|
| Framework | Angular 21 | Najnowsza wersja, signals, standalone components, control flow |
| Monorepo | Nx | Apps + libs w jednym repo, świetnie do CV |
| Design system | Własny na @angular/cdk |
A11y first, headless, reużywalny, portfolio piece |
| Komponenty UI | libs/design-system |
Button, Input, Select, Modal, Table, Card, Toast, ... |
| State | Signals + RxJS | Nowy idiom Angulara, mniejszy boilerplate niż NgRx |
| HTTP client | Auto-generowany z OpenAPI (@openapitools/openapi-generator-cli) |
Typowane DTO + serwisy z backendu |
| Forms | Reactive Forms | Standard Angulara |
| Testy unit | Jest | Szybsze niż Karma, lepsze DX |
| Testy e2e | Playwright | Nowoczesny standard, multi-browser |
| Storybook | tak | Podgląd komponentów design systemu |
| Komponent | Wybór |
|---|---|
| DB | PostgreSQL 16 |
| Lokalnie | Docker Compose (postgres + backend + frontend) |
| CI/CD | GitHub Actions (build + test + lint na każdy PR) |
| Deploy backend | Render.com (free tier) |
| Deploy frontend | Vercel (free tier) |
| DB prod | Render Postgres (free tier) |
| Mail prod | Resend (free tier 3k/mc) |
| Monitoring | Spring Actuator + Render metrics (na razie wystarczy) |
crm/
├── backend/ # Java 21 / Spring Boot 3 / Maven
│ ├── pom.xml
│ ├── src/main/java/pl/dmod/crm/
│ │ ├── CrmApplication.java
│ │ ├── application/ # Bounded context: aplikacje o pracę
│ │ │ ├── api/ # REST controllers + DTO
│ │ │ ├── domain/ # Encje JPA, value objects
│ │ │ ├── service/ # Logika biznesowa
│ │ │ └── repository/ # Spring Data repos
│ │ ├── auth/ # Bounded context: użytkownicy, JWT
│ │ ├── followup/ # Bounded context: przypomnienia + scheduler
│ │ ├── jobboard/ # Bounded context: integracje z JJIT/NFJ
│ │ ├── notification/ # Bounded context: email
│ │ └── shared/ # Wspólne: ExceptionHandler, config, security
│ ├── src/main/resources/
│ │ ├── application.yml
│ │ ├── application-dev.yml
│ │ ├── application-prod.yml
│ │ └── db/migration/ # Flyway: V1__init.sql, V2__...
│ └── src/test/... # JUnit + Testcontainers
│
├── frontend/ # Nx workspace, Angular 21
│ ├── nx.json
│ ├── package.json
│ ├── apps/
│ │ └── crm/ # Główna aplikacja
│ │ ├── src/app/
│ │ │ ├── features/ # applications/, auth/, dashboard/, settings/
│ │ │ ├── core/ # interceptors, guards, services
│ │ │ └── shared/ # małe helpery specyficzne dla app
│ │ └── src/styles/
│ ├── libs/
│ │ ├── design-system/ # ds-button, ds-input, ds-modal, ...
│ │ │ └── src/lib/components/
│ │ └── api-client/ # auto-generated z OpenAPI
│ └── .storybook/ # Storybook dla design-system
│
├── docker-compose.yml # postgres + backend + frontend (dev)
├── docker-compose.prod.yml # bez baz danych, tylko aplikacja
├── .github/workflows/
│ ├── backend-ci.yml
│ ├── frontend-ci.yml
│ └── deploy.yml
└── README.md
User
├── id (UUID)
├── email (unique)
├── passwordHash
├── displayName
├── createdAt
└── settings (1:1 UserSettings)
UserSettings
├── userId
├── followUpDefaultDays (default 7)
├── emailNotificationsEnabled
├── preferredJobLocations (text[])
├── preferredTechnologies (text[])
└── timezone
Application # Aplikacja o pracę
├── id (UUID)
├── userId (FK)
├── companyName
├── position
├── jobUrl (nullable)
├── source (enum: LINKEDIN, JUSTJOIN, NOFLUFF, REFERRAL, COMPANY_SITE, OTHER)
├── salaryMin / salaryMax / currency (nullable)
├── location / remote (enum)
├── appliedAt (timestamp)
├── currentStatus (enum) # denormalizacja dla szybkiego query
├── nextFollowUpAt (nullable)
├── archived (boolean)
├── notes (text)
└── tags (text[])
ApplicationStatus (enum)
DRAFT, APPLIED, ACK_RECEIVED, INTERVIEW_SCHEDULED, INTERVIEW_DONE,
TASK_RECEIVED, TASK_SUBMITTED, OFFER, REJECTED, WITHDRAWN, GHOSTED
StatusChange # Historia zmian statusu (audit)
├── id
├── applicationId (FK)
├── fromStatus / toStatus
├── changedAt
└── note
FollowUp # Zaplanowane / wysłane follow-upy
├── id
├── applicationId (FK)
├── scheduledAt
├── sentAt (nullable)
├── kind (enum: REMINDER, EMAIL_DRAFT)
└── content
Contact # Rekruter / hiring manager
├── id
├── applicationId (FK)
├── name
├── email
├── linkedinUrl
└── role
JobOffer # Z crawlerów job boardów
├── id
├── externalId
├── source (enum)
├── title / company / location
├── salaryMin / salaryMax
├── url
├── description
├── fetchedAt
├── matchScore (calculated)
└── userApplied (boolean)
Wszystko pod /api/v1. Auth: Authorization: Bearer <jwt>.
POST /api/v1/auth/register
POST /api/v1/auth/login
POST /api/v1/auth/refresh
GET /api/v1/auth/me
GET /api/v1/applications # list + filter (status, tag, search, dateRange)
POST /api/v1/applications
GET /api/v1/applications/{id}
PATCH /api/v1/applications/{id}
DELETE /api/v1/applications/{id}
POST /api/v1/applications/{id}/status # zmiana statusu (tworzy StatusChange)
GET /api/v1/applications/{id}/timeline
GET /api/v1/followups/upcoming # to do dziś / w najbliższych dniach
POST /api/v1/followups/{id}/done
POST /api/v1/followups/{id}/snooze
GET /api/v1/joboffers # zacrawlowane oferty z JJIT/NFJ
POST /api/v1/joboffers/{id}/save-as-application
GET /api/v1/stats/dashboard # liczby, response rate, średnie czasy
GET /api/v1/stats/funnel # lejek: applied → interview → offer
GET /api/v1/settings
PATCH /api/v1/settings
Swagger UI: http://localhost:8080/swagger-ui.html
Projekt celuje w WCAG 2.1 AA — zgodność z polską ustawą o dostępności cyfrowej (2019) i European Accessibility Act (obowiązujący od czerwca 2025). Dostępność nie jest checkboxem na końcu sprintu, tylko architekturą — wymuszoną przez lintera i CI.
| Zasada | Implementacja |
|---|---|
| Postrzegalność | Kontrast ≥ 4.5:1 dla body text (przycisk --brand-600 = 4.88:1 zamiast --brand-500 = 3.36:1); status nigdy przez sam kolor — badge + tekst; prefers-reduced-motion: reduce w globalnym _base.scss |
| Funkcjonalność | Skip link Przejdź do treści → <main id="main-content" tabindex="-1">; programowy main.focus() po NavigationEnd (Angular Router nie robi tego sam); cdkTrapFocus [cdkTrapFocusAutoCapture]="true" we wszystkich modalach; każdy interaktywny element to <button> lub <a>, nigdy <div (click)> |
| Zrozumiałość | Etykiety przez <label for>, błędy po polsku w role="alert", i18n PL/EN, spójne wzorce między podstronami |
| Solidność | Semantyczny HTML, atrybuty aria-* po stronie DS, atrybuty walidowane przez ESLint |
LiveAnnouncer—DsToastService.show()wołaannounce(spoken, politeness)(assertive dla błędów/ostrzeżeń, polite dla reszty). Toast jest jednym źródłem komunikatu dla widzących i SR-userów; brak dublowania z DOMaria-live.cdkTrapFocus— wszystkie modale (DsModalprzez host directive + inline modale dashboardu, applications, follow-up). Tab nie ucieka pod modal, focus wraca na trigger po zamknięciu..sr-only(clip pattern) —RouteAnnouncerComponentmirrujePageTitleService.title()doaria-live="polite" role="status", więc SR słyszy zmianę strony w SPA.:focus-visible— globalny focus ring w_base.scss, widoczny tylko dla użytkowników klawiatury.
DsInput i DsTextarea automatycznie ustawiają:
aria-requiredzrequired()signalaria-invalidzshowError()(dirty/touched + invalid)aria-describedbylinkujące input z error-messagem- error wrapper ma
role="alert", więc SR usłyszy walidację natychmiast - toggle "pokaż hasło" niesie
aria-pressed
| Narzędzie | Czego pilnuje | Plik |
|---|---|---|
@angular-eslint template a11y rules (10 reguł promoted to error) |
brak alt, źle podpięte labels, tabindex > 0, <div (click)>, nieprawidłowe aria-*, brak keyboard events przy click |
apps/crm/eslint.config.mjs |
@axe-core/playwright |
runtime axe scan publicznych routes — fail na critical / serious |
apps/crm-e2e/src/a11y.spec.ts |
npx nx lint crm # template a11y rules
npx nx e2e crm-e2e # axe-core przeciw running app--text-3(#8A857Cna cream) = 3.35:1 — poniżej AA dla body text. Używać tylko dla large text (≥ 14pt bold / 18pt regular) lub dekoracji niesłużącej treści. Dla właściwych tekstów:--text-1(primary) lub--text-2(7.9:1).
Pełny checklist wzorców + uzasadnienia: CLAUDE.md sekcja Accessibility — WCAG AA.
Każda faza to osobny commit / PR. Po każdej fazie aplikacja jest w działającym stanie.
| # | Faza | Co | Spring/Angular learning |
|---|---|---|---|
| 1 | Skeleton + Docker Compose | Pusty Spring Boot z healthcheckiem, pusty Nx workspace, Postgres w compose | Spring Boot starter, application.yml, profile |
| 2 | Design system v0 | Storybook + ds-button, ds-input, ds-select, ds-card, ds-modal, ds-table |
Angular CDK, content projection, signals, a11y |
| 3 | Backend CRUD Application | Encja Application, repo, service, controller, DTO, Flyway migration, OpenAPI, testy |
JPA, Spring Data, validation, MapStruct, Testcontainers |
| 4 | Frontend CRUD Application | Lista z filtrami, formularz, używa design system, OpenAPI-generated client | RxJS, Reactive Forms, Angular Router |
| 5 | Auth multi-user | Rejestracja, login, JWT, guard, @PreAuthorize, każdy user widzi swoje |
Spring Security, JwtAuthenticationFilter, BCrypt |
| 6 | Follow-up reminders + email | nextFollowUpAt, @Scheduled co 5 min, JavaMailSender, dashboard "do zrobienia" |
@Scheduled, Spring Mail, transakcje |
| 7 | Job board integration | Crawler JustJoinIT + NoFluffJobs, scheduled, "znalezione oferty" w UI | WebClient, retry, rate limiting, mapping |
| 8 | Stats + dashboard | Lejek, response rate, wykresy | Aggregation queries, JPQL/native |
| 9 | CI/CD + deploy | GitHub Actions, Render/Vercel, README z linkiem do live demo | DevOps, profile prod, secrets |
| 10 | Polish | i18n (PL/EN), PWA, dark mode, accessibility audit | Angular i18n, service worker |
# 1. Klonowanie
git clone <repo-url>
cd crm
# 2. Postgres + backend + frontend w Dockerze
docker compose up -d
# 3. Otwórz w przeglądarce
open http://localhost:4200 # frontend (Angular)
open http://localhost:8080/swagger-ui.html # backend API docs
# 4. Backend osobno (do nauki Springa)
cd backend
./mvnw spring-boot:run
# 5. Frontend osobno
cd frontend
npm install
npx nx serve crmWymagania: Docker, Java 21, Node 20+, pnpm/npm.
| Komponent | Hosting | URL | Config |
|---|---|---|---|
| Backend | Render — free tier (sleeps po 15 min) | https://job-crm-backend.onrender.com | render.yaml |
| Frontend | Vercel — Hobby | https://frontend-beige-omega-33.vercel.app | frontend/vercel.json |
| Database | Render Postgres free (256 MB) | (internal) | declared w render.yaml |
| Resend / Mailtrap (opcjonalne) | — | env vars MAIL_* |
| Workflow | Triggers | Co robi |
|---|---|---|
.github/workflows/backend-ci.yml |
push/PR z backend/** |
mvn verify na Ubuntu, Java 21, Testcontainers (Docker w runnerze) |
.github/workflows/frontend-ci.yml |
push/PR z frontend/** |
npm ci, nx lint, nx test, nx build crm na Node 22 |
- Render —
https://dashboard.render.com/blueprints→ New blueprint → wskaż na repo. Render czytarender.yaml, tworzy: web servicejob-crm-backend(Docker) + Postgresjob-crm-db. Auto-deploy zmainjest włączony. - Render — wypełnij env vars w UI dla
MAIL_HOST/MAIL_USERNAME/MAIL_PASSWORD(zostały oznaczonesync: falseżeby nie commitować sekretów).JWT_SECRETRender generuje sam (generateValue: true). - Backend URL — sprawdź adres w Render dashboard (np.
https://job-crm-backend.onrender.com). Jeśli inny — zaktualizujrewriteswfrontend/vercel.jsoni pushnij. - Vercel —
vercel.com/new→ import repo. Root:frontend/. Framework: Other. Build/output/install jest wvercel.json, więc Vercel sam wykryje. - Czekasz na pierwszy deploy, podajesz adres w CV.
- Render Web Service na free tier zasypia po 15 minutach bezruchu. Pierwsze zapytanie po przerwie = ~30 s wake-up.
- Postgres free tier wygasa po 90 dniach od utworzenia bazy — zrób backup przed deadline.
- Crawler JJIT/NFJ co 6h wywołuje outbound HTTP. Wyłącz przez
JOBBOARD_ENABLED=falsejeśli chcesz oszczędzać CPU.
- Code in English (encje, zmienne, komentarze, commit messages, branch names)
- UI in Polish (planowane i18n PL/EN w fazie 10)
- Commits: Conventional Commits (
feat:,fix:,chore:,docs:, ...) - Branche:
feature/<short-name>,fix/<short-name> - Jeden PR per faza (małe, możliwe do review w 10 minut)
- Testy obowiązkowe dla service layer + controllerów (>70% coverage backendu)
- A11y first — WCAG 2.1 AA wymuszone przez ESLint (10 reguł a11y → error) + axe-core w e2e.
@angular/cdk/a11y(LiveAnnouncer,cdkTrapFocus,.sr-only,:focus-visible) zamiast ręcznych ARIA-hacków. Programowy focus<main>po nawigacji, route announcer dla SR, kontrast ≥ 4.5:1 dla body text. Szczegóły w sekcji Dostępność cyfrowa iCLAUDE.md.
- Faza 1: Skeleton + Docker Compose
- Faza 2: Design system v0 (21 komponentów Angular CDK, SASS abstracts, signal-based forms, ag-grid)
- Faza 3: Backend CRUD Application (JPA, Flyway, OpenAPI, Testcontainers, ProblemDetail)
- Faza 4: Auth multi-user + JWT (Spring Security stateless, BCrypt, per-user data isolation, login/register UI)
- Faza 5: Follow-up reminders + email (@EnableScheduling, JavaMailSender → MailHog dev, /api/v1/follow-ups, dashboard widget)
- Faza 6: Job board integration (Spring RestClient → JustJoinIT + NoFluffJobs, 6h crawler, dedup, save-as-application, ag-grid UI)
- Faza 7: Stats + dashboard (
/api/v1/stats/dashboardagregacje, custom SVG funnel + weekly chart + status bars) - Faza 8: CI/CD + deploy (GitHub Actions backend-ci + frontend-ci, render.yaml blueprint, vercel.json z /api rewrites, application-prod.yml)
- Faza 9: Polish — i18n PL/EN (signal-based
I18nService+trpipe), PWA (manifest + service worker, cache-first dla assets / network-first dla/api), a11y WCAG 2.1 AA (@axe-core/playwrighte2e + 10 ESLint template-a11y rules promoted to error,@angular/cdk/a11yprimitives:LiveAnnouncer/cdkTrapFocus/.sr-only/:focus-visible, skip link + programowy focus<main>poNavigationEnd, route announcer w polite live region) - Faza 10: Brand refresh — Claude Design "jobflow" handoff zaimplementowany 1:1 (paleta terracotta, Instrument Serif + Geist, sidebar/topbar redesign, dashboard z KPI sparkline / kanban / A/B CV / kalendarz wydarzeń, split-screen auth, public landing page na
/landingz hero preview + feature grid + pricing). Pixel-diff ~3% (font sub-pixel rendering)
Repo zawiera kompletny stack od pustego folderu do production-ready aplikacji:
- Backend: Spring Boot 3.4 / Java 21 — JPA + Flyway (6 migracji), Spring Security stateless + JWT, Spring Mail (
@Scheduledreminders), Spring RestClient (crawler JJIT/NFJ), springdoc OpenAPI, ProblemDetail, Testcontainers, integration + service tests - Frontend: Angular 21 zoneless / Nx monorepo — własny design system z 21 komponentami (SASS abstracts + signal forms + Angular CDK overlay), AG Grid dla data tables, signal-based state, custom SVG dashboard charts (KPI sparkline, kanban z drag&drop-ready strukturą, A/B CV bars), i18n PL/EN, PWA (offline). Pełny brand-refresh wg Claude Design "jobflow" (paleta terracotta, Instrument Serif + Geist + JetBrains Mono, split-screen auth, public landing z marketing copy)
- DevOps: Docker Compose (postgres + backend + frontend + mailhog), GitHub Actions CI (dwa workflowy), Render blueprint + Vercel rewrites = zero CORS w prod
- Faza 10: rate limiting (
bucket4j), refresh tokens, password reset flow - Faza 11: Storybook dla
libs/design-system+ visual regression - Faza 12: monitoring — Prometheus + Grafana + Sentry dla frontend errors
- Faza 13: AI integration — auto-summarize job offers, suggest follow-up timing
- Faza 14: mobile — Capacitor wrap albo Flutter rewrite