diff --git a/electron/main.ts b/electron/main.ts index 47b5c21..e2d96f7 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -117,7 +117,7 @@ function createMainWindow(): void { title: 'Roject', } ); - mainWindow.loadURL( `http://localhost:${PORT}/dashboard.html` ); + mainWindow.loadURL( `http://localhost:${PORT}/` ); const localBase = `http://localhost:${PORT}/`; @@ -137,7 +137,7 @@ function createMainWindow(): void { if ( url.includes( '/api/auth/refresh-session' ) ) { event.preventDefault(); const redirectParam = new URL( url ).searchParams.get( 'redirect' ); - const fallback = `http://localhost:${PORT}/dashboard.html`; + const fallback = `http://localhost:${PORT}/`; if ( currentTokens ) { const fresh = await refreshTokens( currentTokens.refreshToken ); diff --git a/source/components/editor-shell/editor-shell.ts b/source/components/editor-shell/editor-shell.ts index 9049be9..f396101 100644 --- a/source/components/editor-shell/editor-shell.ts +++ b/source/components/editor-shell/editor-shell.ts @@ -66,7 +66,7 @@ class EditorShell extends HTMLElement { this.innerHTML = `
- + ${projectName}
diff --git a/source/components/project-home/project-home.css b/source/components/project-home/project-home.css new file mode 100644 index 0000000..3b5f148 --- /dev/null +++ b/source/components/project-home/project-home.css @@ -0,0 +1,46 @@ +project-home { + display: block; + width: 100%; + min-height: 100vh; +} + +.ph-hero { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + background: radial-gradient( ellipse at 40% 35%, #282b3c 0%, #000 70% ); + gap: 2.5rem; +} + +.ph-brand { + font-family: 'Barlow', sans-serif; + font-weight: 900; + font-style: italic; + font-size: clamp( 3rem, 12vw, 7rem ); + text-transform: uppercase; + letter-spacing: 0.03em; + color: #fff; + text-shadow: 0 0 80px rgba( 255, 255, 255, 0.12 ); +} + +.ph-login { + font-family: 'Barlow', sans-serif; + font-weight: 700; + font-style: italic; + text-transform: uppercase; + letter-spacing: 0.1em; + font-size: 1rem; + color: rgba( 255, 255, 255, 0.6 ); + text-decoration: none; + padding: 0.65rem 2.2rem; + border: 1px solid rgba( 255, 255, 255, 0.25 ); + border-radius: 4px; + transition: border-color 0.2s, color 0.2s; +} + +.ph-login:hover { + border-color: rgba( 255, 255, 255, 0.7 ); + color: #fff; +} diff --git a/source/components/project-home/project-home.ts b/source/components/project-home/project-home.ts new file mode 100644 index 0000000..04bac40 --- /dev/null +++ b/source/components/project-home/project-home.ts @@ -0,0 +1,52 @@ +const AUTH_HOST = 'https://account.rokojori.com'; +const APP_URL = 'https://roject.rokojori.com'; + +interface JwtUser { + userId: string; + email: string; + roles: string[]; + products: string[]; +} + +class ProjectHome extends HTMLElement { + async connectedCallback(): Promise { + const res = await fetch( '/api/auth/me' ); + + if ( !res.ok ) { + const loginHref = `${ AUTH_HOST }/login.html?redirect=${ encodeURIComponent( APP_URL ) }`; + this.innerHTML = ` +
+

Roject

+ +
+ `; + return; + } + + const user = await res.json() as JwtUser; + const settingsRes = await fetch( '/api/user/settings' ); + const settings = settingsRes.ok ? await settingsRes.json() as Record : {}; + + const theme = ( settings[ 'theme' ] as string | undefined ) ?? 'default'; + await this.loadTheme( theme, user ); + } + + private async loadTheme( theme: string, user: JwtUser ): Promise { + if ( theme === 'italic-neon' ) { + const italicNeonPath = '../project-list-italic-neon/project-list-italic-neon.js'; + await import( italicNeonPath ); + const el = document.createElement( 'project-list-italic-neon' ) as HTMLElement & { user: JwtUser }; + el.user = user; + this.appendChild( el ); + } else { + await import( '../project-list-default/project-list-default.js' ); + const el = document.createElement( 'project-list-default' ) as HTMLElement & { user: JwtUser }; + el.user = user; + this.appendChild( el ); + } + } +} + +customElements.define( 'project-home', ProjectHome ); + +export {}; diff --git a/source/components/project-list-default/project-list-default.css b/source/components/project-list-default/project-list-default.css new file mode 100644 index 0000000..6aecd9b --- /dev/null +++ b/source/components/project-list-default/project-list-default.css @@ -0,0 +1,460 @@ +project-list-default { + display: block; + min-height: 100vh; + background: radial-gradient( ellipse at 40% 35%, #282b3c 0%, #000 70% ); + color: #fff; + overflow-x: hidden; +} + +/* ── Nav ─────────────────────────────────── */ + +.pld-nav { + display: flex; + align-items: center; + padding: 0 2rem; + height: 84px; + position: fixed; + overflow: visible; + width: 100vw; + +} + +/* Logo: wrapper = inner area (570×240 at 0.35x = 200×84). + Full image (1592×637 at 0.35x = 557×223) positioned so inner + area aligns with wrapper — decorative FX bleed outside. */ +.pld-logo-wrap { + position: relative; + width: 200px; + height: 84px; + flex-shrink: 0; + overflow: visible; + margin-left: -40px; +} + +.pld-logo { + width: 557px; + height: auto; + position: absolute; + left: -179px; + top: -69px; + pointer-events: none; + user-select: none; +} + +/* User group — email + logout, whole thing is one click target */ +.pld-user-group { + position: relative; + margin-left: auto; + text-decoration: none; + cursor: pointer; + display: block; + line-height: 1; +} + +.pld-email { + display: block; + font-size: 0.82rem; + color: rgba( 255, 255, 255, 0.38 ); + transition: color 0.15s; + white-space: nowrap; +} + +.pld-user-group:hover .pld-email { + color: rgba( 255, 255, 255, 0.7 ); +} + +.pld-logout-label { + position: absolute; + top: 100%; + right: 0; + margin-top: 0.35em; + font-family: 'Barlow', sans-serif; + font-weight: 700; + font-style: italic; + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 1.25rem; + color: rgba( 255, 255, 255, 0.55 ); + white-space: nowrap; + opacity: 0; + transform: translateY( -5px ); + transition: opacity 0.15s, transform 0.15s; + +} + +.pld-user-group:hover .pld-logout-label { + opacity: 1; + transform: translateY( 0 ); +} + +/* ── Main layout ─────────────────────────── */ + +.pld-main { + padding: 2.5rem 2rem 5rem; + max-width: 80em; + margin: auto; +} + +/* ── Project list ────────────────────────── */ + +.pld-list { + display: flex; + flex-direction: column; + gap: 0.1rem; + margin-bottom: 3rem; +} + +@keyframes pld-row-in { + from { opacity: 0; transform: translateX( -18px ); } + to { opacity: 1; transform: translateX( 0 ); } +} + +.pld-row { + display: flex; + align-items: center; + gap: 1.5rem; + padding: 2rem 2rem; + cursor: pointer; + border-radius: 26px; + position: relative; + animation: pld-row-in 0.32s cubic-bezier( 0.22, 0.68, 0, 1.15 ) both; + transition: background 0.18s; + overflow: visible; + margin: 0.5em 0em +} + +.pld-row:hover .pld-name +{ + text-shadow: 0em 0.03em 0.2em hsl(200, 90%, 10%); +} + + +.pld-row:hover { + background: rgba( 255, 255, 255, 0.035 ); +} + +/* ── Badge ───────────────────────────────── */ + +.pld-badge-wrap { + position: relative; + flex-shrink: 0; + width: 7em; + height: 7em; + margin-right: 1.5em; +} + +.pld-badge { + width: 100%; + height: 100%; + border-radius: 15%; + background: radial-gradient(circle at 38% 37%, color-mix(in srgb, var(--project-color) 51%, transparent), var(--project-color)); +} + +.new-project .pld-badge { + outline-style: dashed; + outline-width: 0.3em; + outline-color: #fff; + outline-offset: 2px; + background: transparent; +} + +.new-project .pld-name { + color: rgba( 255, 255, 255, 0.8 ); +} + +.new-project:hover .pld-name { + color: #fff; +} + +.new-project .pld-btn-delete, +.new-project .pld-btn-members { + display: none; + pointer-events: none; +} + +/* Create project dialog overlay */ +.pld-create-overlay { + position: fixed; + inset: 0; + background: rgba( 0, 0, 0, 0.72 ); + display: flex; + align-items: center; + justify-content: center; + z-index: 200; +} + +.pld-cp-input { + width: 100%; + box-sizing: border-box; +} + +.pld-cp-actions { + display: flex; + gap: 0.5rem; + justify-content: flex-end; + margin-top: 1rem; +} + +/* ── Overlay buttons on badge ────────────── */ + +.pld-btn-delete, +.pld-btn-members { + position: absolute; + width: 24px; + height: 24px; + border-radius: 50%; + border: none; + padding: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transform: scale( 0.6 ); + transition: opacity 0.14s, transform 0.14s; + z-index: 5; + box-shadow: 0 2px 8px rgba( 0, 0, 0, 0.55 ); +} + +.pld-row:hover .pld-btn-delete, +.pld-row:hover .pld-btn-members { + opacity: 1; + transform: scale( 1 ); +} + +.pld-btn-delete { + top: -7px; + left: -7px; + background: #b02050; +} + +.pld-btn-delete:hover { background: #e93978; } + +.pld-btn-members { + bottom: -7px; + left: -7px; + background: #0574a8; +} + +.pld-btn-members:hover { background: #04bdff; } + +.pld-btn-delete svg, +.pld-btn-members svg { + width: 14px; + height: 14px; +} + +/* ── Project name ────────────────────────── */ + +.pld-name { + font-family: 'Barlow', sans-serif; + font-weight: 900; + font-style: italic; + font-size: 5em; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var( --project-color ); + transform: matrix( 0.953, 0, -0.102, 1.049, 0, 0 ); + transform-origin: left center; + display: inline-block; + user-select: none; + text-shadow: 0em 0.03em 0.2em hsl(200, 90%, 10%,0); +} + +/* ── Create form ─────────────────────────── */ + +.pld-create-form { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.pld-create-input { + flex: 1; + max-width: 300px; + padding: 0.55rem 0.9rem; + background: rgba( 255, 255, 255, 0.05 ); + border: 1px solid rgba( 255, 255, 255, 0.14 ); + border-radius: 6px; + color: #fff; + font-size: 0.88rem; + font-family: inherit; + outline: none; + transition: border-color 0.15s; +} + +.pld-create-input::placeholder { color: rgba( 255, 255, 255, 0.28 ); } +.pld-create-input:focus { border-color: rgba( 255, 255, 255, 0.38 ); } + +.pld-create-btn { + padding: 0.55rem 1.3rem; + background: rgba( 255, 255, 255, 0.07 ); + border: 1px solid rgba( 255, 255, 255, 0.18 ); + border-radius: 6px; + color: rgba( 255, 255, 255, 0.75 ); + font-family: 'Barlow', sans-serif; + font-weight: 700; + font-style: italic; + text-transform: uppercase; + letter-spacing: 0.07em; + font-size: 0.82rem; + cursor: pointer; + transition: background 0.15s, color 0.15s; +} + +.pld-create-btn:hover { + background: rgba( 255, 255, 255, 0.13 ); + color: #fff; +} + +/* ── Members overlay ─────────────────────── */ + +.pld-members-overlay { + position: fixed; + inset: 0; + background: rgba( 0, 0, 0, 0.72 ); + display: flex; + align-items: center; + justify-content: center; + z-index: 200; +} + +.pld-mp { + background: #131320; + border: 1px solid rgba( 255, 255, 255, 0.1 ); + border-radius: 12px; + padding: 1.75rem 2rem; + width: min( 480px, 92vw ); + max-height: 80vh; + overflow-y: auto; +} + +.pld-mp-title { + font-family: 'Barlow', sans-serif; + font-weight: 900; + font-style: italic; + text-transform: uppercase; + font-size: 1.25rem; + letter-spacing: 0.04em; + color: #fff; + margin-bottom: 1.25rem; +} + +.pld-mp-list { + list-style: none; + padding: 0; + margin: 0 0 1rem; +} + +.pld-mp-list li { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.5rem 0; + border-bottom: 1px solid rgba( 255, 255, 255, 0.06 ); + font-size: 0.88rem; +} + +.pld-mp-owner-row { font-weight: 500; } + +.pld-mp-email { + flex: 1; + color: rgba( 255, 255, 255, 0.75 ); + word-break: break-all; +} + +.pld-mp-role { + font-size: 0.72rem; + color: rgba( 255, 255, 255, 0.38 ); + padding: 0.15rem 0.5rem; + border: 1px solid rgba( 255, 255, 255, 0.14 ); + border-radius: 3px; + white-space: nowrap; +} + +.pld-mp-empty { + color: rgba( 255, 255, 255, 0.32 ); + font-size: 0.85rem; + padding: 0.5rem 0; +} + +.pld-mp-loading { + color: rgba( 255, 255, 255, 0.45 ); + font-size: 0.9rem; + text-align: center; + padding: 2rem 0; +} + +.pld-mp-add-form { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + margin-top: 1.25rem; +} + +.pld-mp-input { + flex: 1; + min-width: 150px; + padding: 0.45rem 0.7rem; + background: rgba( 255, 255, 255, 0.05 ); + border: 1px solid rgba( 255, 255, 255, 0.14 ); + border-radius: 5px; + color: #fff; + font-size: 0.85rem; + font-family: inherit; +} + +.pld-mp-select { + padding: 0.45rem 0.6rem; + background: rgba( 255, 255, 255, 0.05 ); + border: 1px solid rgba( 255, 255, 255, 0.14 ); + border-radius: 5px; + color: #fff; + font-size: 0.85rem; +} + +.pld-mp-btn { + padding: 0.45rem 0.9rem; + background: rgba( 255, 255, 255, 0.07 ); + border: 1px solid rgba( 255, 255, 255, 0.17 ); + border-radius: 5px; + color: rgba( 255, 255, 255, 0.75 ); + cursor: pointer; + font-size: 0.85rem; + transition: background 0.14s, color 0.14s; +} + +.pld-mp-btn:hover { + background: rgba( 255, 255, 255, 0.13 ); + color: #fff; +} + +.pld-mp-close { + margin-top: 1.25rem; + width: 100%; + justify-content: center; +} + +/* ── Mobile ──────────────────────────────── */ + +@media ( max-width: 600px ) { + .pld-name { + transform: none; + font-size: 1.15rem; + } + + .pld-row { + animation: none; + opacity: 1; + } + + .pld-btn-delete, + .pld-btn-members { + opacity: 1; + transform: scale( 1 ); + } + + .pld-main { + padding: 1.5rem 1rem 4rem; + } +} diff --git a/source/components/project-list-default/project-list-default.ts b/source/components/project-list-default/project-list-default.ts new file mode 100644 index 0000000..302fd43 --- /dev/null +++ b/source/components/project-list-default/project-list-default.ts @@ -0,0 +1,248 @@ +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 ); diff --git a/source/pages/index.html b/source/pages/index.html new file mode 100644 index 0000000..297360d --- /dev/null +++ b/source/pages/index.html @@ -0,0 +1,25 @@ + + + + + + Roject + + + + + + + + + + + + + + + diff --git a/source/rojos/roject-logo-570x240-1592x637.webp b/source/rojos/roject-logo-570x240-1592x637.webp new file mode 100644 index 0000000..04fada3 Binary files /dev/null and b/source/rojos/roject-logo-570x240-1592x637.webp differ diff --git a/source/rojos/roject-logo.webp b/source/rojos/roject-logo.webp new file mode 100644 index 0000000..04fada3 Binary files /dev/null and b/source/rojos/roject-logo.webp differ diff --git a/source/server/db.ts b/source/server/db.ts index 4fd8c7c..d472ea7 100644 --- a/source/server/db.ts +++ b/source/server/db.ts @@ -88,6 +88,24 @@ export const projects = { } }; +export interface UserSetting { + userId: string; + settings: Record; +} + +export const userSettings = { + forUser( userId: string ): Record | null { + return read( 'user_settings' ).find( s => s.userId === userId )?.settings ?? null; + }, + save( userId: string, settings: Record ): Record { + const all = read( 'user_settings' ); + const idx = all.findIndex( s => s.userId === userId ); + if ( idx >= 0 ) { all[ idx ].settings = settings; } else { all.push( { userId, settings } ); } + write( 'user_settings', all ); + return settings; + } +}; + export const projectMembers = { forProject: (projectId: string): ProjectMember[] => read('project_members').filter(m => m.project_id === projectId), add(data: Omit): ProjectMember { diff --git a/source/server/index.ts b/source/server/index.ts index 3b87fdf..58e725f 100644 --- a/source/server/index.ts +++ b/source/server/index.ts @@ -3,12 +3,12 @@ import cookieParser from 'cookie-parser'; import path from 'path'; import { ROOT } from './rootDir'; import { jwtMiddleware, requireAuth } from './middleware/auth'; -import groupsRouter from './routes/groups'; import projectsRouter from './routes/projects'; import filesRouter from './routes/files'; import localesRouter from './routes/locales'; import layoutRouter from './routes/layout'; import rojosRouter from './routes/rojos'; +import userSettingsRouter from './routes/userSettings'; import { generateLocales } from './localeGenerator'; import deployRouter from './routes/deploy'; import { EmailService } from './email/EmailService'; @@ -25,16 +25,16 @@ app.use( jwtMiddleware ); app.use( express.static( path.join( ROOT, 'build', 'app' ) ) ); -app.use( '/api/groups', groupsRouter ); app.use( '/api/projects', projectsRouter ); app.use( '/api/files', filesRouter ); app.use( '/api/locales', localesRouter ); app.use( '/api/layout', layoutRouter ); app.use( '/api/rojos', rojosRouter ); +app.use( '/api/user/settings', userSettingsRouter ); app.get( '/api/auth/me', requireAuth, ( req, res ) => res.json( req.user ) ); -app.get( '/', ( _req, res ) => res.redirect( '/dashboard.html' ) ); +app.get( '/edit', ( _req, res ) => res.redirect( '/editor.html' ) ); export function startServer( port: number ): void { app.listen( port, () => diff --git a/source/server/routes/userSettings.ts b/source/server/routes/userSettings.ts new file mode 100644 index 0000000..6d173f0 --- /dev/null +++ b/source/server/routes/userSettings.ts @@ -0,0 +1,17 @@ +import { Router } from 'express'; +import { requireAuth } from '../middleware/auth'; +import { userSettings } from '../db'; + +const router = Router(); +router.use( requireAuth ); + +router.get( '/', ( req, res ) => { + res.json( userSettings.forUser( req.user!.userId ) ?? {} ); +} ); + +router.put( '/', ( req, res ) => { + const settings = req.body as Record; + res.json( userSettings.save( req.user!.userId, settings ) ); +} ); + +export default router; diff --git a/workspace/_assets_/nav-data.js b/workspace/_assets_/nav-data.js index 294beac..f445e21 100644 --- a/workspace/_assets_/nav-data.js +++ b/workspace/_assets_/nav-data.js @@ -33,6 +33,7 @@ var NAV_DATA = { title: 'Actions', path: 'actions/index.html', children: [ + { title: 'Update Board', path: 'actions/update-board/index.html' }, { title: 'Update History', path: 'actions/update-history/index.html' }, { title: 'Update Outline', path: 'actions/update-outline/index.html' }, ] diff --git a/workspace/_assets_/roject.svg b/workspace/_assets_/roject.svg index 321fb7c..8d9a168 100644 --- a/workspace/_assets_/roject.svg +++ b/workspace/_assets_/roject.svg @@ -25,15 +25,15 @@ inkscape:deskcolor="#333333" inkscape:document-units="px" showgrid="false" - inkscape:zoom="0.20890503" - inkscape:cx="1605.9929" - inkscape:cy="1093.7985" + inkscape:zoom="0.14771816" + inkscape:cx="1397.9324" + inkscape:cy="470.49055" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" - inkscape:current-layer="g17" />ROJECTROJECTROJECTROJECTROJECTROJECTROJECTROJECTCOMPUTER! ZOOM IN!COMPUTER! ZOOM IN!COMPUTER! ZOOM IN!COMPUTER! ZOOM IN!COMPUTER! ZOOM IN!COMPUTER! ZOOM IN!ROJECTROJECTROJECT + + +MY PROJECTSUPER COOL IDEASECRET PROJECTSECRET PROJECTROJECTROJECTROJECTROJECTROJECTROJECTROJECTCOMPUTER! ZOOM IN!COMPUTER! ZOOM IN!COMPUTER! ZOOM IN!josef@rokojori.com diff --git a/workspace/_assets_/themes/italic-neon-patterns/pattern-01.svg b/workspace/_assets_/themes/italic-neon-patterns/pattern-01.svg new file mode 100644 index 0000000..c1f4224 --- /dev/null +++ b/workspace/_assets_/themes/italic-neon-patterns/pattern-01.svg @@ -0,0 +1,372 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 25 + 50 + 75 + 100 + 0 + 25 + 50 + 75 + 100 + + + + + + + + diff --git a/workspace/_assets_/themes/italic-neon-patterns/pattern-02.svg b/workspace/_assets_/themes/italic-neon-patterns/pattern-02.svg new file mode 100644 index 0000000..5ac415d --- /dev/null +++ b/workspace/_assets_/themes/italic-neon-patterns/pattern-02.svg @@ -0,0 +1,381 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 25 + 50 + 75 + 100 + 0 + 25 + 50 + 75 + 100 + + + + + + + + + diff --git a/workspace/_assets_/themes/italic-neon-patterns/pattern-03.svg b/workspace/_assets_/themes/italic-neon-patterns/pattern-03.svg new file mode 100644 index 0000000..9151877 --- /dev/null +++ b/workspace/_assets_/themes/italic-neon-patterns/pattern-03.svg @@ -0,0 +1,381 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 25 + 50 + 75 + 100 + 0 + 25 + 50 + 75 + 100 + + + + + + + + + diff --git a/workspace/_assets_/themes/italic-neon-patterns/pattern-04.svg b/workspace/_assets_/themes/italic-neon-patterns/pattern-04.svg new file mode 100644 index 0000000..3b16659 --- /dev/null +++ b/workspace/_assets_/themes/italic-neon-patterns/pattern-04.svg @@ -0,0 +1,392 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 25 + 50 + 75 + 100 + 0 + 25 + 50 + 75 + 100 + + + + + + + + + + diff --git a/workspace/_assets_/themes/italic-neon-patterns/pattern-05.svg b/workspace/_assets_/themes/italic-neon-patterns/pattern-05.svg new file mode 100644 index 0000000..e904f49 --- /dev/null +++ b/workspace/_assets_/themes/italic-neon-patterns/pattern-05.svg @@ -0,0 +1,401 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 25 + 50 + 75 + 100 + 0 + 25 + 50 + 75 + 100 + + + + + + + + + + + diff --git a/workspace/_assets_/themes/italic-neon-patterns/pattern-06.svg b/workspace/_assets_/themes/italic-neon-patterns/pattern-06.svg new file mode 100644 index 0000000..add55ce --- /dev/null +++ b/workspace/_assets_/themes/italic-neon-patterns/pattern-06.svg @@ -0,0 +1,401 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 25 + 50 + 75 + 100 + 0 + 25 + 50 + 75 + 100 + + + + + + + + + + + diff --git a/workspace/_assets_/themes/italic-neon-patterns/pattern-07.svg b/workspace/_assets_/themes/italic-neon-patterns/pattern-07.svg new file mode 100644 index 0000000..70d77dd --- /dev/null +++ b/workspace/_assets_/themes/italic-neon-patterns/pattern-07.svg @@ -0,0 +1,411 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 25 + 50 + 75 + 100 + 0 + 25 + 50 + 75 + 100 + + + + + + + + + + + + diff --git a/workspace/_assets_/themes/italic-neon-patterns/pattern-08.svg b/workspace/_assets_/themes/italic-neon-patterns/pattern-08.svg new file mode 100644 index 0000000..0bce866 --- /dev/null +++ b/workspace/_assets_/themes/italic-neon-patterns/pattern-08.svg @@ -0,0 +1,411 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 25 + 50 + 75 + 100 + 0 + 25 + 50 + 75 + 100 + + + + + + + + + + + + diff --git a/workspace/_assets_/themes/italic-neon-patterns/pattern-09.svg b/workspace/_assets_/themes/italic-neon-patterns/pattern-09.svg new file mode 100644 index 0000000..0df3c77 --- /dev/null +++ b/workspace/_assets_/themes/italic-neon-patterns/pattern-09.svg @@ -0,0 +1,391 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 25 + 50 + 75 + 100 + 0 + 25 + 50 + 75 + 100 + + + + + + + + + + diff --git a/workspace/_assets_/themes/italic-neon-patterns/pattern-10.svg b/workspace/_assets_/themes/italic-neon-patterns/pattern-10.svg new file mode 100644 index 0000000..b46d53c --- /dev/null +++ b/workspace/_assets_/themes/italic-neon-patterns/pattern-10.svg @@ -0,0 +1,392 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 25 + 50 + 75 + 100 + 0 + 25 + 50 + 75 + 100 + + + + + + + + + + diff --git a/workspace/_assets_/themes/italic-neon-patterns/pattern-11.svg b/workspace/_assets_/themes/italic-neon-patterns/pattern-11.svg new file mode 100644 index 0000000..0d8f2c4 --- /dev/null +++ b/workspace/_assets_/themes/italic-neon-patterns/pattern-11.svg @@ -0,0 +1,412 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 25 + 50 + 75 + 100 + 0 + 25 + 50 + 75 + 100 + + + + + + + + + + + + diff --git a/workspace/_assets_/themes/italic-neon-patterns/pattern-12.svg b/workspace/_assets_/themes/italic-neon-patterns/pattern-12.svg new file mode 100644 index 0000000..7a7a8b5 --- /dev/null +++ b/workspace/_assets_/themes/italic-neon-patterns/pattern-12.svg @@ -0,0 +1,402 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 25 + 50 + 75 + 100 + 0 + 25 + 50 + 75 + 100 + + + + + + + + + + + diff --git a/workspace/_assets_/themes/italic-neon-patterns/pattern-13.svg b/workspace/_assets_/themes/italic-neon-patterns/pattern-13.svg new file mode 100644 index 0000000..0f8eb90 --- /dev/null +++ b/workspace/_assets_/themes/italic-neon-patterns/pattern-13.svg @@ -0,0 +1,391 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 25 + 50 + 75 + 100 + 0 + 25 + 50 + 75 + 100 + + + + + + + + + + diff --git a/workspace/_assets_/themes/italic-neon-patterns/template.svg b/workspace/_assets_/themes/italic-neon-patterns/template.svg new file mode 100644 index 0000000..04d769d --- /dev/null +++ b/workspace/_assets_/themes/italic-neon-patterns/template.svg @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 25 + 50 + 75 + 100 + 0 + 25 + 50 + 75 + 100 + + + + + + + + + + diff --git a/workspace/_assets_/themes/italic-neon.svg b/workspace/_assets_/themes/italic-neon.svg new file mode 100644 index 0000000..4366854 --- /dev/null +++ b/workspace/_assets_/themes/italic-neon.svg @@ -0,0 +1,640 @@ + + + +MY-OWN-PROJECTMY-OWN-PROJECTMY-OWN-PROJECTVIVI NUMBER ONEVIVI NUMBER ONESOME OTHER PROJECTSOME OTHER PROJECTjosef@rokojori.com diff --git a/workspace/actions/index.html b/workspace/actions/index.html index 91aab49..ebbaabf 100644 --- a/workspace/actions/index.html +++ b/workspace/actions/index.html @@ -20,6 +20,18 @@
+
+

Update Board

+

+ Review what was done in a session and keep the three boards accurate — + moving completed tasks and bugs to Done, adding new To Do items, and + recommending whether Update Outline is also needed. +

+

+ Read the action +

+
+

Update History

diff --git a/workspace/actions/update-board/index.html b/workspace/actions/update-board/index.html new file mode 100644 index 0000000..a03c335 --- /dev/null +++ b/workspace/actions/update-board/index.html @@ -0,0 +1,98 @@ + + + + + + Update Board — Roject + + + + +

+ +
+

Update Board

+

Review what was done in this session and keep the three boards accurate.

+
+ +
+

Summary

+ +
+

+ At an appropriate point in a working session, read the boards and the recent git + history, then propose which items should move, be added, or be removed. + Present the proposal to the user first. Only after approval, apply the changes. + Never move items silently. +

+
+
+ +
+

Steps

+ +
+

Step 1 — Read the current state

+

Read all three board files:

+
workspace/boards/tasks.html
+workspace/boards/bugs.html
+workspace/boards/backlog.html
+

+ Also read the recent git log and the current session's history entry (if one exists) + to understand what was actually completed. +

+
+ +
+

Step 2 — Propose changes

+

+ Present a clear proposal to the user before touching any file. + For each proposed change, state the item name, the board, and the action: +

+
    +
  • Move to Done — task or bug that was completed this session
  • +
  • Move to In Progress — task that was started but not finished
  • +
  • Move to To Do — new task worth adding now
  • +
  • Remove — item that is no longer relevant
  • +
  • Add to Backlog — idea or future work surfaced during the session
  • +
+

+ Also note whether Update Outline should be run — recommend it + when new systems, subsystems, or conventions were added that the outline does not + yet describe. +

+

Wait for the user to confirm before continuing.

+
+ +
+

Step 3 — Apply the changes

+

+ After the user approves the proposal, edit the board HTML files accordingly. + Use the correct colour class for each lane: +

+
    +
  • class="green hide-content" — Done (Tasks)
  • +
  • class="red hide-content" — Critical (Bugs)
  • +
  • class="yellow hide-content" — In Progress (Tasks) or Visual/UI/UX (Bugs)
  • +
  • class="blue hide-content" — To Do (Tasks) or MVP (Backlog)
  • +
  • class="purple hide-content" — Nice To Have (Backlog)
  • +
  • class="orange hide-content" — To The Moon (Backlog)
  • +
+

+ Keep the Done lane in Tasks focused — if it grows beyond three or four items, + ask the user whether old Done items should be cleared out. +

+
+ +
+ +
+ Roject — update board +
+ +
+ + + + + diff --git a/workspace/boards/bugs.html b/workspace/boards/bugs.html index 2026bb3..cb6a9e7 100644 --- a/workspace/boards/bugs.html +++ b/workspace/boards/bugs.html @@ -22,15 +22,6 @@
Critical
- - Projects Lookup/Editing Broken - - The projects a user can view should only be the ones the user created - or is a member of. Currently a second account can open and edit the content - of a project belonging to the first account. - - - 401 Not Handled in Data-Fetching Components @@ -69,11 +60,28 @@ - +
+ +
+
Done
+ + + Projects Lookup/Editing Broken + + Fixed. GET /api/projects now filters to owner-or-member only. All file routes + (tree, read, write, rename, delete) go through checkAccess() in projectAccess.ts. + Delete and member-management routes verify ownership. A single memberMatchesUser() + function centralises the comparison so migrating from email to user ID later + requires changing one line. + + + + Member List Shows Raw UUIDs - The member list UI displays raw UUIDs instead of usernames. - Should resolve user IDs to display names via the auth service. + Fixed. Members are now stored by email (Option B interim). The member panel + shows the owner's email first, then added members by email with their role badge. + Add-member form takes an email address and role; the member_type select is gone. diff --git a/workspace/boards/tasks.html b/workspace/boards/tasks.html index e2aca73..16f999d 100644 --- a/workspace/boards/tasks.html +++ b/workspace/boards/tasks.html @@ -56,10 +56,13 @@ - CI deploy email notification + Investigate Gitea webhook auto-deploy - The /api/deploy endpoint should send an email notification after each successful - restart so deploys are visible without checking server logs. + The webhook did not fire on the last two pushes to main. Check the Gitea + webhook delivery log for the response code from /api/deploy. Also run + journalctl -u roject -n 100 on the server to see whether the endpoint + was reached at all. Most likely causes: signature mismatch, wrong branch + ref, or the deploy command failing silently. @@ -89,6 +92,16 @@
Done
+ + CI deploy email notification + + EmailService added to Roject (SMTP via Nodemailer, same credentials as rokojori-auth). + Startup email sent from startServer() listen callback; deploy email sent from + /api/deploy after signature verification passes. reportEmail defined as a static + field on EmailService. + + + Electron Desktop App Shell diff --git a/workspace/history/2026/07-July/14-Monday/Redesign.txt b/workspace/history/2026/07-July/14-Monday/Redesign.txt new file mode 100644 index 0000000..38c4855 --- /dev/null +++ b/workspace/history/2026/07-July/14-Monday/Redesign.txt @@ -0,0 +1,41 @@ +[ Redesign of Roject ] +- Dashboard, Groups and Projects will be removed as html +- Effectively there will be: +a) "/" ( one for not logged in, one for logged in ) +b) "/edit" + +When the user is not logged in, the dynamic roject svg will be shown. + +Else the overview of the projects is shown. + +[ UX Update ] +Every theme will be able to edit the project by hovering and clicking on it. +Member management and project deletion are button overlays that only appear on the left side of the element. +On top left there will be an circle with an X for deletion, on bottom left there will be an icon for member management. + +[ Different themes ] +The styling/themes of the entry point/project list will change dynamically everyday (or when the user clicks a button). +There will be one styling that matches the roject logo, which will be used when the user chooses to not have different themes. +The themes should be a little bit an interactive journey, with a strong focus on flash websites from around 2008-2010. +They will have big, bold fonts, strong colors, a lot of animations. We will create the themes one by one together. +We would start with two: The default one and the italic-neon one. + + +[ Default Theme ] +The default theme uses Barlow as font-family. Every project has one color that is chosen automatically. +It has one rounded rect left and the title is written in uppercase Barlow Bold Italic. +A mock is here: workspace/_assets_/themes/default.svg + +[ Italic Neon heme ] +The theme uses Barlow as font-family. Every project has one color that is chosen automatically. +It has one procedural icon on the left and the title is written in uppercase Barlow Bold Italic. +Every element has a background skewed rounded rect that has a slight gradient from warmer to colder color from left to right. +The icon is a skewed rounded rect with a glow. Inside is a black skwed rounded rect and on top +there are 1-8 white skewed circles, which can be a third high/wide and can be placed to form a symbol/glyph, mainly non overlapping +with strong geometric relations, symmetry prefered. +A mock is here: workspace/_assets_/themes/italic-neon.svg + + +For both description try to use the information from the image, because the text can have missing information. For instance the colors +vary slightly in Italic Neon from warm (hue towards yellow, less satur, brighter ) to cold (hue towards blue, more satur, darker ) + diff --git a/workspace/history/2026/07-July/14-Monday/index.html b/workspace/history/2026/07-July/14-Monday/index.html index f96f4ba..3eba942 100644 --- a/workspace/history/2026/07-July/14-Monday/index.html +++ b/workspace/history/2026/07-July/14-Monday/index.html @@ -208,13 +208,96 @@
+
+

Project home redesign

+ +
+

Backend: user settings & server cleanup

+

+ Added UserSetting interface and userSettings CRUD module + to source/server/db.ts (persisted in user_settings.json). + Created source/server/routes/userSettings.ts with + GET / PUT /api/user/settings (requires auth). + Cleaned up source/server/index.ts: removed the groups router and + explicit / redirect; added /edit/editor.html + redirect and wired the new userSettings route. +

+
+ user_settings.json + /api/user/settings + groups router removed +
+
+ +
+

project-home component

+

+ New source/pages/index.html entry point loads Barlow 900 Italic + from Google Fonts and the project-home component. + project-home checks auth: logged-out → full-screen dark hero with + brand text and login link; logged-in → fetches user settings, reads + settings.theme, and dynamically imports the matching theme component + (project-list-default by default; project-list-italic-neon + planned). +

+
+ source/pages/index.html + project-home + dynamic import +
+
+ +
+

project-list-default component (default theme)

+

+ Full default theme: dark radial-gradient background, fixed nav with Roject logo + (webp, inner area positioned at 70% scale) and a user-group element where hover + slides in "Log out" beneath the email — clicking anywhere triggers logout. + Each project row: 7em×7em rounded badge with project colour (hue from UUID via + 31-hash), radial-gradient fill (centre at 51% alpha), Barlow 900 Italic skewed + name in project colour. Hover reveals delete (×) and members buttons over the + badge corners. A "New Project…" dashed-outline card sits first in the list and + opens a name dialog on click. Rows animate in staggered on load. + Back arrow in the editor updated to /. +

+
+ project-list-default + hueFromId (31-hash) + Barlow skew matrix + new-project dialog +
+
+ +
+ Groups removed from the UI +

+ The redesign spec removed groups entirely from the frontend and server routes. + The data layer (groups / groupMembers in db.ts) is + retained in case groups return later via rokojori-auth; only the router and all + UI references were deleted. +

+
+ +
+ Theme preference stored server-side, not in localStorage +

+ JWT settings from rokojori-auth are read-only. A local + user_settings.json DB file with a generic + Record<string, unknown> value map provides per-user persistence + for any app setting (starting with theme) without requiring changes + to the auth service. +

+
+
+

What's next

- See Tasks board for the current sprint. - Local filesystem access and remote projects in Electron are the two active items. + Italic Neon theme (batch 2): skewed cards, warm→cold gradient, hand-crafted SVG + pattern icons (13 files already drawn), daily auto-rotation + user toggle. + See Tasks board for the full sprint.

diff --git a/workspace/history/index.html b/workspace/history/index.html index a72e377..3d6fb68 100644 --- a/workspace/history/index.html +++ b/workspace/history/index.html @@ -21,7 +21,7 @@

Monday, 14 July 2026

-

Electron desktop app shell — login window, JWT auth via API, Authorization header injection, token persistence, ROJECT_ROOT path fix, ELECTRON_RUN_AS_NODE workaround. Workspace boards system (Tasks/Bugs/Backlog) with custom task-item elements; outline shortened; workspace index quick orientation added.

+

Electron desktop app shell, workspace boards system. Full project home redesign: new / entry point, project-home component (hero / theme dispatch), default theme (logo nav, UUID-coloured badges, Barlow skewed names, new-project dialog), user settings backend, groups removed from UI.

diff --git a/workspace/outline/index.html b/workspace/outline/index.html index 2c42113..b7d1d5a 100644 --- a/workspace/outline/index.html +++ b/workspace/outline/index.html @@ -52,12 +52,30 @@
-

Groups, projects & storage

+

Projects & storage

- Groups and projects with member management (viewer / editor / admin roles). + Projects with member management (viewer / editor / admin roles). Each project gets a real directory on disk at storage/<uuid>/root/ with a default index.html on creation. All data lives as JSON files - in data/ — no external database. + in build/data/db/ — no external database. Groups exist in the data + layer but have been removed from the UI; they may return later via rokojori-auth. +

+
+ +
+

Project home & theme system

+

+ The root / serves a new index.html entry point that + loads the project-home component. Logged-out users see a full-screen + dark hero; logged-in users get the active theme component dynamically imported. + The default theme (project-list-default) renders a dark + radial-gradient page with a Roject logo nav, per-project coloured badges (hue + from UUID via 31-hash, hsl(h, 95%, 45%)), Barlow 900 Italic + uppercase project names, hover-revealed delete / member buttons, and a dashed + "New Project" card that opens a name dialog on click. Theme preference is + persisted server-side in user_settings.json via + GET / PUT /api/user/settings. A second theme (Italic Neon) is + planned. /edit redirects to the existing /editor.html.

@@ -76,6 +94,20 @@

+
+

Project access control

+

+ source/server/projectAccess.ts is the single point of truth for + ownership and membership checks. GET /api/projects filters to + projects the requesting user owns or is a member of. Every file route + (tree, read, write, rename, delete) calls checkAccess() before + touching the filesystem. Delete and member-management routes verify ownership. + Members are currently stored by email address (Option B interim); + memberMatchesUser() is the one line to change when migrating to + user-ID-based lookup via rokojori-auth. +

+
+

AI chat & utilities

@@ -85,6 +117,9 @@ An EmailService facade (source/server/email/) provides a sendEmail() entry point backed by a swappable EmailSender interface; the default is SMTPEmailSender (Nodemailer). + EmailService.reportEmail is a static field holding the admin + notification address; Roject sends emails on server startup and on each + verified deploy request.