rojects/source/components/project-editor/project-editor.ts

159 lines
5.6 KiB
TypeScript

import { showConfirmDialog } from '../confirm-dialog/confirm-dialog.js';
interface CurrentUser {
userId: string;
email: string;
}
interface Project {
id: string;
name: string;
owner_id: string;
}
interface ProjectMember {
id: string;
project_id: string;
member_type: 'user' | 'group';
member_id: string;
role: string;
}
class ProjectEditor extends HTMLElement {
private currentUser: CurrentUser | null = null;
async connectedCallback(): Promise<void> {
const res = await fetch( '/api/auth/me' );
if ( res.ok ) this.currentUser = await res.json() as CurrentUser;
this.render();
}
async render(): Promise<void> {
const res = await fetch( '/api/projects' );
const projects = await res.json() as Project[];
this.innerHTML = `
<div class="project-editor">
<h2>Projects</h2>
<form class="create-form">
<input name="name" placeholder="New project name" required>
<button type="submit">Create</button>
</form>
<ul class="project-list">
${ projects.map( p => `
<li data-id="${ p.id }">
<div class="project-header">
<strong>${ p.name }</strong>
<a class="btn-edit" href="/editor.html?project=${ p.id }&name=${ encodeURIComponent( p.name ) }">Edit</a>
<button class="btn-members" data-id="${ p.id }">Members</button>
<button class="btn-delete" data-id="${ p.id }" data-name="${ p.name }">Delete</button>
</div>
<div class="members-panel" data-project="${ p.id }" data-owner="${ p.owner_id }" style="display:none"></div>
</li>
` ).join( '' ) }
</ul>
</div>
`;
this.querySelector( '.create-form' )!.addEventListener( 'submit', async ( e: Event ) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const name = ( form.elements.namedItem( 'name' ) as HTMLInputElement ).value.trim();
const res = await fetch( '/api/projects', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify( { name } )
} );
if ( res.ok ) this.render();
} );
this.querySelectorAll( '.btn-delete' ).forEach( btn => {
btn.addEventListener( 'click', async () => {
const el = btn as HTMLElement;
const ok = await showConfirmDialog( {
icon: '🗑',
title: 'Delete Project',
message: `Delete "${ el.dataset.name }"? This cannot be undone.`,
confirmLabel: 'Delete',
cancelLabel: 'Cancel',
danger: true
} );
if ( !ok ) return;
await fetch( `/api/projects/${ el.dataset.id }`, { method: 'DELETE' } );
this.render();
} );
} );
this.querySelectorAll( '.btn-members' ).forEach( btn => {
btn.addEventListener( 'click', () => this.toggleMembers( ( btn as HTMLElement ).dataset.id! ) );
} );
}
async toggleMembers( projectId: string ): Promise<void> {
const panel = this.querySelector( `.members-panel[data-project="${ projectId }"]` ) as HTMLElement;
if ( panel.style.display !== 'none' ) { panel.style.display = 'none'; return; }
panel.style.display = 'block';
await this.loadMembers( projectId, panel );
}
async loadMembers( projectId: string, panel: HTMLElement ): Promise<void> {
const res = await fetch( `/api/projects/${ projectId }/members` );
const members = await res.json() as ProjectMember[];
const ownerId = panel.dataset.owner ?? '';
const isOwner = this.currentUser?.userId === ownerId;
const ownerLabel = isOwner
? `${ this.currentUser!.email } <span class="member-role">owner</span>`
: `${ ownerId } <span class="member-role">owner</span>`;
const memberRows = members.map( m => `
<li>
<span class="member-email">${ m.member_id }</span>
<span class="member-role">${ m.role }</span>
${ isOwner ? `<button class="btn-remove-member" data-pid="${ projectId }" data-mid="${ m.id }">Remove</button>` : '' }
</li>
` ).join( '' );
panel.innerHTML = `
<ul class="member-list">
<li class="member-owner">${ ownerLabel }</li>
${ memberRows || '' }
${ members.length === 0 ? '<li class="empty">No additional members</li>' : '' }
</ul>
${ isOwner ? `
<form class="add-member-form">
<input name="email" type="email" placeholder="Email address" required>
<select name="role">
<option value="viewer">Viewer</option>
<option value="editor">Editor</option>
<option value="admin">Admin</option>
</select>
<button type="submit">Add</button>
</form>
` : '' }
`;
panel.querySelectorAll( '.btn-remove-member' ).forEach( btn => {
btn.addEventListener( 'click', async () => {
const el = btn as HTMLElement;
await fetch( `/api/projects/${ el.dataset.pid }/members/${ el.dataset.mid }`, { method: 'DELETE' } );
await this.loadMembers( projectId, panel );
} );
} );
const addForm = panel.querySelector( '.add-member-form' ) as HTMLFormElement | null;
addForm?.addEventListener( 'submit', async ( e: Event ) => {
e.preventDefault();
const data = Object.fromEntries( new FormData( e.target as HTMLFormElement ) );
await fetch( `/api/projects/${ projectId }/members`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify( data )
} );
await this.loadMembers( projectId, panel );
} );
}
}
customElements.define( 'project-editor', ProjectEditor );