Welcome to part two of “Zero to SaaS in 14 Days” — my real-world series where I tackle building, launching, and documenting a SaaS product in just two weeks. In part one, I built a Subscription Tracker from scratch, fighting through deadline pressure and momentum swings to deliver a working MVP. Now the adventure ramps up.
This time, I’m deep-diving into the creation of a job application tracker, an idea sparked by my own battle with cluttered notes, endless follow-ups, and missed opportunities. As a senior Angular developer, I wanted to combine speed, best practices, and a little bit of chaos — all while learning fast and delivering faster.
Get ready for practical decisions, unconventional Angular tips, and a front-row seat to the drama and satisfaction of solo SaaS building. Whether you code in Angular or just want to see a product rise from nothing, you’re in the right place.
💡 The Spark: What Problem Was I Solving?
Like many of us on the job hunt, I was juggling multiple applications, each at a different stage, all scrambling for my attention. My notes lived in scattered spreadsheets and sticky notes, my brain a fog of due dates and follow-ups. One night after missing a deadline, I realized this chaos was a shared pain point.
I envisioned a clean, minimal app that could bring order to the frenzy — a dashboard tracking statuses, a simple drag-and-drop to move applications through stages, and gentle reminders nudging me when I needed to act. It was like crafting a personal assistant in code.
🔍 But Would Anyone Actually Use It?
Until now, this was my secret project. I hesitated to share it publicly, fearing judgment or failure. But the desire to connect and learn outweighed the fear. Now, I’m excited to present this MVP here — unvarnished and real. It’s a tool I built for myself, and now I invite you to try it, poke at it, and tell me honestly what you think.
By sharing here first, I hope to gather authentic feedback from fellow developers, job seekers, and curious minds. Your insights will help shape the MVP into a product worth sharing with more people. This moment marks a pivotal shift — from solo tinkering to community-driven growth.
💡 Ready to master your job search? Try the FREE ApplyTrack app and say goodbye to missed deadlines 📆, forgotten follow-ups 🛎️, and job hunt chaos 📋. Get organized and track your progress the easy way! Click to join the beta — a smarter career journey is just one signup away! 🚀✨
⚙️ Why Angular? The Tech Behind the Magic
Strong Foundation with Angular
As a senior developer, Angular felt like a trusted, capable partner. Its TypeScript foundation gave me confidence and clarity. Leveraging Angular Material components, I built a UI that felt polished and professional early on. Applying best practices in modularity and reactive programming, I crafted an app that is maintainable and robust, designed to grow beyond the MVP.
⚛️ Simple State Management with Angular Signals
I wanted to keep complexity low. Enter Angular Signals — an elegant way to manage reactive state natively in components. Signals allowed me to easily track application data and UI state, react instantly to changes, and keep the architecture clean without the overhead of Redux or other state libraries.
Here’s how I updated my application list reactively:
private addNewApplication(application: Omit<JobApplication, 'id'>): void {
const newApplication: JobApplication = {
...application,
id: this.generateId(),
};
// Add to the appropriate status array
switch (newApplication.status) {
case 'applied':
this.appliedApplications.update((apps) => [...apps, newApplication]);
break;
case 'interview':
this.interviewApplications.update((apps) => [...apps, newApplication]);
break;
case 'offer':
this.offerApplications.update((apps) => [...apps, newApplication]);
break;
case 'rejected':
this.rejectedApplications.update((apps) => [...apps, newApplication]);
break;
}
// Effects will automatically handle computed value updates
}
private updateExistingApplication(
applicationId: string,
updatedData: Omit<JobApplication, 'id'>
): void {
const updatedApplication: JobApplication = {
...updatedData,
id: applicationId,
};
// Find and remove from current array
const allArrays = [
this.appliedApplications,
this.interviewApplications,
this.offerApplications,
this.rejectedApplications,
];
for (const arraySignal of allArrays) {
const applications = arraySignal();
const index = applications.findIndex((app) => app.id === applicationId);
if (index !== -1) {
// Remove from current array
const updatedApps = applications.filter((app) => app.id !== applicationId);
arraySignal.set(updatedApps);
break;
}
}
// Add to the appropriate status array based on new status
switch (updatedApplication.status) {
case 'applied':
this.appliedApplications.update((apps) => [...apps, updatedApplication]);
break;
case 'interview':
this.interviewApplications.update((apps) => [...apps, updatedApplication]);
break;
case 'offer':
this.offerApplications.update((apps) => [...apps, updatedApplication]);
break;
case 'rejected':
this.rejectedApplications.update((apps) => [...apps, updatedApplication]);
break;
}
// Effects will automatically handle computed value updates
}
This approach kept the app reactive and snappy, making the development process joyful instead of tedious.
Toolbox Highlights
- Angular Material for crafting a sleek and consistent UI
- Angular CDK Drag-and-Drop to bring fluid interactivity
- Reactive Forms to enforce sane, user-friendly data entry
- Angular Signals for intuitive, performant state control
🧩 Peek at the Code
Drag and Drop Status Update
One of the moments that made me smile was the drag-and-drop feature working for the first time — applications moved easily from one stage to another, and data stayed perfectly in sync:
drop(event: CdkDragDrop<JobApplication[]>) {
if (event.previousContainer === event.container) {
// Reordering within the same column - work with original arrays
const containerId = event.container.id;
const originalArray = this.getOriginalArrayFromContainerId(containerId);
// Find the actual indices in the original array
const draggedItem = event.container.data[event.previousIndex];
const actualPrevIndex = originalArray().findIndex(
(app: JobApplication) => app.id === draggedItem.id
);
// For reordering, we need to find where to insert in the original array
// This is complex with filtering, so for now we'll skip reordering when filters are active
if (this.searchTerm() === '' && this.selectedStatus() === 'all') {
moveItemInArray(originalArray(), actualPrevIndex, event.currentIndex);
this.updateFilteredApplications();
}
} else {
// Moving between columns - update the application status
const application = event.previousContainer.data[event.previousIndex];
const newStatus = this.getStatusFromContainerId(event.container.id);
const oldStatus = this.getStatusFromContainerId(event.previousContainer.id);
// Remove from old array
const oldArray = this.getOriginalArrayFromContainerId(event.previousContainer.id);
const oldApplications = oldArray().filter((app: JobApplication) => app.id !== application.id);
oldArray.set(oldApplications);
// Update application status and add to new array
const updatedApplication = { ...application, status: newStatus };
const newArray = this.getOriginalArrayFromContainerId(event.container.id);
const newApplications = [...newArray(), updatedApplication];
newArray.set(newApplications);
// Effects will automatically handle updates
}
}
Reactive Form Setup for Adding Applications
Building the add application modal was a lesson in balancing validation with user experience. Each form control felt like a conversation: “Tell me the company name,” “And the position title,” guiding the user while gently enforcing correctness.
readonly applicationForm = new FormGroup({
company: new FormControl(this.data?.application?.company || '', {
validators: [Validators.required, Validators.minLength(2)],
nonNullable: true,
}),
position: new FormControl(this.data?.application?.position || '', {
validators: [Validators.required, Validators.minLength(2)],
nonNullable: true,
}),
status: new FormControl<ApplicationStatus>(this.data?.application?.status || 'applied', {
validators: [Validators.required],
nonNullable: true,
}),
dateApplied: new FormControl(this.data?.application?.dateApplied || new Date(), {
validators: [Validators.required],
nonNullable: true,
}),
notes: new FormControl(this.data?.application?.notes || '', { nonNullable: true }),
reminderDate: new FormControl<Date | null>(this.data?.application?.reminderDate || null),
reminderType: new FormControl<'follow-up' | 'interview' | 'decision-deadline'>(
this.data?.application?.reminderType || 'follow-up',
{ nonNullable: true }
),
reminderDescription: new FormControl(this.data?.application?.reminderDescription || '', {
nonNullable: true,
}),
});
onSubmit(): void {
if (this.applicationForm.valid) {
const formValue = this.applicationForm.getRawValue();
const result: AddApplicationDialogResult = {
application: {
company: formValue.company,
position: formValue.position,
status: formValue.status,
dateApplied: formValue.dateApplied,
notes: formValue.notes || undefined,
reminderDate: formValue.reminderDate || undefined,
reminderType: formValue.reminderType,
reminderDescription: formValue.reminderDescription || undefined,
},
};
this.dialogRef.close(result);
}
}
Dashboard Stats With Angular Material Cards
Seeing stats update in real-time became a small daily delight — a tangible measure of progress on the job hunt amidst the chaos.
<div class="stats-container">
@for (let stat of statsList; track stat.id) {
<mat-card class="stat-card">
<mat-card-content>
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</mat-card-content>
</mat-card>
}
</div>
this.statsList = [
{ label: 'Total Applications', value: this.stats.total },
{ label: 'Applied', value: this.stats.applied },
{ label: 'Interviews', value: this.stats.interviews },
{ label: 'Offers', value: this.stats.offers },
{ label: 'Success Rate', value: this.stats.successRate + '%' }
];
Reminder Logic With Urgency
Late nights spent perfecting the reminder logic felt worth it when the app started nudging me ahead of deadlines, feeling like a thoughtful assistant:
private updateReminders() {
const currentDate = new Date();
const reminderWindow = 14; // 14 days ahead
const futureDate = new Date();
futureDate.setDate(currentDate.getDate() + reminderWindow);
const allApplications = this.getAllApplications();
const applicationsWithReminders = allApplications.filter(
(app) => app.reminderDate && app.reminderType && app.reminderDescription
);
const computedReminders: ComputedReminder[] = applicationsWithReminders.map((app) => {
const reminderDate = app.reminderDate!;
const timeDiff = reminderDate.getTime() - currentDate.getTime();
const daysUntilDue = Math.ceil(timeDiff / (1000 * 3600 * 24));
return {
applicationId: app.id,
company: app.company,
position: app.position,
type: app.reminderType!,
date: reminderDate,
description: app.reminderDescription!,
isOverdue: reminderDate < currentDate,
daysUntilDue,
};
});
// Separate overdue and upcoming reminders
const overdue = computedReminders
.filter((reminder) => reminder.isOverdue)
.sort((a, b) => b.date.getTime() - a.date.getTime()); // Most recent overdue first
const upcoming = computedReminders
.filter(
(reminder) =>
!reminder.isOverdue && reminder.date >= currentDate && reminder.date <= futureDate
)
.sort((a, b) => a.date.getTime() - b.date.getTime()); // Soonest first
this.overdueReminders.set(overdue);
this.upcomingReminders.set(upcoming);
}
✨ Want simple, powerful online tools that just work? Check out my apps! I build user-friendly software designed to save you money and make your life easier. 🚀 Sign up now and grab your FREE 10-Step Code Review Checklist 📋 - level up your projects today!
🗣️ Your Feedback Matters — Let’s Shape This Together!
Building this micro SaaS MVP has been an exhilarating journey full of learning and late-night coding sprints. Now that the app is shared here for the first time, I’m eager to hear your thoughts. Whether you’re a developer, job seeker, or just curious, your feedback will shape the app’s next chapters.
- Which features stand out?
- What could make the app even more helpful?
- How intuitive does it feel to you?
- Found any quirks or bugs?
Drop a comment below or reach out on X or LinkedIn. Your voices will guide future updates and help build something truly valuable together.
🏗️ Building, Breaking, Fixing: The Development Rollercoaster
Iteration 1: The Barebones MVP
Building core features was rewarding, but unexpected bugs taught me patience. One memorable night, I wrestled with a drag-and-drop reorder bug caused by filtered views. The fix was humble yet crucial: disabling reordering during filtering. A small patch, but a big sigh of relief.
Iteration 2: Powering Up Reminders and Stats
Adding reminders raised the bar. Dates, timezones, urgency — suddenly complexity exploded. I remember a caffeine-fueled debugging marathon that ended with finally squashing a timezone bug. Those moments define the startup grind: frustrating but ultimately satisfying.
🎓 Lessons from the Trenches
Technical Growth
Mastering Angular Signals revolutionized my state management approach. Complex date handling became a trusted friend instead of a foe.
Entrepreneurial Mindset
Owning every piece — from code to customer conversations — deepened my appreciation for user feedback and rapid iteration.
Balance and Focus
Sacrificing evenings and weekends was tough but necessary. Leveraging efficient tooling kept my momentum.
Validation
While formal validation is yet to come, early feedback and insights from this article’s readers will be invaluable. Hearing from you will help steer the product’s next steps, proving why every bug fixed and every feature crafted matters.
🎉 Wrapping Up: Your SaaS Adventure Awaits
This journey is a testament to persistence and passion. Whether starting or scaling, building a SaaS product is a wild ride packed with lessons and rewards. If you have an idea, start now. Validate fast, learn fast, iterate fast.
What’s your story? Comment below or connect on X/LinkedIn. And if you want to try the app, it’s live at https://apply-track.carrd.co/.
Let’s build something incredible together!
Thanks for Reading 🙌
I hope these tips help you ship better, faster, and more maintainable frontend projects.
🚀 Check Out My Portfolio
Discover SaaS products and digital solutions I've built, showcasing expertise in scalable architectures and modern frontend tech.
👉 View Portfolio
🛠 Explore My Developer Resources
Save time and level up your code reviews, architecture, and performance optimization with my premium Angular & frontend tools.
👉 Browse on Gumroad
💬 Let's Connect on LinkedIn
I share actionable insights on Angular & modern frontend development - plus behind‑the‑scenes tips from real‑world projects.
👉 Connect with me here
📣 Follow Me on X
Stay updated with quick frontend tips, Angular insights, and real-time updates - plus join conversations with other developers.
👉 Follow me on X
Your support fuels more guides, checklists, and tools for the frontend community.
Let's keep building together 🚀
Top comments (2)
Brilliant walkthrough, Karol — totally relate to the solo SaaS journey and those late-night “bug battles.” Angular Signals was a smart choice — that reactivity makes small apps feel enterprise-grade. Excited to try ApplyTrack and see how it evolves! 💡
Thanks! Those bug battles are part of the solo SaaS fun. Angular Signals keep things smooth without extra hassle. Can’t wait for you to try ApplyTrack and share your thoughts. If you want in, sign up for the beta here: karol-modelski.kit.com/55b9e7ffa2 — would love to have you onboard!