diff --git a/backend/app/Http/Controllers/ScheduleController.php b/backend/app/Http/Controllers/ScheduleController.php index 33a6316..f80461a 100644 --- a/backend/app/Http/Controllers/ScheduleController.php +++ b/backend/app/Http/Controllers/ScheduleController.php @@ -249,8 +249,6 @@ class ScheduleController extends Controller return [ 'id' => $assignment->getId(), - 'scheduledDate' => $assignment->getScheduledDate() - ->format('Y-m-d'), 'schedule' => [ 'id' => $assignmentForDate->getScheduleId(), 'set' => [ diff --git a/backend/app/Schedule/EloquentScheduleRepository.php b/backend/app/Schedule/EloquentScheduleRepository.php index 8576591..214e8a5 100644 --- a/backend/app/Schedule/EloquentScheduleRepository.php +++ b/backend/app/Schedule/EloquentScheduleRepository.php @@ -63,7 +63,7 @@ class EloquentScheduleRepository implements ScheduleRepository return $schedules; } - public function findIncompleteAssignmentsForUserDueOnOrBefore( + public function findAssignmentsForUserOnDate( User $user, DateTimeImmutable $date, ): array { @@ -76,14 +76,11 @@ class EloquentScheduleRepository implements ScheduleRepository 'schedule_assignments.schedule_id', ) ->where('schedules.user_id', $user->getId()) - ->where( - 'schedule_assignments.scheduled_date', - '<=', - $date->format('Y-m-d'), - ) + ->where('schedule_assignments.scheduled_date', $date->format( + 'Y-m-d', + )) ->whereNull('schedule_assignments.completed_at') ->with('schedule') - ->orderBy('schedule_assignments.scheduled_date') ->orderByDesc('schedules.id') ->orderBy('schedule_assignments.position') ->orderBy('schedule_assignments.id') diff --git a/backend/app/Schedule/ScheduleRepository.php b/backend/app/Schedule/ScheduleRepository.php index 20b5f9f..3222311 100644 --- a/backend/app/Schedule/ScheduleRepository.php +++ b/backend/app/Schedule/ScheduleRepository.php @@ -19,7 +19,7 @@ interface ScheduleRepository /** * @return list */ - public function findIncompleteAssignmentsForUserDueOnOrBefore( + public function findAssignmentsForUserOnDate( User $user, DateTimeImmutable $date, ): array; diff --git a/backend/app/Schedule/UseCases/ListAssignmentsForDate/ListAssignmentsForDate.php b/backend/app/Schedule/UseCases/ListAssignmentsForDate/ListAssignmentsForDate.php index 23f388d..9e3017d 100644 --- a/backend/app/Schedule/UseCases/ListAssignmentsForDate/ListAssignmentsForDate.php +++ b/backend/app/Schedule/UseCases/ListAssignmentsForDate/ListAssignmentsForDate.php @@ -22,11 +22,10 @@ class ListAssignmentsForDate { $date = $this->parseDate($request->date); - return $this->scheduleRepository - ->findIncompleteAssignmentsForUserDueOnOrBefore( - $request->user, - $date, - ); + return $this->scheduleRepository->findAssignmentsForUserOnDate( + $request->user, + $date, + ); } /** diff --git a/backend/tests/Fakes/FakeScheduleRepository.php b/backend/tests/Fakes/FakeScheduleRepository.php index e12d126..e2f35bc 100644 --- a/backend/tests/Fakes/FakeScheduleRepository.php +++ b/backend/tests/Fakes/FakeScheduleRepository.php @@ -78,7 +78,7 @@ class FakeScheduleRepository implements ScheduleRepository }, array_values($schedules)); } - public function findIncompleteAssignmentsForUserDueOnOrBefore( + public function findAssignmentsForUserOnDate( User $user, DateTimeImmutable $date, ): array { @@ -86,7 +86,8 @@ class FakeScheduleRepository implements ScheduleRepository foreach ($this->findAllForUser($user) as $schedule) { foreach ($schedule->getAssignments() as $assignment) { - if ($assignment->getScheduledDate() > $date + if ($assignment->getScheduledDate()->format('Y-m-d') + !== $date->format('Y-m-d') || $assignment->getCompletedAt() !== null ) { continue; @@ -100,37 +101,6 @@ class FakeScheduleRepository implements ScheduleRepository } } - usort( - $assignments, - function ( - AssignmentForDate $first, - AssignmentForDate $second, - ): int { - $firstAssignment = $first->getAssignment(); - $secondAssignment = $second->getAssignment(); - $dateComparison = $firstAssignment->getScheduledDate() - <=> $secondAssignment->getScheduledDate(); - if ($dateComparison !== 0) { - return $dateComparison; - } - - $scheduleComparison = $second->getScheduleId() - <=> $first->getScheduleId(); - if ($scheduleComparison !== 0) { - return $scheduleComparison; - } - - $positionComparison = $firstAssignment->getPosition() - <=> $secondAssignment->getPosition(); - if ($positionComparison !== 0) { - return $positionComparison; - } - - return $firstAssignment->getId() - <=> $secondAssignment->getId(); - }, - ); - return $assignments; } diff --git a/backend/tests/Feature/Schedule/ScheduleEndpointTest.php b/backend/tests/Feature/Schedule/ScheduleEndpointTest.php index 295a245..b68e2ad 100644 --- a/backend/tests/Feature/Schedule/ScheduleEndpointTest.php +++ b/backend/tests/Feature/Schedule/ScheduleEndpointTest.php @@ -225,7 +225,7 @@ class ScheduleEndpointTest extends TestCase ]); } - public function test_it_lists_the_users_assignments_due_through_a_date(): void + public function test_it_lists_the_users_assignments_for_a_date(): void { $user = $this->createUser('reader@example.com'); $otherUser = $this->createUser('other@example.com'); @@ -255,8 +255,8 @@ class ScheduleEndpointTest extends TestCase $this->credentialedPost('/api/schedules', [ 'setId' => $olderSet->getId(), 'levelId' => $olderLevel->getId(), - 'startDate' => '2026-08-14', - 'targetDate' => '2026-08-14', + 'startDate' => '2026-08-15', + 'targetDate' => '2026-08-15', ])->assertCreated(); $this->credentialedPost('/api/schedules', [ 'setId' => $newerSet->getId(), @@ -286,9 +286,22 @@ class ScheduleEndpointTest extends TestCase ->assertExactJson([ 'date' => '2026-08-15', 'assignments' => [ + [ + 'id' => 3, + 'schedule' => [ + 'id' => 2, + 'set' => [ + 'name' => 'Newer course', + ], + ], + 'element' => [ + 'name' => 'Only chapter', + 'kind' => 'chapter', + 'path' => ['Only chapter'], + ], + ], [ 'id' => 1, - 'scheduledDate' => '2026-08-14', 'schedule' => [ 'id' => 1, 'set' => [ @@ -303,7 +316,6 @@ class ScheduleEndpointTest extends TestCase ], [ 'id' => 2, - 'scheduledDate' => '2026-08-14', 'schedule' => [ 'id' => 1, 'set' => [ @@ -316,21 +328,6 @@ class ScheduleEndpointTest extends TestCase 'path' => ['Second lesson'], ], ], - [ - 'id' => 3, - 'scheduledDate' => '2026-08-15', - 'schedule' => [ - 'id' => 2, - 'set' => [ - 'name' => 'Newer course', - ], - ], - 'element' => [ - 'name' => 'Only chapter', - 'kind' => 'chapter', - 'path' => ['Only chapter'], - ], - ], ], ]); } diff --git a/backend/tests/Unit/Schedule/UseCases/ListAssignmentsForDateTest.php b/backend/tests/Unit/Schedule/UseCases/ListAssignmentsForDateTest.php index 4fca1fc..c6bc95f 100644 --- a/backend/tests/Unit/Schedule/UseCases/ListAssignmentsForDateTest.php +++ b/backend/tests/Unit/Schedule/UseCases/ListAssignmentsForDateTest.php @@ -17,7 +17,7 @@ use Tests\Fakes\FakeScheduleRepository; class ListAssignmentsForDateTest extends TestCase { - public function test_it_lists_the_users_assignments_due_through_the_date(): void + public function test_it_lists_the_users_assignments_for_the_date(): void { $user = $this->user(1, 'reader@example.com'); $otherUser = $this->user(2, 'other@example.com'); @@ -25,7 +25,7 @@ class ListAssignmentsForDateTest extends TestCase $olderSchedule = $repository->create($this->schedule( user: $user, setName: 'Older plan', - date: '2026-08-14', + date: '2026-08-15', assignmentNames: ['First', 'Second'], )); $repository->create($this->schedule( @@ -37,7 +37,7 @@ class ListAssignmentsForDateTest extends TestCase $repository->create($this->schedule( user: $otherUser, setName: 'Private plan', - date: '2026-08-13', + date: '2026-08-15', assignmentNames: ['Hidden'], )); $repository->create($this->schedule( @@ -61,18 +61,18 @@ class ListAssignmentsForDateTest extends TestCase ); $this->assertSame( - ['Older plan', 'Newer plan'], + ['Newer plan', 'Older plan'], array_map(function ($assignment): string { return $assignment->getSetName(); }, $assignments), ); $this->assertSame( - ['Second', 'Third'], + ['Third', 'Second'], array_map(function ($assignment): string { return $assignment->getAssignment()->getName(); }, $assignments), ); - $this->assertSame(1, $assignments[0]->getScheduleId()); + $this->assertSame(2, $assignments[0]->getScheduleId()); } public function test_it_rejects_a_missing_date(): void diff --git a/frontend/website/cypress/e2e/set-scheduling.cy.ts b/frontend/website/cypress/e2e/set-scheduling.cy.ts index 0511405..9f83629 100644 --- a/frontend/website/cypress/e2e/set-scheduling.cy.ts +++ b/frontend/website/cypress/e2e/set-scheduling.cy.ts @@ -121,55 +121,6 @@ const completedScheduleDetail = { }, } -const splitScheduleDetail = { - schedule: { - ...scheduleDetail.schedule, - targetDate: '2026-08-13', - assignmentCount: 4, - days: [ - scheduleDetail.schedule.days[0], - scheduleDetail.schedule.days[1], - { - date: '2026-08-12', - assignments: [ - { - id: 2, - completedAt: '2026-08-11T08:30:00+00:00', - element: { - name: 'Chapter 1', - kind: 'Chapter_sections-v2', - path: ['Exodus', 'Shemot', 'Chapter 1'], - }, - }, - { - id: 3, - completedAt: null, - element: { - name: 'Chapter 1', - kind: 'Chapter_sections-v2', - path: ['Leviticus', 'Vayikra', 'Chapter 1'], - }, - }, - ], - }, - { - date: '2026-08-13', - assignments: [ - { - id: 4, - completedAt: '2026-08-11T09:45:00+00:00', - element: { - name: 'Chapter 1', - kind: 'Chapter_sections-v2', - path: ['Numbers', 'Bamidbar', 'Chapter 1'], - }, - }, - ], - }, - ], - }, -} - function interceptAuthenticatedUser(): void { cy.intercept('GET', '**/api/me', { statusCode: 200, @@ -208,12 +159,9 @@ describe('set scheduling', () => { cy.location('pathname').should('equal', '/sets/41/schedules/new') cy.get('h1').should('have.text', 'Schedule Bible') cy.get('#schedule-level option').then(($options) => { - expect([...$options].map((option) => option.textContent?.trim())).to.deep.equal([ - 'Choose a level', - 'book (2)', - 'portion (2)', - 'Chapter_sections-v2 (2)', - ]) + expect([...$options].map((option) => option.textContent?.trim())).to.deep.equal( + ['Choose a level', 'book (2)', 'portion (2)', 'Chapter_sections-v2 (2)'], + ) }) cy.get('#schedule-level').select('3') cy.get('#schedule-start-date').type('2026-08-10') @@ -236,12 +184,6 @@ describe('set scheduling', () => { 'contain.text', 'Exodus / Shemot / Chapter 1', ) - cy.get('[data-assignment-section="completed"]').should('not.have.attr', 'open') - cy.get('[data-assignment-section="completed"] summary').click() - cy.get('[data-assignment-section="completed"]').should( - 'contain.text', - 'No assignments completed yet.', - ) }) it('validates the schedule form before submitting', () => { @@ -306,72 +248,6 @@ describe('set scheduling', () => { cy.get('h1').should('have.text', 'Schedule not found') }) - it('splits assignments by progress while preserving the schedule timeline', () => { - cy.clock(new Date(2026, 7, 11, 12).getTime(), ['Date']) - cy.intercept('GET', '**/api/schedules/73', { - statusCode: 200, - body: splitScheduleDetail, - }).as('schedule') - - cy.visit('/schedules/73') - cy.wait('@me') - cy.wait('@schedule') - - cy.get('[data-assignment-section="remaining"]') - .should('have.attr', 'data-assignment-count', '2') - .within(() => { - cy.get('h2').should('have.text', 'Remaining') - cy.get('[data-schedule-day]').then(($days) => { - expect([...$days].map((day) => day.getAttribute('data-schedule-date'))).to.deep.equal([ - '2026-08-10', - '2026-08-11', - '2026-08-12', - ]) - }) - cy.get('[data-schedule-date="2026-08-10"]') - .should('contain.text', 'Overdue') - .and('contain.text', 'Genesis / Creation / Chapter 1') - cy.get('[data-schedule-date="2026-08-11"]') - .should('contain.text', 'Rest day') - .and('not.contain.text', 'Overdue') - cy.get('[data-schedule-date="2026-08-12"]') - .should('contain.text', 'Leviticus / Vayikra / Chapter 1') - .and('not.contain.text', 'Exodus / Shemot / Chapter 1') - cy.get('[data-schedule-date="2026-08-13"]').should('not.exist') - }) - - cy.get('[data-assignment-section="completed"]').should( - 'have.attr', - 'data-assignment-count', - '2', - ) - cy.get('[data-assignment-section="completed"]').should('not.have.attr', 'open') - cy.get('[data-assignment-section="completed"]').within(() => { - cy.get('summary').should('contain.text', 'Completed').click() - cy.get('[data-schedule-day]').then(($days) => { - expect([...$days].map((day) => day.getAttribute('data-schedule-date'))).to.deep.equal([ - '2026-08-12', - '2026-08-13', - ]) - }) - cy.get('[data-schedule-date="2026-08-12"]') - .should('contain.text', 'Exodus / Shemot / Chapter 1') - .and('not.contain.text', 'Leviticus / Vayikra / Chapter 1') - cy.get('[data-schedule-date="2026-08-13"]').should( - 'contain.text', - 'Numbers / Bamidbar / Chapter 1', - ) - cy.contains('Rest day').should('not.exist') - }) - - cy.viewport(375, 667) - cy.document().then((document) => { - expect(document.documentElement.scrollWidth).to.be.at.most( - document.documentElement.clientWidth, - ) - }) - }) - it('completes and reopens assignments from the schedule', () => { const completedAt = '2026-08-15T12:30:00+00:00' cy.intercept('GET', '**/api/schedules/73', { @@ -402,79 +278,33 @@ describe('set scheduling', () => { cy.wait('@me') cy.wait('@schedule') - cy.get('[data-assignment-section="completed"] summary').click() - cy.get('[data-assignment-section="completed"] [data-schedule-assignment="2"]').within(() => { + cy.get('[data-schedule-assignment="2"]').within(() => { cy.contains('Completed').should('be.visible') cy.get('time') .should('have.attr', 'datetime', '2026-08-15T09:30:00+00:00') .and('have.text', formatCompletionTime('2026-08-15T09:30:00+00:00')) - }) - - cy.get('[data-assignment-section="remaining"] [data-schedule-assignment="1"]').within(() => { - cy.contains('button', 'Mark complete').click() - cy.contains('button', 'Saving...').should('be.disabled') - }) - cy.wait('@completeAssignment') - cy.get('[data-assignment-section="remaining"]') - .should('have.attr', 'data-assignment-count', '0') - .and('contain.text', 'All assignments are complete.') - .and('contain.text', 'Rest day') - cy.get('[data-assignment-section="completed"]') - .should('have.attr', 'data-assignment-count', '2') - .within(() => { - cy.get('[data-schedule-assignment="1"]').within(() => { - cy.contains('Completed').should('be.visible') - cy.get('time') - .should('have.attr', 'datetime', completedAt) - .and('have.text', formatCompletionTime(completedAt)) - cy.contains('button', 'Reopen').should('be.enabled') - }) - }) - cy.get('[data-completion-announcement]').should('have.text', 'Assignment moved to Completed.') - - cy.get('[data-assignment-section="completed"] [data-schedule-assignment="2"]').within(() => { cy.contains('button', 'Reopen').click() }) cy.wait('@reopenAssignment') - cy.get('[data-assignment-section="remaining"]') - .should('have.attr', 'data-assignment-count', '1') - .find('[data-schedule-assignment="2"]') + cy.get('[data-schedule-assignment="2"]') .should('contain.text', 'Not completed') .and('not.contain.text', 'Completed') .within(() => { cy.contains('button', 'Mark complete').should('be.enabled') }) - cy.get('[data-assignment-section="completed"]') - .should('have.attr', 'data-assignment-count', '1') - .find('[data-schedule-assignment="2"]') - .should('not.exist') - cy.get('[data-completion-announcement]').should('have.text', 'Assignment moved to Remaining.') - }) - it('keeps an assignment in place when a completion request fails', () => { - cy.intercept('GET', '**/api/schedules/73', { - statusCode: 200, - body: scheduleDetail, - }).as('schedule') - cy.intercept('PATCH', '**/api/assignments/1', { - statusCode: 500, - }).as('completeAssignment') - - cy.visit('/schedules/73') - cy.wait('@me') - cy.wait('@schedule') - - cy.get('[data-assignment-section="remaining"] [data-schedule-assignment="1"]') - .contains('button', 'Mark complete') - .click() + cy.get('[data-schedule-assignment="1"]').within(() => { + cy.contains('button', 'Mark complete').click() + cy.contains('button', 'Saving...').should('be.disabled') + }) cy.wait('@completeAssignment') - - cy.get('[data-assignment-section="remaining"] [data-schedule-assignment="1"]') - .should('be.visible') - .and('contain.text', "We couldn't update this assignment.") - cy.get('[data-assignment-section="completed"]') - .find('[data-schedule-assignment="1"]') - .should('not.exist') + cy.get('[data-schedule-assignment="1"]').within(() => { + cy.contains('Completed').should('be.visible') + cy.get('time') + .should('have.attr', 'datetime', completedAt) + .and('have.text', formatCompletionTime(completedAt)) + cy.contains('button', 'Reopen').should('be.enabled') + }) }) it('lists the users schedules on the dashboard', () => { diff --git a/frontend/website/cypress/e2e/today-assignments.cy.ts b/frontend/website/cypress/e2e/today-assignments.cy.ts index 553c57c..77d2ba5 100644 --- a/frontend/website/cypress/e2e/today-assignments.cy.ts +++ b/frontend/website/cypress/e2e/today-assignments.cy.ts @@ -36,7 +36,6 @@ describe("today's assignments", () => { assignments: [ { id: 12, - scheduledDate: '2026-08-13', schedule: { id: 73, set: { name: 'Bible' }, @@ -49,7 +48,6 @@ describe("today's assignments", () => { }, { id: 13, - scheduledDate: browserToday, schedule: { id: 81, set: { name: 'Course' }, @@ -79,16 +77,6 @@ describe("today's assignments", () => { 'have.length', 2, ) - cy.get('[data-today-assignment]') - .first() - .should('have.attr', 'data-today-assignment', '12') - cy.get('[data-today-assignment="12"] .today-assignment__overdue').should( - 'have.text', - 'Overdue · Due Aug 13, 2026', - ) - cy.get('[data-today-assignment="13"] .today-assignment__overdue').should( - 'not.exist', - ) cy.contains('a', 'Genesis / Creation / Chapter 1') .should('contain.text', 'Bible') .and('have.attr', 'href', '/schedules/73') @@ -121,7 +109,7 @@ describe("today's assignments", () => { cy.get('.today-assignments [role="status"]').should( 'contain.text', - 'Nothing is due today.', + 'Nothing is assigned for today.', ) }) @@ -133,7 +121,6 @@ describe("today's assignments", () => { assignments: [ { id: 12, - scheduledDate: browserToday, schedule: { id: 73, set: { name: 'Bible' }, @@ -190,7 +177,7 @@ describe("today's assignments", () => { cy.get('[data-today-assignment="12"]').should('not.exist') cy.get('.today-assignments [role="status"]').should( 'contain.text', - 'Nothing is due today.', + 'Nothing is assigned for today.', ) }) @@ -232,7 +219,7 @@ describe("today's assignments", () => { cy.get('.today-assignments [role="status"]').should( 'contain.text', - 'Nothing is due today.', + 'Nothing is assigned for today.', ) }) }) diff --git a/frontend/website/src/components/ScheduleAssignmentTimeline.vue b/frontend/website/src/components/ScheduleAssignmentTimeline.vue deleted file mode 100644 index 779d712..0000000 --- a/frontend/website/src/components/ScheduleAssignmentTimeline.vue +++ /dev/null @@ -1,335 +0,0 @@ - - - - - diff --git a/frontend/website/src/stores/schedules.ts b/frontend/website/src/stores/schedules.ts index 825bbfb..2471d66 100644 --- a/frontend/website/src/stores/schedules.ts +++ b/frontend/website/src/stores/schedules.ts @@ -31,7 +31,6 @@ const scheduleAssignmentSchema = assignmentIdentitySchema.extend({ }) export const assignmentForDateSchema = assignmentIdentitySchema.extend({ - scheduledDate: isoDateSchema, schedule: z.object({ id: z.number().int().positive(), set: z.object({ diff --git a/frontend/website/src/views/DashboardView.vue b/frontend/website/src/views/DashboardView.vue index 7d1eeaf..822da86 100644 --- a/frontend/website/src/views/DashboardView.vue +++ b/frontend/website/src/views/DashboardView.vue @@ -36,14 +36,6 @@ function formatDate(value: string): string { }).format(new Date(`${value}T00:00:00Z`)) } -function isOverdue(scheduledDate: string): boolean { - return scheduledDate < todayDate.value -} - -function overdueLabel(scheduledDate: string): string { - return `Overdue · Due ${formatDate(scheduledDate)}` -} - async function completeAssignment(assignmentId: number): Promise { await schedulesStore.setAssignmentCompleted(assignmentId, true) } @@ -84,7 +76,7 @@ async function completeAssignment(assignmentId: number): Promise {

- Nothing is due today. + Nothing is assigned for today.

    @@ -105,12 +97,6 @@ async function completeAssignment(assignmentId: number): Promise {

    {{ assignment.schedule.set.name }}

    {{ assignment.element.kind }} -

    {{ assignment.element.path.join(' / ') }}

    @@ -404,19 +390,6 @@ async function completeAssignment(assignmentId: number): Promise { text-transform: none; } -.today-assignment__overdue { - display: inline-flex; - width: fit-content; - margin-top: 0.85rem; - padding: 0.3rem 0.55rem; - border-radius: 999px; - color: #923f31; - background: #f7e5dc; - font-size: 0.68rem; - font-weight: 800; - letter-spacing: 0.025em; -} - .today-assignment__path { margin: 1.75rem 0 0; color: #183029; diff --git a/frontend/website/src/views/ScheduleDetailView.vue b/frontend/website/src/views/ScheduleDetailView.vue index 5c72243..9a455a9 100644 --- a/frontend/website/src/views/ScheduleDetailView.vue +++ b/frontend/website/src/views/ScheduleDetailView.vue @@ -1,55 +1,15 @@ @@ -145,82 +93,73 @@ async function setAssignmentCompleted(assignmentId: number, completed: boolean):

    -

    - {{ completionAnnouncement }} -

    +
      +
    1. +
      + + {{ day.assignments.length }} assigned +
      -
      -
      -
      -

      Your plan

      -

      Remaining

      -

      Everything still ahead, kept in scheduled order.

      -
      - - {{ remainingAssignmentCount }} - {{ remainingAssignmentCount === 1 ? 'assignment' : 'assignments' }} - -
      +

      Rest day

      -

      - All assignments are complete. -

      - - -
      - -
      - - - Completed - Finished work, grouped by its original schedule date. - - - - {{ completedAssignmentCount }} - {{ completedAssignmentCount === 1 ? 'assignment' : 'assignments' }} - - - - - -
      -

      - No assignments completed yet. -

      - -
      -
      +
        +
      • +
        + + {{ assignment.element.path.join(' / ') }} + + {{ assignment.element.kind }} +
        +
        +

        + Completed + +

        +

        Not completed

        + + +
        +
      • +
      +
    2. +
    @@ -294,144 +233,155 @@ h1 { color: #68776f; } -.completion-announcement { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} - -.assignment-section { - margin-top: 3rem; -} - -.assignment-section__header { - display: flex; - align-items: end; - justify-content: space-between; - gap: 2rem; -} - -.assignment-section__eyebrow { - margin: 0 0 0.45rem; - color: #926044; - font-size: 0.68rem; - font-weight: 800; - letter-spacing: 0.12em; - text-transform: uppercase; -} - -.assignment-section__header h2, -.completed-section__heading { - font-family: Georgia, 'Times New Roman', serif; - font-weight: 500; - letter-spacing: -0.035em; -} - -.assignment-section__header h2 { - margin: 0; - font-size: clamp(2rem, 5vw, 3rem); - line-height: 1; -} - -.assignment-section__header p:last-child { - margin: 0.65rem 0 0; - color: #68776f; - line-height: 1.55; -} - -.assignment-section__count { - flex: 0 0 auto; - padding: 0.45rem 0.7rem; - border-radius: 999px; - color: #4f665d; - background: rgb(224 233 223 / 72%); - font-size: 0.72rem; - font-weight: 800; -} - -.section-state { +.schedule-days { display: grid; - min-height: 6rem; - place-items: center; - margin: 1.4rem 0 0; - padding: 1.25rem; - border: 1px dashed rgb(24 48 41 / 18%); - border-radius: 1rem; - color: #5e7067; - background: rgb(255 253 247 / 52%); - text-align: center; -} - -.completed-section { - overflow: hidden; - border: 1px solid rgb(24 48 41 / 12%); - border-radius: 1rem; - background: rgb(246 245 237 / 78%); -} - -.completed-section__summary { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1.5rem; - padding: 1.15rem 1.25rem; - cursor: pointer; + gap: 1rem; + margin: 3rem 0 0; + padding: 0; list-style: none; } -.completed-section__summary::-webkit-details-marker { - display: none; +.schedule-day { + overflow: hidden; + border: 1px solid rgb(24 48 41 / 12%); + border-radius: 1rem; + background: rgb(255 253 247 / 82%); + box-shadow: 0 0.75rem 2rem rgb(40 62 52 / 6%); } -.completed-section__summary:focus-visible { - outline: 3px solid rgb(86 127 112 / 34%); - outline-offset: -0.25rem; -} - -.completed-section__introduction { - display: grid; - gap: 0.25rem; -} - -.completed-section__heading { - color: #344e45; - font-size: 1.35rem; -} - -.completed-section__introduction > span:last-child { - color: #79877f; - font-size: 0.78rem; - line-height: 1.45; -} - -.completed-section__controls { - display: inline-flex; +.schedule-day__header { + display: flex; align-items: center; - gap: 0.75rem; + justify-content: space-between; + gap: 1rem; + padding: 0.85rem 1rem; + border-bottom: 1px solid rgb(24 48 41 / 10%); + color: #5e7067; + background: rgb(239 228 212 / 42%); + font-size: 0.75rem; + font-weight: 800; } -.completed-section__chevron { - color: #68776f; - font-size: 1.35rem; - line-height: 1; - transition: transform 160ms ease; +.rest-day { + margin: 0; + padding: 1.2rem 1rem; + color: #79877f; + font-style: italic; } -.completed-section[open] .completed-section__chevron { - transform: rotate(180deg); +.assignment-list { + display: grid; + gap: 0; + margin: 0; + padding: 0; + list-style: none; } -.completed-section__content { - padding: 0 1rem 1rem; +.assignment-list li { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: start; + gap: 1rem; + padding: 1rem; +} + +.assignment-list li.is-completed { + background: rgb(220 235 224 / 34%); +} + +.assignment-list li + li { border-top: 1px solid rgb(24 48 41 / 10%); } +.assignment-path { + min-width: 0; + font-family: Georgia, 'Times New Roman', serif; + font-size: 1.05rem; + overflow-wrap: anywhere; +} + +.assignment-description { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + min-width: 0; +} + +.assignment-kind { + flex: 0 0 auto; + padding: 0.35rem 0.55rem; + border-radius: 999px; + color: #81533a; + background: #efe4d4; + font-size: 0.62rem; + font-weight: 800; + letter-spacing: 0.1em; + text-transform: none; +} + +.assignment-completion { + display: grid; + justify-items: end; + gap: 0.55rem; + min-width: 12rem; +} + +.assignment-completion__status, +.assignment-completion__error { + margin: 0; + font-size: 0.74rem; + font-weight: 700; + text-align: right; +} + +.assignment-completion__status { + color: #5e7067; +} + +.assignment-completion__status time { + display: block; + margin-top: 0.2rem; + color: #344e45; +} + +.assignment-completion__error { + max-width: 14rem; + color: #9b3f32; +} + +.assignment-completion-button { + min-height: 2.4rem; + padding: 0.55rem 0.8rem; + border: 1px solid rgb(24 58 49 / 28%); + border-radius: 0.65rem; + color: #183a31; + background: #fffdf7; + font-size: 0.75rem; + font-weight: 800; + cursor: pointer; +} + +.is-completed .assignment-completion-button { + color: #5e7067; + background: transparent; +} + +.assignment-completion-button:hover:not(:disabled) { + border-color: #285c4e; + background: #f9f5e9; +} + +.assignment-completion-button:focus-visible { + outline: 3px solid rgb(86 127 112 / 34%); + outline-offset: 0.25rem; +} + +.assignment-completion-button:disabled { + cursor: wait; + opacity: 0.65; +} + .page-state { display: grid; min-height: 10rem; @@ -480,26 +430,22 @@ h1 { margin-top: 3rem; } - .assignment-section__header { + .assignment-list li { + grid-template-columns: 1fr; + } + + .assignment-description { align-items: flex-start; - flex-direction: column; - gap: 1rem; } - .completed-section__summary { - align-items: flex-start; - gap: 1rem; - padding: 1rem; + .assignment-completion { + justify-items: start; + min-width: 0; } - .completed-section__controls { - flex-direction: column; - align-items: flex-end; - gap: 0.35rem; - } - - .completed-section__content { - padding: 0 0.75rem 0.75rem; + .assignment-completion__status, + .assignment-completion__error { + text-align: left; } }