← Tillbaka till bloggen
Dynamic Storytelling: Building an Interactive Timeline
Dynamic Storytelling: Building an Interactive Timeline
The Problem with Static CVs
Traditional CVs are:
- Outdated the moment you finish them
- One-size-fits-all (same document for every application)
- Boring (walls of text nobody reads)
- Hard to update (PDF hell)
I wanted something different. Something alive.
The Vision: A Living Story
Instead of a PDF that collects dust, I built a dynamic journey timeline that:
- Updates in real-time
- Shows my progression visually
- Lets me add new milestones instantly
- Tells a story, not just lists facts
The Architecture
Database Schema
interface JourneyEntry {
id: number;
type: 'education' | 'work' | 'project' | 'achievement';
title: string;
organization: string;
description: string;
startDate: Date;
endDate?: Date; // null = "Present"
technologies: string[];
orderIndex: number;
}
Admin Panel for Real-Time Editing
// src/lib/actions/journey.ts
'use server';
export async function createJourneyEntry(data: JourneyFormData) {
const session = await getSession();
if (!session.isAuthenticated) redirect('/auth/signin');
const maxOrder = await db
.selectFrom('journeyEntries')
.select((eb) => eb.fn.max('orderIndex').as('maxOrder'))
.executeTakeFirst();
const newEntry = await db
.insertInto('journeyEntries')
.values({
...data,
orderIndex: (maxOrder?.maxOrder || 0) + 1,
})
.returningAll()
.executeTakeFirst();
revalidatePath('/journey');
return newEntry;
}
Visual Timeline Component
The timeline uses a vertical layout with clear visual separation:
<div className="relative border-l-2 border-primary pl-6">
{entries.map((entry) => (
<div key={entry.id} className="mb-8 relative">
{/* Timeline dot */}
<div className="absolute -left-[1.6rem] top-2 w-4 h-4 rounded-full bg-primary border-4 border-background" />
{/* Content card */}
<Card>
<h3>{entry.title}</h3>
<p className="text-muted-foreground">{entry.organization}</p>
<div className="flex gap-2 mt-2">
{entry.technologies.map(tech => (
<Badge key={tech}>{tech}</Badge>
))}
</div>
</Card>
</div>
))}
</div>
Smart Date Formatting
function formatDateRange(start: Date, end?: Date) {
const formatter = new Intl.DateTimeFormat('sv-SE', {
year: 'numeric',
month: 'short',
});
const startStr = formatter.format(start);
const endStr = end ? formatter.format(end) : 'Present';
return `${startStr} - ${endStr}`;
}
Why This Matters
For Visitors
- Engaging: Visual timeline > wall of text
- Scannable: See progression at a glance
- Current: Always up-to-date
For Me
- Fast updates: Add new entry in 30 seconds
- Flexible: Reorder, edit, delete anytime
- Portfolio integration: Same database powers multiple views
For Employers
- Credibility: Live system = I actually build things
- Transparency: See my tech stack choices
- Story: Understand my journey, not just jobs
Admin Features
Drag-and-Drop Reordering
async function updateOrder(entries: Array<{id: number, orderIndex: number}>) {
await Promise.all(
entries.map(entry =>
db.updateTable('journeyEntries')
.set({ orderIndex: entry.orderIndex })
.where('id', '=', entry.id)
.execute()
)
);
revalidatePath('/journey');
}
Bulk Actions
- Archive old entries
- Highlight featured items
- Export to PDF (for traditional applications)
The Bio Connection
The journey timeline connects with dynamic bio section:
interface Bio {
id: number;
name: string;
title: string;
description: string;
skills: string[];
values: string[];
contactEmail: string;
avatarUrl?: string;
}
Same pattern:
- Editable via admin panel
- Stored in database
- Type-safe with Kysely
- Server actions for mutations
Tech Stack Benefits
TypeScript everywhere means I can't mess up field names
Server Actions eliminate API boilerplate
Kysely gives me SQL power with type safety
Revalidation updates pages instantly after edits
Use Cases Beyond Portfolio
This pattern works for:
- Company history timelines
- Product roadmaps
- Personal journals
- Project milestones
- Learning logs
Performance
- SSR: Timeline renders server-side
- Caching: 60s revalidation for public views
- No hydration delay: Pure HTML until interaction
- Small bundle: No heavy charting libraries
The Result
A portfolio section that:
- ✅ Updates in seconds, not hours
- ✅ Tells a story, not just facts
- ✅ Looks professional and modern
- ✅ Demonstrates full-stack skills
And when someone asks "Can you update X on your CV?" - I do it live during the call.
Database entries: Dynamic (currently 8+) Admin edit time: 30 seconds per entry Page load time: <100ms Maintenance: Zero