From 76e12e3d73fab5905caefc561869317e129f4a29 Mon Sep 17 00:00:00 2001 From: Alexey Maltsev Date: Wed, 3 Jun 2026 00:12:35 +0200 Subject: [PATCH 1/5] feat: implement randomUUID utility function and update CV creation to use it - Added a randomUUID function to utils.ts for generating UUIDs in non-secure contexts. - Updated CV creation in multiple components to utilize the new randomUUID function instead of crypto.randomUUID for consistency and compatibility. --- src/lib/components/dashboard/new-cv-button.svelte | 3 ++- src/lib/components/dev/dummy-cv.ts | 3 ++- src/lib/components/ui/logo/logo.svelte | 5 +++-- src/lib/features/tailoring/create-tailored.ts | 3 ++- src/lib/types/cv.ts | 4 +++- src/lib/utils.ts | 12 ++++++++++++ src/routes/+page.svelte | 3 ++- 7 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/lib/components/dashboard/new-cv-button.svelte b/src/lib/components/dashboard/new-cv-button.svelte index e3bb51d..c5c6bf5 100644 --- a/src/lib/components/dashboard/new-cv-button.svelte +++ b/src/lib/components/dashboard/new-cv-button.svelte @@ -4,6 +4,7 @@ import { db } from '$lib/db/index'; import { createCVFromTemplate } from '$lib/services/cv/create'; import { capture } from '$lib/analytics'; + import { randomUUID } from '$lib/utils.js'; interface Props { onCreate: (id: string) => void; @@ -12,7 +13,7 @@ let { onCreate }: Props = $props(); async function handleClick() { - const id = crypto.randomUUID(); + const id = randomUUID(); const cv = createCVFromTemplate(id, 'Untitled CV'); await db.cvs.add(cv); capture('cv_created', { source: 'dashboard' }); diff --git a/src/lib/components/dev/dummy-cv.ts b/src/lib/components/dev/dummy-cv.ts index c9621a6..89a29fb 100644 --- a/src/lib/components/dev/dummy-cv.ts +++ b/src/lib/components/dev/dummy-cv.ts @@ -1,13 +1,14 @@ import { createObjectId } from '$lib/types/cv'; import type { CV, Tag } from '$lib/types/cv'; import { createCVFromTemplate } from '$lib/services/cv/create'; +import { randomUUID } from '$lib/utils.js'; 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 = createCVFromTemplate(randomUUID(), 'Dummy — Full CV'); cv.blocks.fullName.value = 'Jordan Rivera'; cv.blocks.position.value = 'Senior Software Engineer'; 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 @@ -HumbleHire +HumbleHire +HumbleHire diff --git a/src/lib/features/tailoring/create-tailored.ts b/src/lib/features/tailoring/create-tailored.ts index 8760575..7db9ea6 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 { randomUUID } from '$lib/utils.js'; export async function createTailoredCV( master: CV, name: string, company?: string ): Promise { - const id = crypto.randomUUID(); + const id = randomUUID(); const now = Date.now(); const tailored: CV = { diff --git a/src/lib/types/cv.ts b/src/lib/types/cv.ts index 1852dc6..039e24c 100644 --- a/src/lib/types/cv.ts +++ b/src/lib/types/cv.ts @@ -1,7 +1,9 @@ +import { randomUUID } from '$lib/utils.js'; + export type ObjectId = string & { readonly __brand: 'ObjectId' }; export function createObjectId(): ObjectId { - return crypto.randomUUID() as ObjectId; + return randomUUID() as ObjectId; } export interface WithId { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 9643e73..69585ed 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -17,6 +17,18 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } +/** Works in non-secure contexts where `crypto.randomUUID` is unavailable. */ +export function randomUUID(): string { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any export type WithoutChild = T extends { child?: any } ? Omit : T; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 7760e5a..1f812b4 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -8,6 +8,7 @@ import { Hero, About } from '$lib/components/landing'; import { Dashboard } from '$lib/components/dashboard'; import { capture } from '$lib/analytics'; + import { randomUUID } from '$lib/utils.js'; let mode = $state<'hero' | 'dashboard'>(getHasCvsHint() ? 'dashboard' : 'hero'); @@ -18,7 +19,7 @@ }); async function handleCreateFirstCv() { - const id = crypto.randomUUID(); + const id = randomUUID(); const cv = createCVFromTemplate(id, 'Untitled CV'); await db.cvs.add(cv); capture('cv_created', { source: 'hero' }); From 259733d1aedb4bf0ffe2dd9a25527e95e2412410 Mon Sep 17 00:00:00 2001 From: Alexey Maltsev Date: Wed, 3 Jun 2026 00:45:23 +0200 Subject: [PATCH 2/5] feat: add nanoid dependency and refactor CV creation logic - Added `nanoid` package for unique ID generation. - Refactored CV creation functions to utilize `createId` instead of `randomUUID`. - Updated multiple components to ensure consistent ID generation for CVs. - Removed the deprecated `createCVFromTemplate` function and its associated template file. --- docs/decisions/ADR-004-e2e-test-strategy.md | 2 +- package.json | 1 + pnpm-lock.yaml | 10 +++++++ .../components/blocks/contacts-block.svelte | 3 +-- .../components/blocks/education-block.svelte | 3 +-- .../blocks/job-history-block.svelte | 3 +-- .../components/blocks/projects-block.svelte | 3 +-- src/lib/components/blocks/skills-block.svelte | 3 +-- .../components/dashboard/new-cv-button.svelte | 8 +++--- src/lib/components/dev/dummy-cv.ts | 8 +++--- .../ui/editable-list/editable-list.svelte | 3 +-- .../components/ui/tag-input/tag-input.svelte | 3 +-- src/lib/features/export/generate.test.ts | 3 +-- src/lib/features/export/preprocess.test.ts | 3 +-- .../export/themes/classic/classic.test.ts | 3 +-- src/lib/features/tailoring/_fixtures.ts | 26 +++++++++---------- src/lib/features/tailoring/create-tailored.ts | 4 +-- src/lib/features/tailoring/locate.test.ts | 10 +++++-- src/lib/features/tailoring/present.test.ts | 11 ++++++-- src/lib/id.ts | 9 +++++++ src/lib/services/cv/create.ts | 18 ++++++------- src/lib/services/cv/templates.ts | 25 ------------------ src/lib/types/cv.ts | 4 +-- src/lib/utils.ts | 12 --------- src/routes/+page.svelte | 8 +++--- .../blocks/helpers/ContactsWrapper.svelte | 3 +-- .../blocks/helpers/EducationWrapper.svelte | 3 +-- .../blocks/helpers/FullNameWrapper.svelte | 3 +-- .../blocks/helpers/HighlightsWrapper.svelte | 3 +-- .../blocks/helpers/JobHistoryWrapper.svelte | 3 +-- .../blocks/helpers/LocationWrapper.svelte | 3 +-- .../blocks/helpers/PositionWrapper.svelte | 3 +-- .../blocks/helpers/ProjectsWrapper.svelte | 3 +-- .../blocks/helpers/SkillsWrapper.svelte | 3 +-- .../ui/helpers/BlockWrapperWrapper.svelte | 4 +-- .../ui/helpers/EditableListWrapper.svelte | 3 +-- src/stories/ui/helpers/TagInputWrapper.svelte | 3 +-- 37 files changed, 96 insertions(+), 127 deletions(-) create mode 100644 src/lib/id.ts delete mode 100644 src/lib/services/cv/templates.ts 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/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 c5c6bf5..9553f29 100644 --- a/src/lib/components/dashboard/new-cv-button.svelte +++ b/src/lib/components/dashboard/new-cv-button.svelte @@ -2,9 +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 { randomUUID } from '$lib/utils.js'; + import { createId } from '$lib/id.js'; interface Props { onCreate: (id: string) => void; @@ -13,8 +13,8 @@ let { onCreate }: Props = $props(); async function handleClick() { - const id = 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 89a29fb..2ad5d2b 100644 --- a/src/lib/components/dev/dummy-cv.ts +++ b/src/lib/components/dev/dummy-cv.ts @@ -1,14 +1,12 @@ -import { createObjectId } from '$lib/types/cv'; -import type { CV, Tag } from '$lib/types/cv'; -import { createCVFromTemplate } from '$lib/services/cv/create'; -import { randomUUID } from '$lib/utils.js'; +import { createObjectId, type CV, type Tag } from '$lib/types/cv'; +import { createCV } from '$lib/services/cv/create'; function tags(...values: string[]): Tag[] { return values.map((value) => ({ objectId: createObjectId(), value })); } export function createDummyCV(): CV { - const cv = createCVFromTemplate(randomUUID(), 'Dummy — Full CV'); + const cv = createCV({ name: 'Dummy — Full CV' }); cv.blocks.fullName.value = 'Jordan Rivera'; cv.blocks.position.value = 'Senior Software Engineer'; 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/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 7db9ea6..69a5ff2 100644 --- a/src/lib/features/tailoring/create-tailored.ts +++ b/src/lib/features/tailoring/create-tailored.ts @@ -1,13 +1,13 @@ import { db } from '$lib/db/index'; import type { CV } from '$lib/types/cv'; -import { randomUUID } from '$lib/utils.js'; +import { createId } from '$lib/id.js'; export async function createTailoredCV( master: CV, name: string, company?: string ): Promise { - const id = 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 039e24c..80ac0f5 100644 --- a/src/lib/types/cv.ts +++ b/src/lib/types/cv.ts @@ -1,9 +1,9 @@ -import { randomUUID } from '$lib/utils.js'; +import { createId } from '$lib/id.js'; export type ObjectId = string & { readonly __brand: 'ObjectId' }; export function createObjectId(): ObjectId { - return randomUUID() as ObjectId; + return createId() as ObjectId; } export interface WithId { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 69585ed..9643e73 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -17,18 +17,6 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } -/** Works in non-secure contexts where `crypto.randomUUID` is unavailable. */ -export function randomUUID(): string { - if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { - return crypto.randomUUID(); - } - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { - const r = (Math.random() * 16) | 0; - const v = c === 'x' ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); -} - // eslint-disable-next-line @typescript-eslint/no-explicit-any export type WithoutChild = T extends { child?: any } ? Omit : T; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 1f812b4..23a2272 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -3,12 +3,11 @@ 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'; import { capture } from '$lib/analytics'; - import { randomUUID } from '$lib/utils.js'; let mode = $state<'hero' | 'dashboard'>(getHasCvsHint() ? 'dashboard' : 'hero'); @@ -19,11 +18,10 @@ }); async function handleCreateFirstCv() { - const id = 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 @@