import { showConfirmDialog } from '../confirm-dialog/confirm-dialog.js'; const AUTH_HOST = 'https://account.rokojori.com'; const APP_URL = 'https://roject.rokojori.com'; interface JwtUser { 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; } function hueFromId( id: string ): number { let h = 0; for ( let i = 0; i < id.length; i++ ) { h = ( h * 31 + id.charCodeAt( i ) ) % 360; } return Math.abs( h ); } class ProjectListDefault extends HTMLElement { user: JwtUser | null = null; async connectedCallback(): Promise { await this.render(); } private async render(): Promise { const res = await fetch( '/api/projects' ); const projects = res.ok ? await res.json() as Project[] : []; const logoutHref = `${ AUTH_HOST }/api/auth/logout?redirect=${ encodeURIComponent( APP_URL ) }`; this.innerHTML = `
New Project…
${ projects.map( ( p, i ) => this.rowHtml( p, i ) ).join( '' ) }
`; this.bindEvents( projects ); } private rowHtml( p: Project, i: number ): string { const color = `hsl( ${ hueFromId( p.id ) }, 95%, 45% )`; return `
${ p.name.toUpperCase() }
`; } private bindEvents( projects: Project[] ): void { this.querySelector( '#pld-new-row' )!.addEventListener( 'click', () => this.openCreateDialog() ); this.querySelectorAll( '.pld-row:not(.new-project)' ).forEach( row => { row.addEventListener( 'click', ( e: Event ) => { if ( ( e.target as HTMLElement ).closest( '.pld-btn-delete, .pld-btn-members' ) ) return; const el = row as HTMLElement; location.href = `/editor.html?project=${ el.dataset.id }&name=${ encodeURIComponent( el.dataset.name! ) }`; } ); } ); this.querySelectorAll( '.pld-btn-delete' ).forEach( btn => { btn.addEventListener( 'click', async ( e: Event ) => { e.stopPropagation(); 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' } ); await this.render(); } ); } ); this.querySelectorAll( '.pld-btn-members' ).forEach( btn => { btn.addEventListener( 'click', ( e: Event ) => { e.stopPropagation(); const id = ( btn as HTMLElement ).dataset.id!; const row = this.querySelector( `.pld-row[data-id="${ id }"]` ) as HTMLElement; this.openMembersPanel( id, row ); } ); } ); } private openCreateDialog(): void { const overlay = this.querySelector( '.pld-create-overlay' ) as HTMLElement; overlay.style.display = 'flex'; overlay.innerHTML = `

New Project

`; const panel = overlay.querySelector( '.pld-mp' )!; panel.addEventListener( 'click', e => e.stopPropagation() ); overlay.addEventListener( 'click', () => { overlay.style.display = 'none'; }, { once: true } ); const input = overlay.querySelector( '.pld-cp-input' ) as HTMLInputElement; setTimeout( () => input.focus(), 30 ); overlay.querySelector( '.pld-cp-cancel' )!.addEventListener( 'click', () => { overlay.style.display = 'none'; } ); overlay.querySelector( '.pld-cp-form' )!.addEventListener( 'submit', async ( e: Event ) => { e.preventDefault(); const name = input.value.trim(); if ( !name ) return; overlay.style.display = 'none'; const res = await fetch( '/api/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify( { name } ) } ); if ( res.ok ) await this.render(); } ); } private async openMembersPanel( projectId: string, row: HTMLElement ): Promise { const overlay = this.querySelector( '.pld-members-overlay' ) as HTMLElement; overlay.style.display = 'flex'; overlay.innerHTML = `

Loading…

`; const panel = overlay.querySelector( '.pld-mp' )!; panel.addEventListener( 'click', e => e.stopPropagation() ); overlay.addEventListener( 'click', () => { overlay.style.display = 'none'; }, { once: true } ); const res = await fetch( `/api/projects/${ projectId }/members` ); const members = res.ok ? await res.json() as ProjectMember[] : []; const ownerId = row.dataset.owner ?? ''; const isOwner = this.user?.userId === ownerId; const ownerLabel = isOwner ? this.user!.email : ownerId; panel.innerHTML = `

${ row.dataset.name }

  • ${ ownerLabel } owner
  • ${ members.map( m => `
  • ${ m.member_id } ${ m.role } ${ isOwner ? `` : '' }
  • ` ).join( '' ) } ${ members.length === 0 ? '
  • No additional members
  • ' : '' }
${ isOwner ? `
` : '' } `; panel.querySelector( '.pld-mp-close' )!.addEventListener( 'click', () => { overlay.style.display = 'none'; } ); panel.querySelectorAll( '.pld-mp-remove' ).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.openMembersPanel( projectId, row ); } ); } ); const addForm = panel.querySelector( '.pld-mp-add-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.openMembersPanel( projectId, row ); } ); } } customElements.define( 'project-list-default', ProjectListDefault );