Overview
TypeScript interfaces for CV version control, including version metadata, full version data, and CV document structure.
CVVersion
Complete version data including full CV content.
interface CVVersion {
id: string;
userId: string;
data: CVData;
versionName: string;
description?: string;
tags?: string[];
createdAt: Date;
isAutoSave: boolean;
}
Fields
Unique version identifier (Firestore document ID).
User ID who owns this version (Firebase Auth UID).
Complete CV content snapshot at this version.See CVData for structure.
User-defined version name (e.g., “Senior Developer Resume”, “Tech Company Application”).
Optional description of changes or purpose (e.g., “Updated for FAANG applications”).
Optional tags for organization (e.g., ['tech', 'senior', '2024']).
Timestamp when version was created.
true if version was created automatically, false if manually saved by user.
Usage
const version: CVVersion = {
id: 'version_abc123',
userId: 'user_xyz789',
data: cvData,
versionName: 'Software Engineer Resume - Google',
description: 'Tailored for Google L5 position',
tags: ['google', 'senior', 'tech'],
createdAt: new Date(),
isAutoSave: false
};
Lightweight version metadata without full CV content.
interface CVVersionMetadata {
id: string;
userId: string;
versionName: string;
description?: string;
tags?: string[];
createdAt: Date;
isAutoSave: boolean;
}
Fields
Unique version identifier.
User ID who owns this version.
User-defined version name.
Version creation timestamp.
Whether version was auto-saved.
Usage
Use for list views to reduce data transfer:
// Efficient: Only loads metadata
const versions: CVVersionMetadata[] = await getVersions(userId);
// Later, load full data only when needed
const fullVersion: CVVersion = await getVersion(versionId);
Typical list rendering:
const VersionList = ({ versions }: { versions: CVVersionMetadata[] }) => {
return (
<ul>
{versions.map(v => (
<li key={v.id}>
<h3>{v.versionName}</h3>
<p>{v.description}</p>
<div>
{v.tags?.map(tag => <Badge key={tag}>{tag}</Badge>)}
</div>
<time>{v.createdAt.toLocaleDateString()}</time>
{v.isAutoSave && <Badge variant="secondary">Auto-saved</Badge>}
</li>
))}
</ul>
);
};
Input data for creating a new version.
interface CVVersionInput {
versionName: string;
description?: string;
tags?: string[];
}
Fields
Name for the new version.Validation:
- Must be non-empty
- Recommended: 3-100 characters
- Should be descriptive
Optional description of changes or purpose.Recommended for:
- Documenting major changes
- Explaining version purpose
- Adding context for future reference
Optional tags for categorization.Best practices:
- Use lowercase
- Keep tags short (1-20 chars)
- Use common categories (e.g., year, industry, level)
Usage
const input: CVVersionInput = {
versionName: 'Senior Backend Engineer - Netflix',
description: 'Emphasized distributed systems experience',
tags: ['netflix', 'senior', 'backend', '2024']
};
const versionId = await saveVersion(userId, cvData, input, false);
Form validation:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const versionSchema = z.object({
versionName: z.string().min(3).max(100),
description: z.string().max(500).optional(),
tags: z.array(z.string().max(20)).max(10).optional()
});
const VersionForm = () => {
const form = useForm<CVVersionInput>({
resolver: zodResolver(versionSchema)
});
const onSubmit = async (data: CVVersionInput) => {
await saveNewVersion(data);
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<input {...form.register('versionName')} placeholder="Version name" />
<textarea {...form.register('description')} placeholder="Description" />
<input {...form.register('tags')} placeholder="Tags (comma-separated)" />
<button type="submit">Save Version</button>
</form>
);
};
CVDocument
Main CV document stored in Firestore.
interface CVDocument {
data: CVData;
updatedAt: Date;
createdAt: Date;
currentVersionId?: string;
}
Fields
Current CV content.See CVData for structure.
Last modification timestamp. Auto-updated on each save.
Document creation timestamp. Set once, never changes.
ID of the version that was last restored. undefined if never restored.Use cases:
- Track which version is currently active
- Link main CV to version history
- Show “Current version” badge in UI
Usage
Firestore structure:
users/{userId}/cv
├─ data: CVData
├─ updatedAt: Timestamp
├─ createdAt: Timestamp
└─ currentVersionId?: string
users/{userId}/versions/{versionId}
├─ data: CVData
├─ versionName: string
├─ description?: string
├─ tags?: string[]
├─ createdAt: Timestamp
└─ isAutoSave: boolean
Loading CV document:
const loadCV = async (userId: string): Promise<CVData | null> => {
const docRef = doc(db, 'users', userId, 'cv');
const snapshot = await getDoc(docRef);
if (!snapshot.exists()) return null;
const cvDoc = snapshot.data() as CVDocument;
return cvDoc.data;
};
Saving CV document:
const saveCV = async (userId: string, data: CVData): Promise<void> => {
const docRef = doc(db, 'users', userId, 'cv');
await setDoc(docRef, {
data,
updatedAt: serverTimestamp(),
// createdAt is set only on first save via setDoc merge
}, { merge: true });
};
Usage Examples
Creating a Version
import { useCVVersions } from '@/hooks/useCVVersions';
import { CVVersionInput } from '@/lib/types';
const CreateVersionDialog = () => {
const { saveNewVersion } = useCVVersions({ userId });
const [input, setInput] = useState<CVVersionInput>({
versionName: '',
description: '',
tags: []
});
const handleSave = async () => {
const versionId = await saveNewVersion(input);
if (versionId) {
toast.success('Version saved!');
onClose();
} else {
toast.error('Failed to save version');
}
};
return (
<Dialog>
<DialogContent>
<DialogHeader>
<DialogTitle>Save Version</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Input
placeholder="Version name"
value={input.versionName}
onChange={e => setInput({ ...input, versionName: e.target.value })}
/>
<Textarea
placeholder="Description (optional)"
value={input.description}
onChange={e => setInput({ ...input, description: e.target.value })}
/>
<TagInput
tags={input.tags || []}
onChange={tags => setInput({ ...input, tags })}
/>
</div>
<DialogFooter>
<Button onClick={handleSave}>Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
Version History with Metadata
const VersionHistory = () => {
const { user } = useAuth();
const { versions, isLoading, getVersionData, restoreToVersion } = useCVVersions({
userId: user?.uid ?? null
});
const [previewVersion, setPreviewVersion] = useState<CVVersion | null>(null);
const handlePreview = async (versionId: string) => {
const version = await getVersionData(versionId);
setPreviewVersion(version);
};
const handleRestore = async (versionId: string) => {
const confirmed = await confirm('Restore this version?');
if (confirmed) {
const success = await restoreToVersion(versionId);
if (success) {
toast.success('Version restored');
window.location.reload();
}
}
};
return (
<div className="grid grid-cols-2 gap-4">
<div>
<h2>Versions</h2>
{isLoading ? (
<SkeletonList count={5} />
) : (
<ul className="space-y-2">
{versions.map(v => (
<li key={v.id} className="border rounded p-4">
<h3 className="font-semibold">{v.versionName}</h3>
{v.description && (
<p className="text-sm text-muted-foreground">{v.description}</p>
)}
<div className="flex gap-2 mt-2">
{v.tags?.map(tag => (
<Badge key={tag} variant="secondary">{tag}</Badge>
))}
</div>
<div className="flex gap-2 mt-4">
<Button size="sm" onClick={() => handlePreview(v.id)}>
Preview
</Button>
<Button size="sm" onClick={() => handleRestore(v.id)}>
Restore
</Button>
</div>
</li>
))}
</ul>
)}
</div>
<div>
{previewVersion ? (
<div>
<h2>Preview: {previewVersion.versionName}</h2>
<CVPreview data={previewVersion.data} />
</div>
) : (
<div className="flex items-center justify-center h-full">
<p className="text-muted-foreground">Select a version to preview</p>
</div>
)}
</div>
</div>
);
};
const FilteredVersions = () => {
const { versions } = useCVVersions({ userId });
const [selectedTag, setSelectedTag] = useState<string | null>(null);
// Get all unique tags
const allTags = useMemo(() => {
const tags = new Set<string>();
versions.forEach(v => v.tags?.forEach(tag => tags.add(tag)));
return Array.from(tags).sort();
}, [versions]);
// Filter versions by selected tag
const filteredVersions = useMemo(() => {
if (!selectedTag) return versions;
return versions.filter(v => v.tags?.includes(selectedTag));
}, [versions, selectedTag]);
return (
<div>
<div className="flex gap-2 mb-4">
<Button
variant={selectedTag === null ? 'default' : 'outline'}
onClick={() => setSelectedTag(null)}
>
All
</Button>
{allTags.map(tag => (
<Button
key={tag}
variant={selectedTag === tag ? 'default' : 'outline'}
onClick={() => setSelectedTag(tag)}
>
{tag}
</Button>
))}
</div>
<VersionList versions={filteredVersions} />
</div>
);
};
Best Practices
Version Naming
// Good names
'Senior SWE - Google Application'
'Backend Focus - Netflix'
'Tech Lead Resume - 2024'
'Startup Applications'
// Avoid
'Version 1'
'Latest'
'Updated'
'asdf'
const commonTags = {
year: ['2024', '2023'],
level: ['entry', 'mid', 'senior', 'staff', 'principal'],
industry: ['tech', 'finance', 'healthcare', 'startup'],
focus: ['frontend', 'backend', 'fullstack', 'mobile', 'devops'],
company: ['faang', 'startup', 'enterprise']
};
Auto-Save vs Manual
// Manual save: User-initiated
await saveVersion(userId, cvData, {
versionName: 'Google Application',
description: 'Ready for submission'
}, false); // isAutoSave = false
// Auto-save: System-initiated
await saveVersion(userId, cvData, {
versionName: `Auto-save ${new Date().toISOString()}`
}, true); // isAutoSave = true