feat: initial working version of project manager
This commit is contained in:
85
app/src/lib/project-manager/ProjectManager.svelte
Normal file
85
app/src/lib/project-manager/ProjectManager.svelte
Normal file
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import type { Graph } from "$lib/types";
|
||||
import { defaultPlant } from "$lib/graph-templates";
|
||||
import type { ProjectManager } from "./project-manager.svelte";
|
||||
|
||||
const { projectManager } = $props<{ projectManager: ProjectManager }>();
|
||||
|
||||
let showNewProject = $state(false);
|
||||
let newProjectName = $state("");
|
||||
|
||||
function handleCreate() {
|
||||
projectManager.handleCreateProject(
|
||||
defaultPlant as unknown as Graph,
|
||||
newProjectName,
|
||||
);
|
||||
newProjectName = "";
|
||||
showNewProject = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<header
|
||||
class="flex justify-between px-4 h-[70px] border-b-1 border-[var(--outline)] items-center"
|
||||
>
|
||||
<h3>Project</h3>
|
||||
<button
|
||||
class="px-3 py-1 bg-[var(--layer-0)] rounded"
|
||||
onclick={() => (showNewProject = !showNewProject)}
|
||||
>
|
||||
New
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{#if showNewProject}
|
||||
<div
|
||||
class="flex justify-between px-4 h-[70px] border-b-1 border-[var(--outline)] items-center gap-2"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newProjectName}
|
||||
placeholder="Project name"
|
||||
class="flex-1 min-w-0 px-2 py-2 bg-gray-800 border border-gray-700 rounded"
|
||||
onkeydown={(e) => e.key === "Enter" && handleCreate()}
|
||||
/>
|
||||
<button class="cursor-pointer" onclick={() => handleCreate()}>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="p-4 text-white min-h-screen">
|
||||
{#if projectManager.loading}
|
||||
<p>Loading...</p>
|
||||
{/if}
|
||||
|
||||
<ul class="space-y-2">
|
||||
{#each projectManager.projects as project (project.id)}
|
||||
<li>
|
||||
<div
|
||||
class="w-full text-left px-3 py-2 rounded cursor-pointer {projectManager
|
||||
.activeProjectId.value === project.id
|
||||
? 'bg-blue-600'
|
||||
: 'bg-gray-800 hover:bg-gray-700'}"
|
||||
onclick={() => projectManager.handleSelectProject(project.id!)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onkeydown={(e) =>
|
||||
e.key === "Enter" &&
|
||||
projectManager.handleSelectProject(project.id!)}
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<span>{project.meta?.title || "Untitled"}</span>
|
||||
<button
|
||||
class="text-red-400 hover:text-red-300"
|
||||
onclick={() => {
|
||||
projectManager.handleDeleteProject(project.id!);
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
54
app/src/lib/project-manager/project-database.svelte.ts
Normal file
54
app/src/lib/project-manager/project-database.svelte.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { Graph } from '@nodarium/types';
|
||||
import { type IDBPDatabase, openDB } from 'idb';
|
||||
|
||||
export interface GraphDatabase {
|
||||
projects: Graph;
|
||||
}
|
||||
|
||||
const DB_NAME = 'nodarium-graphs';
|
||||
const DB_VERSION = 1;
|
||||
const STORE_NAME = 'graphs';
|
||||
|
||||
let dbPromise: Promise<IDBPDatabase<GraphDatabase>> | null = null;
|
||||
|
||||
export function getDB() {
|
||||
if (!dbPromise) {
|
||||
dbPromise = openDB<GraphDatabase>(DB_NAME, DB_VERSION, {
|
||||
upgrade(db) {
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return dbPromise;
|
||||
}
|
||||
|
||||
export async function getGraph(id: number): Promise<Graph | undefined> {
|
||||
const db = await getDB();
|
||||
return db.get(STORE_NAME, id);
|
||||
}
|
||||
|
||||
export async function saveGraph(graph: Graph): Promise<Graph> {
|
||||
const db = await getDB();
|
||||
graph.meta = { ...graph.meta, lastModified: new Date().toISOString() };
|
||||
console.log('SAVING GRAPH', { graph });
|
||||
await db.put(STORE_NAME, graph);
|
||||
return graph;
|
||||
}
|
||||
|
||||
export async function deleteGraph(id: number): Promise<void> {
|
||||
const db = await getDB();
|
||||
await db.delete(STORE_NAME, id);
|
||||
console.log('DELETE GRAPH', { id });
|
||||
}
|
||||
|
||||
export async function getGraphs(): Promise<Graph[]> {
|
||||
const db = await getDB();
|
||||
return db.getAll(STORE_NAME);
|
||||
}
|
||||
|
||||
export async function clear(): Promise<void> {
|
||||
const db = await getDB();
|
||||
return db.clear(STORE_NAME);
|
||||
}
|
||||
93
app/src/lib/project-manager/project-manager.svelte.ts
Normal file
93
app/src/lib/project-manager/project-manager.svelte.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import * as templates from '$lib/graph-templates';
|
||||
import { localState } from '$lib/helpers/localState.svelte';
|
||||
import type { Graph } from '@nodarium/types';
|
||||
import * as db from './project-database.svelte';
|
||||
|
||||
export class ProjectManager {
|
||||
public graph = $state<Graph>();
|
||||
private projects = $state<Graph[]>([]);
|
||||
private activeProjectId = localState<number | undefined>(
|
||||
'node.activeProjectId',
|
||||
undefined
|
||||
);
|
||||
public readonly loading = $derived(this.graph?.id !== this.activeProjectId.value);
|
||||
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
async saveGraph(g: Graph) {
|
||||
db.saveGraph(g);
|
||||
}
|
||||
|
||||
private async init() {
|
||||
await db.getDB();
|
||||
this.projects = await db.getGraphs();
|
||||
|
||||
console.log('PM: INIT', {
|
||||
projects: this.projects,
|
||||
activeProjectId: this.activeProjectId.value
|
||||
});
|
||||
|
||||
if (this.activeProjectId.value !== undefined) {
|
||||
let loadedGraph = await db.getGraph(this.activeProjectId.value);
|
||||
console.log('PM: LOAD ACTIVE PROJECT', { loadedGraph });
|
||||
if (loadedGraph) {
|
||||
console.log('Load active project');
|
||||
this.graph = loadedGraph;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.graph) {
|
||||
console.log('Load first active project', { projectsAmount: this.projects.length });
|
||||
if (this.projects?.length && this.projects[0]?.id !== undefined) {
|
||||
this.graph = this.projects[0];
|
||||
this.activeProjectId.value = this.graph.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.graph) {
|
||||
console.log('Create default project');
|
||||
this.handleCreateProject();
|
||||
}
|
||||
}
|
||||
|
||||
public handleCreateProject(
|
||||
g: Graph = templates.defaultPlant as unknown as Graph,
|
||||
title: string = 'New Project'
|
||||
) {
|
||||
let id = g?.id || 0;
|
||||
while (this.projects.find((p) => p.id === id)) {
|
||||
id++;
|
||||
}
|
||||
|
||||
console.log('CREATE PROJECT', { id, title });
|
||||
|
||||
g.id = id;
|
||||
if (!g.meta) g.meta = {};
|
||||
if (!g.meta.title) g.meta.title = title;
|
||||
|
||||
db.saveGraph(g);
|
||||
this.projects = [...this.projects, g];
|
||||
this.handleSelectProject(id);
|
||||
}
|
||||
|
||||
public async handleDeleteProject(projectId: number) {
|
||||
await db.deleteGraph(projectId);
|
||||
if (this.projects.length === 1) {
|
||||
this.graph = undefined;
|
||||
this.projects = [];
|
||||
} else {
|
||||
this.projects = this.projects.filter((p) => p.id !== projectId);
|
||||
this.handleSelectProject(this.projects[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
public async handleSelectProject(id: number) {
|
||||
if (this.activeProjectId.value !== id) {
|
||||
const project = await db.getGraph(id);
|
||||
this.graph = project;
|
||||
this.activeProjectId.value = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user