Adds first version of the app. Local storage only.
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import type { Project } from '../domain/types'
|
||||
import { normalizeProject } from './exportImport'
|
||||
import type { ProjectRepository } from './ProjectRepository'
|
||||
|
||||
const STORAGE_KEY = 'av-planner:project:v1'
|
||||
|
||||
export class LocalStorageProjectRepository implements ProjectRepository {
|
||||
async load(): Promise<Project | null> {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) return null
|
||||
return normalizeProject(JSON.parse(raw) as Project)
|
||||
} catch (err) {
|
||||
console.error('Failed to load project from localStorage', err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async save(project: Project): Promise<void> {
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(project))
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
window.localStorage.removeItem(STORAGE_KEY)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Project } from '../domain/types'
|
||||
|
||||
/**
|
||||
* Storage abstraction the rest of the app codes against. Today only
|
||||
* `LocalStorageProjectRepository` implements it; a future backend would add
|
||||
* e.g. `ApiProjectRepository` (REST/GraphQL) behind this same interface so
|
||||
* the store/UI layers wouldn't need to change.
|
||||
*/
|
||||
export interface ProjectRepository {
|
||||
load(): Promise<Project | null>
|
||||
save(project: Project): Promise<void>
|
||||
clear(): Promise<void>
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { Project } from '../domain/types'
|
||||
|
||||
export function projectToJson(project: Project): string {
|
||||
return JSON.stringify(project, null, 2)
|
||||
}
|
||||
|
||||
export function downloadProjectFile(project: Project): void {
|
||||
const json = projectToJson(project)
|
||||
const blob = new Blob([json], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
const safeName = project.name.trim().replace(/[^a-z0-9-_]+/gi, '_') || 'project'
|
||||
a.href = url
|
||||
a.download = `${safeName}.avplan.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
class ProjectImportError extends Error {}
|
||||
|
||||
/** Minimal structural check — enough to catch "wrong file" without pulling
|
||||
* in a schema validation library for a single call site. */
|
||||
function assertLooksLikeProject(value: unknown): asserts value is Project {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
throw new ProjectImportError('File does not contain a JSON object.')
|
||||
}
|
||||
const p = value as Partial<Project>
|
||||
if (typeof p.id !== 'string' || typeof p.name !== 'string') {
|
||||
throw new ProjectImportError('File is missing required project fields.')
|
||||
}
|
||||
if (!Array.isArray(p.devices) || !Array.isArray(p.connections)) {
|
||||
throw new ProjectImportError('File is missing devices/connections arrays.')
|
||||
}
|
||||
}
|
||||
|
||||
export async function readProjectFile(file: File): Promise<Project> {
|
||||
const text = await file.text()
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(text)
|
||||
} catch {
|
||||
throw new ProjectImportError('File is not valid JSON.')
|
||||
}
|
||||
assertLooksLikeProject(parsed)
|
||||
return normalizeProject(parsed)
|
||||
}
|
||||
|
||||
/** Fills in collections added after a project may have been saved/exported,
|
||||
* so older data (from localStorage or an older export file) doesn't crash
|
||||
* code that expects these arrays to always exist. */
|
||||
export function normalizeProject(project: Project): Project {
|
||||
return {
|
||||
...project,
|
||||
customDeviceTemplates: project.customDeviceTemplates ?? [],
|
||||
customPortTypes: project.customPortTypes ?? [],
|
||||
customCableTypes: project.customCableTypes ?? [],
|
||||
customDeviceCategories: project.customDeviceCategories ?? [],
|
||||
hiddenBuiltInDeviceTemplateIds: project.hiddenBuiltInDeviceTemplateIds ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
export { ProjectImportError }
|
||||
Reference in New Issue
Block a user