diff --git a/docs/decisions/ADR-004-e2e-test-strategy.md b/docs/decisions/ADR-004-e2e-test-strategy.md
index cf201b4..21c32d6 100644
--- a/docs/decisions/ADR-004-e2e-test-strategy.md
+++ b/docs/decisions/ADR-004-e2e-test-strategy.md
@@ -18,7 +18,7 @@ So the e2e suite stays at the level of "does the button call the right thing, do
State lives entirely in IndexedDB (Dexie); there is no server to load fixtures from. Two ways to get a CV into that state, picked per automated consumer by what it is actually checking.
-Editor tests build their CV through the real UI, because the editor is the thing under test there. Dashboard, export, tailoring tests, and the asset-generation harness seed through `window.__hhTest`, a bridge that runs the production constructors (`createCVFromTemplate`, `createDummyCV`, `createTailoredCV`, the `db`). Building a populated nine-block master by typing, only to set up a sync test or a screenshot, would be slow and would tie every subsequent step to editor stability — a change to a field input would break setups that have nothing to do with editing.
+Editor tests build their CV through the real UI, because the editor is the thing under test there. Dashboard, export, tailoring tests, and the asset-generation harness seed through `window.__hhTest`, a bridge that runs the production constructors (`createCV`, `createDummyCV`, `createTailoredCV`, the `db`). Building a populated nine-block master by typing, only to set up a sync test or a screenshot, would be slow and would tie every subsequent step to editor stability — a change to a field input would break setups that have nothing to do with editing.
The bridge runs production code rather than hand-rolled fixtures on purpose. A CV carries derived state — `blockHashes`, `syncBaseline`, `syncBaselineHashes` — that a raw fixture would have to reproduce and keep in step as the schema moves. Going through the real constructors keeps seeded data valid for free while the app is still changing shape.
diff --git a/e2e/editor.spec.ts b/e2e/editor.spec.ts
index 8ba5726..d55b752 100644
--- a/e2e/editor.spec.ts
+++ b/e2e/editor.spec.ts
@@ -70,10 +70,10 @@ test.describe('Editor', () => {
const masterId = await b.seedMaster();
await page.goto(`/cv/${masterId}`);
- // Dummy CV has 3 jobs: Nimbus Labs (0), Cobalt Financial (1), Bright Pixel Studio (2)
+ // Dummy CV has 3 jobs: Dunder Mifflin (0), Schrute Farms (1), Sheriff's Dept (2)
const entries = page.getByTestId('job-entry');
await expect(entries).toHaveCount(3);
- await expect(entries.nth(0)).toContainText('Nimbus Labs');
+ await expect(entries.nth(0)).toContainText('Dunder Mifflin');
// Drag first entry to the position of the third
await reorder(page, entries.nth(0).getByTestId('drag-handle'), entries.nth(2));
@@ -81,9 +81,9 @@ test.describe('Editor', () => {
await waitForAutosave(page);
await page.reload();
- // Nimbus Labs should now be at the end
+ // Dunder Mifflin should now be at the end
await expect(page.getByTestId('job-entry')).toHaveCount(3);
- await expect(page.getByTestId('job-entry').nth(0)).not.toContainText('Nimbus Labs');
- await expect(page.getByTestId('job-entry').nth(2)).toContainText('Nimbus Labs');
+ await expect(page.getByTestId('job-entry').nth(0)).not.toContainText('Dunder Mifflin');
+ await expect(page.getByTestId('job-entry').nth(2)).toContainText('Dunder Mifflin');
});
});
diff --git a/package.json b/package.json
index e4b7114..40db6f2 100644
--- a/package.json
+++ b/package.json
@@ -95,6 +95,7 @@
"@fontsource-variable/space-grotesk": "^5.2.10",
"dexie": "^4.3.0",
"fuse.js": "^7.3.0",
+ "nanoid": "^5.1.11",
"pdfjs-dist": "^5.7.284",
"pdfmake": "^0.3.8",
"posthog-js": "^1.376.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index af9bc78..ae5de29 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,6 +23,9 @@ importers:
fuse.js:
specifier: ^7.3.0
version: 7.4.0
+ nanoid:
+ specifier: ^5.1.11
+ version: 5.1.11
pdfjs-dist:
specifier: ^5.7.284
version: 5.7.284
@@ -2941,6 +2944,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ nanoid@5.1.11:
+ resolution: {integrity: sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==}
+ engines: {node: ^18 || >=20}
+ hasBin: true
+
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@@ -6244,6 +6252,8 @@ snapshots:
nanoid@3.3.12: {}
+ nanoid@5.1.11: {}
+
natural-compare@1.4.0: {}
nwsapi@2.2.23: {}
diff --git a/src/lib/components/blocks/contacts-block.svelte b/src/lib/components/blocks/contacts-block.svelte
index 94f3856..3f62092 100644
--- a/src/lib/components/blocks/contacts-block.svelte
+++ b/src/lib/components/blocks/contacts-block.svelte
@@ -6,8 +6,7 @@
import { DragDropProvider, DragOverlay } from '@dnd-kit/svelte';
import type { Draggable } from '@dnd-kit/dom';
import { Trash2, Plus, GripVertical } from '@lucide/svelte';
- import type { ContactEntry, ObjectId } from '$lib/types/cv';
- import { createObjectId } from '$lib/types/cv';
+ import { createObjectId, type ContactEntry, type ObjectId } from '$lib/types/cv';
interface Props {
contacts: ContactEntry[];
diff --git a/src/lib/components/blocks/education-block.svelte b/src/lib/components/blocks/education-block.svelte
index a2ab8a1..23e3320 100644
--- a/src/lib/components/blocks/education-block.svelte
+++ b/src/lib/components/blocks/education-block.svelte
@@ -8,8 +8,7 @@
import { DragDropProvider, DragOverlay } from '@dnd-kit/svelte';
import type { Draggable } from '@dnd-kit/dom';
import { Trash2, Plus, GripVertical } from '@lucide/svelte';
- import type { EducationEntry, ObjectId } from '$lib/types/cv';
- import { createObjectId } from '$lib/types/cv';
+ import { createObjectId, type EducationEntry, type ObjectId } from '$lib/types/cv';
interface Props {
education: EducationEntry[];
diff --git a/src/lib/components/blocks/job-history-block.svelte b/src/lib/components/blocks/job-history-block.svelte
index 893d195..b5c4435 100644
--- a/src/lib/components/blocks/job-history-block.svelte
+++ b/src/lib/components/blocks/job-history-block.svelte
@@ -10,8 +10,7 @@
import { DragDropProvider, DragOverlay } from '@dnd-kit/svelte';
import type { Draggable } from '@dnd-kit/dom';
import { Trash2, Plus, GripVertical } from '@lucide/svelte';
- import type { JobEntry, ObjectId } from '$lib/types/cv';
- import { createObjectId } from '$lib/types/cv';
+ import { createObjectId, type JobEntry, type ObjectId } from '$lib/types/cv';
interface Props {
jobs: JobEntry[];
diff --git a/src/lib/components/blocks/projects-block.svelte b/src/lib/components/blocks/projects-block.svelte
index 33d1fa2..6455e2b 100644
--- a/src/lib/components/blocks/projects-block.svelte
+++ b/src/lib/components/blocks/projects-block.svelte
@@ -8,8 +8,7 @@
import { DragDropProvider, DragOverlay } from '@dnd-kit/svelte';
import type { Draggable } from '@dnd-kit/dom';
import { Trash2, Plus, GripVertical } from '@lucide/svelte';
- import type { ProjectEntry, ObjectId } from '$lib/types/cv';
- import { createObjectId } from '$lib/types/cv';
+ import { createObjectId, type ProjectEntry, type ObjectId } from '$lib/types/cv';
interface Props {
projects: ProjectEntry[];
diff --git a/src/lib/components/blocks/skills-block.svelte b/src/lib/components/blocks/skills-block.svelte
index 8b7a450..5c5ba3c 100644
--- a/src/lib/components/blocks/skills-block.svelte
+++ b/src/lib/components/blocks/skills-block.svelte
@@ -7,8 +7,7 @@
import { DragDropProvider, DragOverlay } from '@dnd-kit/svelte';
import type { Draggable } from '@dnd-kit/dom';
import { Trash2, Plus, GripVertical } from '@lucide/svelte';
- import type { SkillCategory, ObjectId } from '$lib/types/cv';
- import { createObjectId } from '$lib/types/cv';
+ import { createObjectId, type SkillCategory, type ObjectId } from '$lib/types/cv';
interface Props {
skills: SkillCategory[];
diff --git a/src/lib/components/dashboard/new-cv-button.svelte b/src/lib/components/dashboard/new-cv-button.svelte
index e3bb51d..9553f29 100644
--- a/src/lib/components/dashboard/new-cv-button.svelte
+++ b/src/lib/components/dashboard/new-cv-button.svelte
@@ -2,8 +2,9 @@
import { Button } from '$lib/components/ui/button';
import { Plus } from '@lucide/svelte';
import { db } from '$lib/db/index';
- import { createCVFromTemplate } from '$lib/services/cv/create';
+ import { createCV } from '$lib/services/cv/create';
import { capture } from '$lib/analytics';
+ import { createId } from '$lib/id.js';
interface Props {
onCreate: (id: string) => void;
@@ -12,8 +13,8 @@
let { onCreate }: Props = $props();
async function handleClick() {
- const id = crypto.randomUUID();
- const cv = createCVFromTemplate(id, 'Untitled CV');
+ const id = createId();
+ const cv = createCV({ id, name: 'Untitled CV' });
await db.cvs.add(cv);
capture('cv_created', { source: 'dashboard' });
onCreate(id);
diff --git a/src/lib/components/dev/dummy-cv.ts b/src/lib/components/dev/dummy-cv.ts
index c9621a6..6cf5816 100644
--- a/src/lib/components/dev/dummy-cv.ts
+++ b/src/lib/components/dev/dummy-cv.ts
@@ -1,161 +1,172 @@
-import { createObjectId } from '$lib/types/cv';
-import type { CV, Tag } from '$lib/types/cv';
-import { createCVFromTemplate } from '$lib/services/cv/create';
+import { computeBlockHashes } from '$lib/features/tailoring/hash';
+import { createCV } from '$lib/services/cv/create';
+import { createObjectId, type CV, type Tag } from '$lib/types/cv';
function tags(...values: string[]): Tag[] {
return values.map((value) => ({ objectId: createObjectId(), value }));
}
export function createDummyCV(): CV {
- const cv = createCVFromTemplate(crypto.randomUUID(), 'Dummy — Full CV');
+ const cv = createCV({ name: 'Dummy — Dwight Schrute' });
- cv.blocks.fullName.value = 'Jordan Rivera';
- cv.blocks.position.value = 'Senior Software Engineer';
- cv.blocks.location.value = 'Berlin, Germany';
+ cv.blocks.fullName.value = 'Dwight K. Schrute';
+ cv.blocks.position.value = 'Assistant Regional Manager';
+ cv.blocks.location.value = 'Scranton, Pennsylvania';
cv.blocks.contacts.value = [
- { objectId: createObjectId(), label: 'Email', value: 'jordan.rivera@example.com' },
- { objectId: createObjectId(), label: 'Phone', value: '+49 30 1234567' },
- { objectId: createObjectId(), label: 'LinkedIn', value: 'linkedin.com/in/jordanrivera' },
- { objectId: createObjectId(), label: 'GitHub', value: 'github.com/jordanrivera' },
- { objectId: createObjectId(), label: 'Website', value: 'jordanrivera.dev' }
+ { objectId: createObjectId(), label: 'Email', value: 'dwight@dundermifflin.com' },
+ { objectId: createObjectId(), label: 'Phone', value: '+1 570 555 0198' },
+ { objectId: createObjectId(), label: 'Website', value: 'schrutefarms.biz' },
+ { objectId: createObjectId(), label: 'LinkedIn', value: 'linkedin.com/in/dwightschrute' }
];
cv.blocks.highlights.value = [
{
objectId: createObjectId(),
- text: 'Eight years building and scaling web applications across fintech and developer tooling.'
+ text: 'Top-performing Dunder Mifflin salesman for six consecutive quarters.'
},
{
objectId: createObjectId(),
- text: 'Led a team of five engineers through a full migration to a TypeScript monorepo.'
+ text: 'Owner and proprietor of Schrute Farms, a working beet farm and bed & breakfast.'
},
{
objectId: createObjectId(),
- text: 'Cut median API latency by 40% by reworking the data access layer.'
+ text: "Volunteer sheriff's deputy with advanced training in hand-to-hand combat and surveillance."
},
{
objectId: createObjectId(),
- text: 'Regular speaker at local meetups on frontend performance.'
+ text: 'Holds a black belt in Goju-Ryu karate and a brown belt in Judo.'
}
];
cv.blocks.skills.value = [
{
objectId: createObjectId(),
- name: 'Languages',
- skills: tags('TypeScript', 'JavaScript', 'Python', 'Go', 'SQL')
+ name: 'Sales',
+ skills: tags('Paper Sales', 'Cold Calling', 'Account Management', 'Client Retention')
},
{
objectId: createObjectId(),
- name: 'Frameworks',
- skills: tags('Svelte', 'SvelteKit', 'React', 'Node.js', 'Express')
+ name: 'Agriculture',
+ skills: tags('Beet Farming', 'Agritourism', 'Animal Husbandry', 'Beekeeping')
},
{
objectId: createObjectId(),
- name: 'Tools',
- skills: tags('Docker', 'PostgreSQL', 'Redis', 'GitHub Actions', 'AWS')
+ name: 'Security',
+ skills: tags('Surveillance', 'Brazilian Jiu-Jitsu', 'Nunchucks', 'Volunteer Deputy Work')
}
];
cv.blocks.jobHistory.value = [
{
objectId: createObjectId(),
- company: 'Nimbus Labs',
- role: 'Senior Software Engineer',
- startDate: new Date('2021-03-01'),
+ company: 'Dunder Mifflin Paper Company',
+ role: 'Assistant Regional Manager',
+ startDate: new Date('2004-03-24'),
endDate: undefined,
current: true,
achievements: [
{
objectId: createObjectId(),
- text: 'Designed the event-sourcing pipeline powering the billing system.'
+ text: 'Consistently led the Scranton branch in paper and office supply sales.'
},
{
objectId: createObjectId(),
- text: 'Mentored three junior engineers to mid-level promotions.'
+ text: 'Managed the safety committee, reducing workplace incidents by 40%.'
},
{
objectId: createObjectId(),
- text: 'Introduced end-to-end tests, dropping production regressions by half.'
+ text: 'Instituted a branch-wide volunteer militia programme, improving morale by 73%.'
},
{
objectId: createObjectId(),
- text: 'Owned the design system adopted across four product teams.'
+ text: 'Served as Acting Regional Manager during three separate managerial vacancies.'
}
],
- skills: tags('TypeScript', 'SvelteKit', 'PostgreSQL', 'AWS')
+ skills: tags('Paper Sales', 'Team Leadership', 'Safety Compliance')
},
{
objectId: createObjectId(),
- company: 'Cobalt Financial',
- role: 'Software Engineer',
- startDate: new Date('2018-06-01'),
- endDate: new Date('2021-02-01'),
- current: false,
+ company: 'Schrute Farms',
+ role: 'Owner & Proprietor',
+ startDate: new Date('1998-05-01'),
+ endDate: undefined,
+ current: true,
achievements: [
{
objectId: createObjectId(),
- text: 'Built the customer dashboard used by 200k monthly active users.'
+ text: 'Operates a 60-acre beet farm producing award-winning varieties of red and golden beets.'
},
- { objectId: createObjectId(), text: 'Reduced page load time from 4.2s to 1.1s.' },
- { objectId: createObjectId(), text: 'Integrated three third-party payment providers.' }
+ {
+ objectId: createObjectId(),
+ text: 'Converted the farmhouse into a bed & breakfast with five themed guest rooms.'
+ },
+ {
+ objectId: createObjectId(),
+ text: "Featured in TripAdvisor's top 10 most haunted B&Bs in Pennsylvania."
+ }
],
- skills: tags('React', 'Node.js', 'Redis')
+ skills: tags('Farming', 'Hospitality', 'Business Operations')
},
{
objectId: createObjectId(),
- company: 'Bright Pixel Studio',
- role: 'Junior Developer',
- startDate: new Date('2016-09-01'),
- endDate: new Date('2018-05-01'),
- current: false,
+ company: "Lackawanna County Sheriff's Department",
+ role: 'Volunteer Deputy',
+ startDate: new Date('2003-01-01'),
+ endDate: undefined,
+ current: true,
achievements: [
{
objectId: createObjectId(),
- text: 'Delivered marketing sites for a dozen agency clients.'
+ text: 'Assisted with crowd control at three consecutive Steamtown Marathon events.'
},
- { objectId: createObjectId(), text: 'Automated the deploy process with a CI pipeline.' }
+ {
+ objectId: createObjectId(),
+ text: 'Trained in prisoner transport, evidence handling, and report writing.'
+ }
],
- skills: tags('JavaScript', 'CSS', 'Docker')
+ skills: tags('Law Enforcement', 'Crowd Control')
}
];
cv.blocks.projects.value = [
{
objectId: createObjectId(),
- name: 'OpenLedger',
- description: 'An open-source double-entry accounting library with a plugin system.',
- stack: tags('TypeScript', 'Node.js', 'PostgreSQL'),
- link: 'github.com/jordanrivera/openledger'
+ name: 'Schrute Farms B&B',
+ description:
+ 'An agritourism bed & breakfast with five themed rooms, farm tours, and seasonal beet harvest experiences.',
+ stack: tags('Hospitality', 'Agritourism', 'Marketing'),
+ link: 'schrutefarms.biz'
},
{
objectId: createObjectId(),
- name: 'Pulse',
- description: 'A real-time dashboard for monitoring self-hosted services.',
- stack: tags('Svelte', 'Go', 'Redis'),
- link: 'pulse.jordanrivera.dev'
+ name: 'Recyclops',
+ description:
+ 'A grassroots office recycling programme that evolved into a full workplace environmental enforcement role.',
+ stack: tags('Sustainability', 'Waste Management', 'Community Outreach'),
+ link: ''
}
];
cv.blocks.education.value = [
{
objectId: createObjectId(),
- institution: 'Technical University of Munich',
- degree: 'M.Sc. Computer Science',
- startDate: new Date('2014-10-01'),
- endDate: new Date('2016-07-01'),
+ institution: 'Penn State University',
+ degree: 'B.A. Business Administration',
+ startDate: new Date('1992-09-01'),
+ endDate: new Date('1996-05-15'),
current: false
},
{
objectId: createObjectId(),
- institution: 'University of Vienna',
- degree: 'B.Sc. Software Engineering',
- startDate: new Date('2011-10-01'),
- endDate: new Date('2014-07-01'),
+ institution: 'Lackawanna County Community College',
+ degree: 'Certificate in Criminal Justice',
+ startDate: new Date('2001-01-15'),
+ endDate: new Date('2002-12-20'),
current: false
}
];
+ cv.blockHashes = computeBlockHashes(cv.blocks);
return cv;
}
diff --git a/src/lib/components/ui/editable-list/editable-list.svelte b/src/lib/components/ui/editable-list/editable-list.svelte
index 7a1daa3..2c7636d 100644
--- a/src/lib/components/ui/editable-list/editable-list.svelte
+++ b/src/lib/components/ui/editable-list/editable-list.svelte
@@ -6,8 +6,7 @@
import { cn } from '$lib/utils';
import { DragDropProvider, DragOverlay } from '@dnd-kit/svelte';
import type { Draggable } from '@dnd-kit/dom';
- import { createObjectId } from '$lib/types/cv';
- import type { ObjectId } from '$lib/types/cv';
+ import { createObjectId, type ObjectId } from '$lib/types/cv';
interface ListItem {
objectId: ObjectId;
diff --git a/src/lib/components/ui/logo/logo.svelte b/src/lib/components/ui/logo/logo.svelte
index 1a3db29..96971d2 100644
--- a/src/lib/components/ui/logo/logo.svelte
+++ b/src/lib/components/ui/logo/logo.svelte
@@ -1,9 +1,10 @@
-
+
+
diff --git a/src/lib/components/ui/tag-input/tag-input.svelte b/src/lib/components/ui/tag-input/tag-input.svelte
index c461127..7eb4f3d 100644
--- a/src/lib/components/ui/tag-input/tag-input.svelte
+++ b/src/lib/components/ui/tag-input/tag-input.svelte
@@ -5,8 +5,7 @@
import { SortableItem, createSortableDragHandlers } from '$lib/components/ui/sortable';
import { DragDropProvider, DragOverlay } from '@dnd-kit/svelte';
import type { Draggable } from '@dnd-kit/dom';
- import { createObjectId } from '$lib/types/cv';
- import type { Tag } from '$lib/types/cv';
+ import { createObjectId, type Tag } from '$lib/types/cv';
interface Props {
tags: Tag[];
diff --git a/src/lib/features/export/generate.test.ts b/src/lib/features/export/generate.test.ts
index 31cc425..467d731 100644
--- a/src/lib/features/export/generate.test.ts
+++ b/src/lib/features/export/generate.test.ts
@@ -1,6 +1,5 @@
import { describe, expect, it } from 'vitest';
-import { createObjectId } from '$lib/types/cv';
-import type { CV, CVBlocks } from '$lib/types/cv';
+import { createObjectId, type CV, type CVBlocks } from '$lib/types/cv';
import { buildDocDef, sanitizeFilename } from './generate';
import { computeBlockHashes } from '$lib/features/tailoring/hash';
diff --git a/src/lib/features/export/preprocess.test.ts b/src/lib/features/export/preprocess.test.ts
index acd374f..225c906 100644
--- a/src/lib/features/export/preprocess.test.ts
+++ b/src/lib/features/export/preprocess.test.ts
@@ -1,6 +1,5 @@
import { describe, expect, it } from 'vitest';
-import { createObjectId } from '$lib/types/cv';
-import type { CV, CVBlocks, ObjectId } from '$lib/types/cv';
+import { createObjectId, type CV, type CVBlocks, type ObjectId } from '$lib/types/cv';
import { preprocessBlocks } from './preprocess';
import { computeBlockHashes } from '$lib/features/tailoring/hash';
diff --git a/src/lib/features/export/themes/classic/classic.test.ts b/src/lib/features/export/themes/classic/classic.test.ts
index 57770f0..5a6239e 100644
--- a/src/lib/features/export/themes/classic/classic.test.ts
+++ b/src/lib/features/export/themes/classic/classic.test.ts
@@ -1,6 +1,5 @@
import { describe, expect, it } from 'vitest';
-import { createObjectId } from '$lib/types/cv';
-import type { CVBlocks, ObjectId } from '$lib/types/cv';
+import { createObjectId, type CVBlocks, type ObjectId } from '$lib/types/cv';
import { classicTheme } from './classic';
function id(): ObjectId {
diff --git a/src/lib/features/tailoring/_fixtures.ts b/src/lib/features/tailoring/_fixtures.ts
index cd81987..741ea33 100644
--- a/src/lib/features/tailoring/_fixtures.ts
+++ b/src/lib/features/tailoring/_fixtures.ts
@@ -1,16 +1,16 @@
-import { createObjectId } from '$lib/types/cv';
-import type {
- Achievement,
- CV,
- CVBlocks,
- JobEntry,
- ObjectId,
- Tag,
- Highlight,
- ProjectEntry,
- SkillCategory,
- ContactEntry,
- EducationEntry
+import {
+ createObjectId,
+ type Achievement,
+ type CV,
+ type CVBlocks,
+ type JobEntry,
+ type ObjectId,
+ type Tag,
+ type Highlight,
+ type ProjectEntry,
+ type SkillCategory,
+ type ContactEntry,
+ type EducationEntry
} from '$lib/types/cv';
import { computeBlockHashes } from './hash';
diff --git a/src/lib/features/tailoring/create-tailored.ts b/src/lib/features/tailoring/create-tailored.ts
index 8760575..69a5ff2 100644
--- a/src/lib/features/tailoring/create-tailored.ts
+++ b/src/lib/features/tailoring/create-tailored.ts
@@ -1,12 +1,13 @@
import { db } from '$lib/db/index';
import type { CV } from '$lib/types/cv';
+import { createId } from '$lib/id.js';
export async function createTailoredCV(
master: CV,
name: string,
company?: string
): Promise {
- const id = crypto.randomUUID();
+ const id = createId();
const now = Date.now();
const tailored: CV = {
diff --git a/src/lib/features/tailoring/locate.test.ts b/src/lib/features/tailoring/locate.test.ts
index f7fedcf..8f0137a 100644
--- a/src/lib/features/tailoring/locate.test.ts
+++ b/src/lib/features/tailoring/locate.test.ts
@@ -1,6 +1,12 @@
import { describe, expect, it } from 'vitest';
-import { createObjectId } from '$lib/types/cv';
-import type { CV, CVBlocks, JobEntry, ObjectId, SkillCategory } from '$lib/types/cv';
+import {
+ createObjectId,
+ type CV,
+ type CVBlocks,
+ type JobEntry,
+ type ObjectId,
+ type SkillCategory
+} from '$lib/types/cv';
import type { DiffItem } from './types';
import { computeBlockHashes } from './hash';
import {
diff --git a/src/lib/features/tailoring/present.test.ts b/src/lib/features/tailoring/present.test.ts
index 47caa32..b86332b 100644
--- a/src/lib/features/tailoring/present.test.ts
+++ b/src/lib/features/tailoring/present.test.ts
@@ -1,6 +1,13 @@
import { describe, expect, it } from 'vitest';
-import { createObjectId } from '$lib/types/cv';
-import type { CV, CVBlocks, JobEntry, ObjectId, SkillCategory, Tag } from '$lib/types/cv';
+import {
+ createObjectId,
+ type CV,
+ type CVBlocks,
+ type JobEntry,
+ type ObjectId,
+ type SkillCategory,
+ type Tag
+} from '$lib/types/cv';
import type { DiffItem } from './types';
import { entryTitle, formatPeriod, formatValue, entryKind, describeDiff } from './present';
import { computeBlockHashes } from './hash';
diff --git a/src/lib/id.ts b/src/lib/id.ts
new file mode 100644
index 0000000..bb0cbd2
--- /dev/null
+++ b/src/lib/id.ts
@@ -0,0 +1,9 @@
+import { nanoid as secureNanoid } from 'nanoid';
+import { nanoid as insecureNanoid } from 'nanoid/non-secure';
+
+export function createId(): string {
+ if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
+ return secureNanoid();
+ }
+ return insecureNanoid();
+}
diff --git a/src/lib/services/cv/create.ts b/src/lib/services/cv/create.ts
index 2350db9..8c54ead 100644
--- a/src/lib/services/cv/create.ts
+++ b/src/lib/services/cv/create.ts
@@ -1,15 +1,13 @@
-import { createObjectId } from '$lib/types/cv';
-import type { CV, CVBlocks } from '$lib/types/cv';
+import { createId } from '$lib/id.js';
+import { createObjectId, type CV, type CVBlocks } from '$lib/types/cv';
import { computeBlockHashes } from '$lib/features/tailoring/hash';
-import { CLASSIC_TEMPLATE } from './templates';
-import type { CVTemplate } from './templates';
-export function createCVFromTemplate(
- id: string,
- name: string,
- template: CVTemplate = CLASSIC_TEMPLATE
-): CV {
- void template;
+export interface CreateCVOptions {
+ name: string;
+ id?: string;
+}
+
+export function createCV({ name, id = createId() }: CreateCVOptions): CV {
const now = Date.now();
const blocks: CVBlocks = {
diff --git a/src/lib/services/cv/templates.ts b/src/lib/services/cv/templates.ts
deleted file mode 100644
index 43e98b8..0000000
--- a/src/lib/services/cv/templates.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-export interface CVTemplateBlock {
- label: string;
- blockKey: string;
- kind: 'text' | 'array';
-}
-
-export interface CVTemplate {
- name: string;
- blocks: CVTemplateBlock[];
-}
-
-export const CLASSIC_TEMPLATE: CVTemplate = {
- name: 'Classic',
- blocks: [
- { label: 'Full Name', blockKey: 'fullName', kind: 'text' },
- { label: 'Position', blockKey: 'position', kind: 'text' },
- { label: 'Location', blockKey: 'location', kind: 'text' },
- { label: 'Contacts', blockKey: 'contacts', kind: 'array' },
- { label: 'Highlights', blockKey: 'highlights', kind: 'array' },
- { label: 'Skills', blockKey: 'skills', kind: 'array' },
- { label: 'Job History', blockKey: 'jobHistory', kind: 'array' },
- { label: 'Projects', blockKey: 'projects', kind: 'array' },
- { label: 'Education', blockKey: 'education', kind: 'array' }
- ]
-};
diff --git a/src/lib/types/cv.ts b/src/lib/types/cv.ts
index 1852dc6..80ac0f5 100644
--- a/src/lib/types/cv.ts
+++ b/src/lib/types/cv.ts
@@ -1,7 +1,9 @@
+import { createId } from '$lib/id.js';
+
export type ObjectId = string & { readonly __brand: 'ObjectId' };
export function createObjectId(): ObjectId {
- return crypto.randomUUID() as ObjectId;
+ return createId() as ObjectId;
}
export interface WithId {
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 7760e5a..23a2272 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -3,7 +3,7 @@
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { db } from '$lib/db/index';
- import { createCVFromTemplate } from '$lib/services/cv/create';
+ import { createCV } from '$lib/services/cv/create';
import { getHasCvsHint } from '$lib/services/cv/has-cvs-hint';
import { Hero, About } from '$lib/components/landing';
import { Dashboard } from '$lib/components/dashboard';
@@ -18,11 +18,10 @@
});
async function handleCreateFirstCv() {
- const id = crypto.randomUUID();
- const cv = createCVFromTemplate(id, 'Untitled CV');
+ const cv = createCV({ name: 'Untitled CV' });
await db.cvs.add(cv);
capture('cv_created', { source: 'hero' });
- await goto(resolve(`/cv/${id}`));
+ await goto(resolve(`/cv/${cv.id}`));
}
diff --git a/src/stories/blocks/helpers/ContactsWrapper.svelte b/src/stories/blocks/helpers/ContactsWrapper.svelte
index 8896f7e..9ca4a0a 100644
--- a/src/stories/blocks/helpers/ContactsWrapper.svelte
+++ b/src/stories/blocks/helpers/ContactsWrapper.svelte
@@ -1,7 +1,6 @@
diff --git a/src/stories/ui/helpers/EditableListWrapper.svelte b/src/stories/ui/helpers/EditableListWrapper.svelte
index 2909a2d..f0e71db 100644
--- a/src/stories/ui/helpers/EditableListWrapper.svelte
+++ b/src/stories/ui/helpers/EditableListWrapper.svelte
@@ -1,7 +1,6 @@