diff --git a/bootstrap/app.php b/bootstrap/app.php index 5e80c73..1e4d6cf 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -27,7 +27,6 @@ $app->post('/api/auth/register', [AuthController::class, 'register']); // Authenticated routes (any logged-in user) $app->group('', function (RouteCollectorProxy $group) { - $group->get('/', [ViewController::class, 'home']); $group->get('/home', [ViewController::class, 'home']); $group->get('/today', [ViewController::class, 'today']); $group->get('/texts', [ViewController::class, 'userTexts']); diff --git a/cypress/e2e/adminText.cy.js b/cypress/e2e/adminText.cy.js index 83415c2..4e0893c 100644 --- a/cypress/e2e/adminText.cy.js +++ b/cypress/e2e/adminText.cy.js @@ -32,8 +32,7 @@ describe('The admin text detail page', () => { }) it('clicking "Add child" reveals an inline form', () => { - cy.get('#text-detail li').first().activateNode() - .children('button.add-child').click() + cy.get('#text-detail li').first().children('button.add-child').click() cy.get('#text-detail li').first().children('input.child-title').should('be.visible') cy.get('#text-detail li').first().children('button.save-child').should('be.visible') }) @@ -42,8 +41,7 @@ describe('The admin text detail page', () => { cy.intercept('POST', '/api/nodes').as('createNode') cy.intercept('GET', '/api/nodes/0').as('getNodesRefresh') - cy.get('#text-detail > ul > li').first().activateNode() - .children('button.add-child').click() + cy.get('#text-detail > ul > li').first().children('button.add-child').click() cy.get('#text-detail > ul > li').first().children('input.child-title').type('New Child Node') cy.get('#text-detail > ul > li').first().children('button.save-child').click() @@ -57,8 +55,7 @@ describe('The admin text detail page', () => { cy.intercept('POST', '/api/nodes').as('createNode') cy.intercept('GET', '/api/nodes/0').as('getNodesRefresh') - cy.get('#text-detail > ul > li > ul > li').first().activateNode() - .children('button.add-child').click() + cy.get('#text-detail > ul > li > ul > li').first().children('button.add-child').click() cy.get('#text-detail > ul > li > ul > li').first().children('input.child-title').type('Nested Child Node') cy.get('#text-detail > ul > li > ul > li').first().children('button.save-child').click() @@ -72,9 +69,6 @@ describe('The admin text detail page', () => { cy.intercept('POST', '/api/nodes').as('createNode') cy.intercept('GET', '/api/nodes/0').as('getNodesRefresh') - cy.get('#text-detail > ul > li > ul > li') - .first() - .activateNode() cy.get('#text-detail > ul > li > ul > li') .first() .find('button.toggle-children') @@ -113,7 +107,6 @@ describe('The admin text detail page', () => { cy.get('#text-detail > ul > li') .first() - .activateNode() .children('button.add-child') .click() cy.get('#text-detail > ul > li') @@ -130,7 +123,6 @@ describe('The admin text detail page', () => { it('opening add-child on another node closes the first one', () => { cy.get('#text-detail > ul > li') .first() - .activateNode() .children('button.add-child') .click() cy.get('#text-detail > ul > li') @@ -140,7 +132,6 @@ describe('The admin text detail page', () => { cy.get('#text-detail > ul > li > ul > li') .first() - .activateNode() .children('button.add-child') .click() @@ -161,7 +152,6 @@ describe('The admin text detail page', () => { it('opening bulk-add closes an open add-child form', () => { cy.get('#text-detail > ul > li') .first() - .activateNode() .children('button.add-child') .click() cy.get('#text-detail > ul > li') @@ -171,7 +161,6 @@ describe('The admin text detail page', () => { cy.get('#text-detail > ul > li > ul > li') .first() - .activateNode() .children('button.bulk-add-children') .click() @@ -189,8 +178,7 @@ describe('The admin text detail page', () => { cy.intercept('POST', '/api/nodes').as('createNode') cy.intercept('GET', '/api/nodes/0').as('getNodesRefresh') - cy.get('#text-detail > ul > li').first().activateNode() - .children('button.add-child').click() + cy.get('#text-detail > ul > li').first().children('button.add-child').click() cy.get('#text-detail > ul > li').first().children('input.child-title').type('Persistent Child') cy.get('#text-detail > ul > li').first().children('button.save-child').click() diff --git a/cypress/e2e/adminTextBulkAdd.cy.js b/cypress/e2e/adminTextBulkAdd.cy.js index 39b06d3..47d3c7f 100644 --- a/cypress/e2e/adminTextBulkAdd.cy.js +++ b/cypress/e2e/adminTextBulkAdd.cy.js @@ -20,16 +20,14 @@ describe('Bulk add children on the admin text detail page', () => { }) it('clicking "Bulk add children" reveals inline form inputs', () => { - cy.get('#text-detail > ul > li').first().activateNode() - .children('button.bulk-add-children').click() + cy.get('#text-detail > ul > li').first().children('button.bulk-add-children').click() cy.get('#text-detail > ul > li').first().children('input.bulk-title').should('be.visible') cy.get('#text-detail > ul > li').first().children('input.bulk-count').should('be.visible') cy.get('#text-detail > ul > li').first().children('button.save-bulk').should('be.visible') }) it('clicking "Bulk add children" again hides the form', () => { - cy.get('#text-detail > ul > li').first().activateNode() - .children('button.bulk-add-children').click() + cy.get('#text-detail > ul > li').first().children('button.bulk-add-children').click() cy.get('#text-detail > ul > li').first().children('input.bulk-title').should('be.visible') cy.get('#text-detail > ul > li').first().children('button.bulk-add-children').click() cy.get('#text-detail > ul > li').first().children('input.bulk-title').should('not.exist') @@ -40,8 +38,7 @@ describe('Bulk add children on the admin text detail page', () => { it('can bulk add children to the root node', () => { cy.intercept('POST', '/api/nodes/bulk').as('bulkCreate') cy.intercept('GET', '/api/nodes/0').as('getNodesRefresh') - cy.get('#text-detail > ul > li').first().activateNode() - .children('button.bulk-add-children').click() + cy.get('#text-detail > ul > li').first().children('button.bulk-add-children').click() cy.get('#text-detail > ul > li').first().children('input.bulk-title').type('Page') cy.get('#text-detail > ul > li').first().children('input.bulk-count').type('3') cy.get('#text-detail > ul > li').first().children('button.save-bulk').click() @@ -54,8 +51,7 @@ describe('Bulk add children on the admin text detail page', () => { it('does not submit if title prefix is empty', () => { cy.intercept('POST', '/api/nodes/bulk').as('bulkCreate') - cy.get('#text-detail > ul > li').first().activateNode() - .children('button.bulk-add-children').click() + cy.get('#text-detail > ul > li').first().children('button.bulk-add-children').click() cy.get('#text-detail > ul > li').first().children('input.bulk-count').type('3') cy.get('#text-detail > ul > li').first().children('button.save-bulk').click() cy.get('@bulkCreate.all').should('have.length', 0) @@ -63,8 +59,7 @@ describe('Bulk add children on the admin text detail page', () => { it('does not submit if count is empty', () => { cy.intercept('POST', '/api/nodes/bulk').as('bulkCreate') - cy.get('#text-detail > ul > li').first().activateNode() - .children('button.bulk-add-children').click() + cy.get('#text-detail > ul > li').first().children('button.bulk-add-children').click() cy.get('#text-detail > ul > li').first().children('input.bulk-title').type('Page') cy.get('#text-detail > ul > li').first().children('button.save-bulk').click() cy.get('@bulkCreate.all').should('have.length', 0) @@ -73,8 +68,7 @@ describe('Bulk add children on the admin text detail page', () => { it('pressing Enter in the bulk-count input submits', () => { cy.intercept('POST', '/api/nodes/bulk').as('bulkCreate') cy.intercept('GET', '/api/nodes/0').as('getNodesRefresh') - cy.get('#text-detail > ul > li').first().activateNode() - .children('button.bulk-add-children').click() + cy.get('#text-detail > ul > li').first().children('button.bulk-add-children').click() cy.get('#text-detail > ul > li').first().children('input.bulk-title').type('Enter') cy.get('#text-detail > ul > li').first().children('input.bulk-count').type('2{enter}') cy.wait('@bulkCreate').its('response.statusCode').should('eq', 201) @@ -86,8 +80,7 @@ describe('Bulk add children on the admin text detail page', () => { it('pressing Enter in the bulk-title input submits', () => { cy.intercept('POST', '/api/nodes/bulk').as('bulkCreate') cy.intercept('GET', '/api/nodes/0').as('getNodesRefresh') - cy.get('#text-detail > ul > li').first().activateNode() - .children('button.bulk-add-children').click() + cy.get('#text-detail > ul > li').first().children('button.bulk-add-children').click() cy.get('#text-detail > ul > li').first().children('input.bulk-count').type('2') cy.get('#text-detail > ul > li').first().children('input.bulk-title').type('Title{enter}') cy.wait('@bulkCreate').its('response.statusCode').should('eq', 201) @@ -99,8 +92,7 @@ describe('Bulk add children on the admin text detail page', () => { it('bulk added nodes persist after page reload', () => { cy.intercept('POST', '/api/nodes/bulk').as('bulkCreate') cy.intercept('GET', '/api/nodes/0').as('getNodesRefresh') - cy.get('#text-detail > ul > li').first().activateNode() - .children('button.bulk-add-children').click() + cy.get('#text-detail > ul > li').first().children('button.bulk-add-children').click() cy.get('#text-detail > ul > li').first().children('input.bulk-title').type('Page') cy.get('#text-detail > ul > li').first().children('input.bulk-count').type('3') cy.get('#text-detail > ul > li').first().children('button.save-bulk').click() diff --git a/cypress/e2e/adminTextLayout.cy.js b/cypress/e2e/adminTextLayout.cy.js deleted file mode 100644 index 6d741a1..0000000 --- a/cypress/e2e/adminTextLayout.cy.js +++ /dev/null @@ -1,61 +0,0 @@ -describe('The admin text detail page horizontal layout', () => { - beforeEach(() => { - cy.exec('npm run db:seed') - cy.loginAsAdmin() - cy.intercept('GET', '/api/texts/0').as('getText') - cy.intercept('GET', '/api/nodes/0').as('getNodes') - cy.visit('/admin/texts/0') - cy.wait('@getText') - cy.wait('@getNodes') - }) - - afterEach(() => { - cy.exec('npm run db:wipe') - }) - - it('renders child list to the right of the parent title', () => { - cy.get('#text-detail > ul > li').first().as('rootLi') - cy.get('@rootLi').children('span').first().as('rootTitle') - cy.get('@rootLi').children('ul').first().as('childList') - - cy.get('@childList').should('be.visible') - - cy.get('@rootTitle').then(($title) => { - const titleRect = $title[0].getBoundingClientRect() - cy.get('@childList').then(($list) => { - const listRect = $list[0].getBoundingClientRect() - expect(listRect.left).to.be.greaterThan(titleRect.right) - }) - }) - }) - - it('stacks sibling child nodes vertically within the right column', () => { - cy.intercept('POST', '/api/nodes').as('createNode') - cy.intercept('GET', '/api/nodes/0').as('getNodesRefresh') - - cy.get('#text-detail > ul > li') - .first() - .activateNode() - .children('button.add-child') - .click() - cy.get('#text-detail > ul > li') - .first() - .children('input.child-title') - .type('Sibling One{enter}') - cy.wait('@createNode') - cy.wait('@getNodesRefresh') - - cy.get('#text-detail > ul > li > ul > li').should( - 'have.length.greaterThan', - 1 - ) - - cy.get('#text-detail > ul > li > ul > li').then(($siblings) => { - const firstRect = $siblings[0].getBoundingClientRect() - const lastRect = $siblings[$siblings.length - 1] - .getBoundingClientRect() - expect(lastRect.top).to.be.greaterThan(firstRect.top) - expect(Math.abs(lastRect.left - firstRect.left)).to.be.lessThan(2) - }) - }) -}) diff --git a/cypress/e2e/userText.cy.js b/cypress/e2e/userText.cy.js index 7c3e379..2318f07 100644 --- a/cypress/e2e/userText.cy.js +++ b/cypress/e2e/userText.cy.js @@ -30,7 +30,7 @@ describe('The user text detail page', () => { cy.visit('/texts/0') cy.wait('@getNodes') - cy.get('#text-detail > ul > li').first().activateNode() + cy.get('#text-detail > ul > li').first() .children('button.add-child').click() cy.get('#text-detail > ul > li').first() .children('input.child-title').type('My new child') diff --git a/cypress/support/commands.js b/cypress/support/commands.js index af58025..1c1d028 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -17,8 +17,3 @@ Cypress.Commands.add('loginAsUser', () => { Cypress.Commands.add('loginAsSecondUser', () => { cy.login('user2@example.com', 'password2') }) - -Cypress.Commands.add('activateNode', { prevSubject: 'element' }, ($li) => { - cy.wrap($li).children('span').first().click() - return cy.wrap($li) -}) diff --git a/public/css/app.css b/public/css/app.css index 9e3c2cf..767dddd 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -444,12 +444,6 @@ form { elements added. */ -.node-tree ul { - display: flex; - flex-direction: column; - gap: var(--space-2); -} - .node-tree > ul { border-left: 2px solid var(--color-border); padding-left: var(--space-4); @@ -457,41 +451,26 @@ form { .node-tree ul ul { border-left: 1px solid var(--color-border); - margin-left: var(--space-3); + margin-top: var(--space-2); + margin-left: var(--space-2); padding-left: var(--space-4); } .node-tree li { display: flex; - flex-wrap: nowrap; - align-items: flex-start; + flex-wrap: wrap; + align-items: center; gap: var(--space-2); - padding: 0; + padding: var(--space-2) 0; } .node-tree li > ul { - flex-shrink: 0; + flex-basis: 100%; + margin-top: var(--space-2); } .node-tree li > span { - flex-shrink: 0; font-weight: 500; - cursor: pointer; -} - -.node-tree li > span:hover, -.node-tree li.is-active > span { - color: var(--color-primary); -} - -.node-tree li > button.add-child, -.node-tree li > button.bulk-add-children { - display: none; -} - -.node-tree li.is-active > button.add-child, -.node-tree li.is-active > button.bulk-add-children { - display: inline-block; } .node-tree li > button { diff --git a/public/js/text.js b/public/js/text.js index aca2210..7566869 100644 --- a/public/js/text.js +++ b/public/js/text.js @@ -1,5 +1,4 @@ -const expandedChildByParent = new Map(); -let activeNodeId = null; +const expandedNodeIds = new Set(); document.addEventListener('DOMContentLoaded', () => { const textId = window.location.pathname.split('/').pop(); @@ -76,36 +75,12 @@ function renderTree(nodes, textId, depth = 0) { const titleSpan = document.createElement('span'); titleSpan.textContent = node.title; - titleSpan.addEventListener('click', () => { - if (activeNodeId === node.id) { - activeNodeId = null; - li.classList.remove('is-active'); - closeAllAddForms(); - return; - } - const previousActive = document.querySelector( - '#text-detail li.is-active' - ); - if (previousActive) { - previousActive.classList.remove('is-active'); - } - closeAllAddForms(); - li.classList.add('is-active'); - activeNodeId = node.id; - }); li.appendChild(titleSpan); - if (node.id === activeNodeId) { - li.classList.add('is-active'); - } - const addBtn = document.createElement('button'); addBtn.textContent = 'Add child'; addBtn.className = 'add-child'; - addBtn.addEventListener( - 'click', - () => toggleAddForm(li, node.id, node.parentNodeId, textId) - ); + addBtn.addEventListener('click', () => toggleAddForm(li, node.id, textId)); li.appendChild(addBtn); const bulkBtn = document.createElement('button'); @@ -113,32 +88,31 @@ function renderTree(nodes, textId, depth = 0) { bulkBtn.className = 'bulk-add-children'; bulkBtn.addEventListener( 'click', - () => toggleBulkAddForm(li, node.id, node.parentNodeId, textId) + () => toggleBulkAddForm(li, node.id, textId) ); li.appendChild(bulkBtn); if (node.children.length > 0) { const childUl = renderTree(node.children, textId, depth + 1); const childrenVisible = - depth === 0 || - expandedChildByParent.get(node.parentNodeId) === node.id; + expandedNodeIds.has(node.id) || depth === 0; + if (childrenVisible) { + expandedNodeIds.add(node.id); + } childUl.style.display = childrenVisible ? '' : 'none'; const toggleBtn = document.createElement('button'); toggleBtn.className = 'toggle-children'; toggleBtn.textContent = childrenVisible ? '▼' : '▶'; toggleBtn.addEventListener('click', () => { - if (depth === 0) { - return; - } - const expandedSibling = - expandedChildByParent.get(node.parentNodeId); - if (expandedSibling === node.id) { - expandedChildByParent.delete(node.parentNodeId); + const isHidden = childUl.style.display === 'none'; + childUl.style.display = isHidden ? '' : 'none'; + toggleBtn.textContent = isHidden ? '▼' : '▶'; + if (isHidden) { + expandedNodeIds.add(node.id); } else { - expandedChildByParent.set(node.parentNodeId, node.id); + expandedNodeIds.delete(node.id); } - fetchAndRenderNodes(textId); }); li.appendChild(toggleBtn); @@ -165,7 +139,7 @@ function closeAllAddForms() { }); } -function toggleAddForm(li, parentNodeId, grandparentId, textId) { +function toggleAddForm(li, parentNodeId, textId) { const existing = li.querySelector('input.child-title'); if (existing) { existing.remove(); @@ -188,7 +162,7 @@ function toggleAddForm(li, parentNodeId, grandparentId, textId) { const title = input.value.trim(); if (!title) return; - expandedChildByParent.set(grandparentId, parentNodeId); + expandedNodeIds.add(parentNodeId); fetch('/api/nodes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -215,7 +189,7 @@ function toggleAddForm(li, parentNodeId, grandparentId, textId) { input.focus(); } -function toggleBulkAddForm(li, parentNodeId, grandparentId, textId) { +function toggleBulkAddForm(li, parentNodeId, textId) { const existing = li.querySelector('input.bulk-title'); if (existing) { existing.remove(); @@ -246,7 +220,7 @@ function toggleBulkAddForm(li, parentNodeId, grandparentId, textId) { const count = parseInt(countInput.value); if (!titlePrefix || !count || count < 1) return; - expandedChildByParent.set(grandparentId, parentNodeId); + expandedNodeIds.add(parentNodeId); fetch('/api/nodes/bulk', { method: 'POST', headers: { 'Content-Type': 'application/json' },