From 4ac01460fd810ad63066d9a7ec96d508c5856948 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 3 Aug 2026 20:22:05 +0300 Subject: [PATCH 01/11] test set persistence --- .../Feature/Set/EloquentSetRepositoryTest.php | 107 ++++++++++++++++++ backend/tests/Unit/Set/SetTest.php | 29 +++++ 2 files changed, 136 insertions(+) create mode 100644 backend/tests/Feature/Set/EloquentSetRepositoryTest.php create mode 100644 backend/tests/Unit/Set/SetTest.php diff --git a/backend/tests/Feature/Set/EloquentSetRepositoryTest.php b/backend/tests/Feature/Set/EloquentSetRepositoryTest.php new file mode 100644 index 0000000..bb4679f --- /dev/null +++ b/backend/tests/Feature/Set/EloquentSetRepositoryTest.php @@ -0,0 +1,107 @@ +createUser('creator@example.com'); + $set = app(SetRepository::class)->create(new CreateSetDto( + name: 'Bible', + creator: $creator, + )); + + $this->assertGreaterThan(0, $set->getId()); + $this->assertSame('Bible', $set->getName()); + $this->assertSame($creator->getId(), $set->getCreator()->getId()); + $this->assertDatabaseHas('sets', [ + 'id' => $set->getId(), + 'name' => 'Bible', + 'creator_id' => $creator->getId(), + ]); + } + + public function test_it_lists_every_set_alphabetically(): void + { + $firstCreator = $this->createUser('first@example.com'); + $secondCreator = $this->createUser('second@example.com'); + $repository = app(SetRepository::class); + $repository->create(new CreateSetDto( + name: 'Fitness Program', + creator: $firstCreator, + )); + $repository->create(new CreateSetDto( + name: 'Bible', + creator: $secondCreator, + )); + $repository->create(new CreateSetDto( + name: 'Course', + creator: $firstCreator, + )); + + $sets = $repository->all(); + + $this->assertSame( + ['Bible', 'Course', 'Fitness Program'], + array_map(function ($set): string { + return $set->getName(); + }, $sets), + ); + $this->assertSame( + $secondCreator->getId(), + $sets[0]->getCreator()->getId(), + ); + } + + public function test_it_rejects_duplicate_set_names(): void + { + $creator = $this->createUser('creator@example.com'); + $repository = app(SetRepository::class); + $repository->create(new CreateSetDto( + name: 'Bible', + creator: $creator, + )); + + $this->expectException(QueryException::class); + + $repository->create(new CreateSetDto( + name: 'Bible', + creator: $creator, + )); + } + + public function test_it_prevents_deleting_a_set_creator(): void + { + $creator = $this->createUser('creator@example.com'); + app(SetRepository::class)->create(new CreateSetDto( + name: 'Bible', + creator: $creator, + )); + + $this->expectException(QueryException::class); + + DB::table('users')->where('id', $creator->getId())->delete(); + } + + private function createUser(string $email): User + { + return app(UserRepository::class)->create(new CreateUserDto( + email: new EmailAddress($email), + passwordHash: 'hashed-password', + )); + } +} diff --git a/backend/tests/Unit/Set/SetTest.php b/backend/tests/Unit/Set/SetTest.php new file mode 100644 index 0000000..ed7d75d --- /dev/null +++ b/backend/tests/Unit/Set/SetTest.php @@ -0,0 +1,29 @@ +assertSame(42, $set->getId()); + $this->assertSame('Bible', $set->getName()); + $this->assertSame($creator, $set->getCreator()); + } +} From 5b9df7d02ae87772a0bc64d738866f246e165be3 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 3 Aug 2026 20:22:58 +0300 Subject: [PATCH 02/11] add set persistence --- backend/app/Providers/AppServiceProvider.php | 6 ++ backend/app/Set/CreateSetDto.php | 13 +++++ backend/app/Set/EloquentSetRepository.php | 56 +++++++++++++++++++ backend/app/Set/Set.php | 29 ++++++++++ backend/app/Set/SetModel.php | 26 +++++++++ backend/app/Set/SetRepository.php | 13 +++++ .../2026_08_03_000000_create_sets_table.php | 24 ++++++++ 7 files changed, 167 insertions(+) create mode 100644 backend/app/Set/CreateSetDto.php create mode 100644 backend/app/Set/EloquentSetRepository.php create mode 100644 backend/app/Set/Set.php create mode 100644 backend/app/Set/SetModel.php create mode 100644 backend/app/Set/SetRepository.php create mode 100644 backend/database/migrations/2026_08_03_000000_create_sets_table.php diff --git a/backend/app/Providers/AppServiceProvider.php b/backend/app/Providers/AppServiceProvider.php index 2255353..c9490ba 100644 --- a/backend/app/Providers/AppServiceProvider.php +++ b/backend/app/Providers/AppServiceProvider.php @@ -16,6 +16,8 @@ use App\Email\Emailer; use App\Email\EmailFactory; use App\Email\LaravelEmailer; use App\Email\LaravelEmailFactory; +use App\Set\EloquentSetRepository; +use App\Set\SetRepository; use App\User\EloquentUserRepository; use App\User\UserRepository; use Carbon\CarbonImmutable; @@ -45,6 +47,10 @@ class AppServiceProvider extends ServiceProvider ); $this->app->bind(Emailer::class, LaravelEmailer::class); $this->app->bind(EmailFactory::class, LaravelEmailFactory::class); + $this->app->bind( + SetRepository::class, + EloquentSetRepository::class, + ); $this->app->bind(PasswordHasher::class, BcryptPasswordHasher::class); $this->app->bind(TokenGenerator::class, RandomTokenGenerator::class); $this->app->bind(Clock::class, SystemClock::class); diff --git a/backend/app/Set/CreateSetDto.php b/backend/app/Set/CreateSetDto.php new file mode 100644 index 0000000..ac7b460 --- /dev/null +++ b/backend/app/Set/CreateSetDto.php @@ -0,0 +1,13 @@ + $dto->name, + 'creator_id' => $dto->creator->getId(), + ]); + + return new Set( + id: $model->id, + name: $model->name, + creator: $dto->creator, + ); + } + + public function all(): array + { + $models = SetModel::query() + ->orderBy('name') + ->orderBy('id') + ->get(); + $sets = []; + + foreach ($models as $model) { + $sets[] = $this->toDomain($model); + } + + return $sets; + } + + private function toDomain(SetModel $model): Set + { + $creator = $this->userRepository->find($model->creator_id); + if ($creator === null) { + throw new RuntimeException('set creator not found'); + } + + return new Set( + id: $model->id, + name: $model->name, + creator: $creator, + ); + } +} diff --git a/backend/app/Set/Set.php b/backend/app/Set/Set.php new file mode 100644 index 0000000..064e475 --- /dev/null +++ b/backend/app/Set/Set.php @@ -0,0 +1,29 @@ +id; + } + + public function getName(): string + { + return $this->name; + } + + public function getCreator(): User + { + return $this->creator; + } +} diff --git a/backend/app/Set/SetModel.php b/backend/app/Set/SetModel.php new file mode 100644 index 0000000..021b3a5 --- /dev/null +++ b/backend/app/Set/SetModel.php @@ -0,0 +1,26 @@ +|SetModel newModelQuery() + * @method static Builder|SetModel newQuery() + * @method static Builder|SetModel query() + * + * @mixin \Eloquent + */ +#[Fillable(['name', 'creator_id'])] +class SetModel extends Model +{ + protected $table = 'sets'; + + public $timestamps = false; +} diff --git a/backend/app/Set/SetRepository.php b/backend/app/Set/SetRepository.php new file mode 100644 index 0000000..0545ab1 --- /dev/null +++ b/backend/app/Set/SetRepository.php @@ -0,0 +1,13 @@ + + */ + public function all(): array; +} diff --git a/backend/database/migrations/2026_08_03_000000_create_sets_table.php b/backend/database/migrations/2026_08_03_000000_create_sets_table.php new file mode 100644 index 0000000..589609b --- /dev/null +++ b/backend/database/migrations/2026_08_03_000000_create_sets_table.php @@ -0,0 +1,24 @@ +id(); + $table->string('name')->unique(); + $table->foreignId('creator_id') + ->constrained('users') + ->restrictOnDelete(); + }); + } + + public function down(): void + { + Schema::dropIfExists('sets'); + } +}; From 61ab8c09dc8f20d113f1191f123df445a46236f6 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 3 Aug 2026 20:24:01 +0300 Subject: [PATCH 03/11] test sets list endpoint --- backend/tests/Fakes/FakeSetRepository.php | 49 +++++++++ .../Feature/Set/ListSetsEndpointTest.php | 102 ++++++++++++++++++ .../tests/Unit/Set/UseCases/ListSetsTest.php | 40 +++++++ 3 files changed, 191 insertions(+) create mode 100644 backend/tests/Fakes/FakeSetRepository.php create mode 100644 backend/tests/Feature/Set/ListSetsEndpointTest.php create mode 100644 backend/tests/Unit/Set/UseCases/ListSetsTest.php diff --git a/backend/tests/Fakes/FakeSetRepository.php b/backend/tests/Fakes/FakeSetRepository.php new file mode 100644 index 0000000..a4cff4c --- /dev/null +++ b/backend/tests/Fakes/FakeSetRepository.php @@ -0,0 +1,49 @@ + + */ + private array $sets = []; + + public function create(CreateSetDto $dto): Set + { + $id = count($this->sets) + 1; + $set = new Set( + id: $id, + name: $dto->name, + creator: $dto->creator, + ); + $this->sets[$id] = $set; + + return $this->copy($set); + } + + public function all(): array + { + $sets = array_values($this->sets); + usort($sets, function (Set $first, Set $second): int { + return $first->getName() <=> $second->getName(); + }); + + return array_map(function (Set $set): Set { + return $this->copy($set); + }, $sets); + } + + private function copy(Set $set): Set + { + return new Set( + id: $set->getId(), + name: $set->getName(), + creator: $set->getCreator(), + ); + } +} diff --git a/backend/tests/Feature/Set/ListSetsEndpointTest.php b/backend/tests/Feature/Set/ListSetsEndpointTest.php new file mode 100644 index 0000000..6f8085b --- /dev/null +++ b/backend/tests/Feature/Set/ListSetsEndpointTest.php @@ -0,0 +1,102 @@ +createUser('reader@example.com'); + $otherUser = $this->createUser('creator@example.com'); + $repository = app(SetRepository::class); + $course = $repository->create(new CreateSetDto( + name: 'Course', + creator: $authenticatedUser, + )); + $bible = $repository->create(new CreateSetDto( + name: 'Bible', + creator: $otherUser, + )); + $this->createSession($authenticatedUser); + + $response = $this->withCredentials() + ->withUnencryptedCookie( + AuthMiddleware::COOKIE_NAME, + 'valid-token', + )->getJson('/api/sets'); + + $response->assertOk()->assertExactJson([ + 'sets' => [ + [ + 'id' => $bible->getId(), + 'name' => 'Bible', + ], + [ + 'id' => $course->getId(), + 'name' => 'Course', + ], + ], + ]); + } + + public function test_it_returns_an_empty_catalog(): void + { + $authenticatedUser = $this->createUser('reader@example.com'); + $this->createSession($authenticatedUser); + + $response = $this->withCredentials() + ->withUnencryptedCookie( + AuthMiddleware::COOKIE_NAME, + 'valid-token', + )->getJson('/api/sets'); + + $response->assertOk()->assertExactJson(['sets' => []]); + } + + public function test_it_rejects_an_unauthenticated_request(): void + { + $response = $this->getJson('/api/sets'); + + $response + ->assertStatus(401) + ->assertExactJson(['error' => 'unauthenticated']); + } + + private function createUser(string $email): User + { + return app(UserRepository::class)->create(new CreateUserDto( + email: new EmailAddress($email), + passwordHash: 'hashed-password', + )); + } + + private function createSession(User $user): void + { + $createdAt = new DateTimeImmutable( + '2026-08-03T12:00:00', + new DateTimeZone('UTC'), + ); + app(SessionRepository::class)->create(new CreateSessionDto( + token: 'valid-token', + user: $user, + createdAt: $createdAt, + expiresAt: $createdAt->modify('+7 days'), + )); + } +} diff --git a/backend/tests/Unit/Set/UseCases/ListSetsTest.php b/backend/tests/Unit/Set/UseCases/ListSetsTest.php new file mode 100644 index 0000000..fbdf5fb --- /dev/null +++ b/backend/tests/Unit/Set/UseCases/ListSetsTest.php @@ -0,0 +1,40 @@ +create(new CreateSetDto( + name: 'Course', + creator: $creator, + )); + $repository->create(new CreateSetDto( + name: 'Bible', + creator: $creator, + )); + + $sets = (new ListSets($repository))->execute(); + + $this->assertSame( + ['Bible', 'Course'], + array_map(function ($set): string { + return $set->getName(); + }, $sets), + ); + } +} From 3d96a8d3168090428e57317973e138e207f68caa Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 3 Aug 2026 20:24:36 +0300 Subject: [PATCH 04/11] expose sets list endpoint --- .../app/Http/Controllers/SetController.php | 26 +++++++++++++++++++ .../app/Set/UseCases/ListSets/ListSets.php | 21 +++++++++++++++ backend/routes/api.php | 2 ++ 3 files changed, 49 insertions(+) create mode 100644 backend/app/Http/Controllers/SetController.php create mode 100644 backend/app/Set/UseCases/ListSets/ListSets.php diff --git a/backend/app/Http/Controllers/SetController.php b/backend/app/Http/Controllers/SetController.php new file mode 100644 index 0000000..e432739 --- /dev/null +++ b/backend/app/Http/Controllers/SetController.php @@ -0,0 +1,26 @@ + $set->getId(), + 'name' => $set->getName(), + ]; + }, $this->listSets->execute()); + + return new JsonResponse(['sets' => $sets]); + } +} diff --git a/backend/app/Set/UseCases/ListSets/ListSets.php b/backend/app/Set/UseCases/ListSets/ListSets.php new file mode 100644 index 0000000..d4e80df --- /dev/null +++ b/backend/app/Set/UseCases/ListSets/ListSets.php @@ -0,0 +1,21 @@ + + */ + public function execute(): array + { + return $this->setRepository->all(); + } +} diff --git a/backend/routes/api.php b/backend/routes/api.php index d471fcf..62384a9 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -1,6 +1,7 @@ group(function (): void { Route::get('/me', [AuthController::class, 'me']); + Route::get('/sets', [SetController::class, 'index']); Route::post('/logout', [AuthController::class, 'logout']); }); From 4dfea4ebb3e2d78543c4a8952727a258a3ad2f5e Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 3 Aug 2026 20:25:08 +0300 Subject: [PATCH 05/11] test available set seeds --- .../Feature/Database/DatabaseSeederTest.php | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/backend/tests/Feature/Database/DatabaseSeederTest.php b/backend/tests/Feature/Database/DatabaseSeederTest.php index ca1ec91..45a7e51 100644 --- a/backend/tests/Feature/Database/DatabaseSeederTest.php +++ b/backend/tests/Feature/Database/DatabaseSeederTest.php @@ -3,8 +3,10 @@ namespace Tests\Feature\Database; use App\Auth\PasswordHasher; +use App\Set\SetRepository; use App\Shared\ValueObject\EmailAddress; use App\User\UserRepository; +use Database\Seeders\UserSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -36,4 +38,26 @@ class DatabaseSeederTest extends TestCase $user->getPasswordHash(), )); } + + public function test_it_seeds_the_available_sets_idempotently(): void + { + $this->seed(); + $this->seed(); + + $sets = app(SetRepository::class)->all(); + + $this->assertDatabaseCount('sets', 3); + $this->assertSame( + ['Bible', 'Course', 'Fitness Program'], + array_map(function ($set): string { + return $set->getName(); + }, $sets), + ); + foreach ($sets as $set) { + $this->assertSame( + UserSeeder::EMAIL, + $set->getCreator()->getEmail()->value(), + ); + } + } } From 41c0e7bebeb7b4d7a6e75e451dc668f315abd263 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 3 Aug 2026 20:25:37 +0300 Subject: [PATCH 06/11] seed available sets --- backend/database/seeders/DatabaseSeeder.php | 1 + backend/database/seeders/SetSeeder.php | 45 +++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 backend/database/seeders/SetSeeder.php diff --git a/backend/database/seeders/DatabaseSeeder.php b/backend/database/seeders/DatabaseSeeder.php index def224c..31a5820 100644 --- a/backend/database/seeders/DatabaseSeeder.php +++ b/backend/database/seeders/DatabaseSeeder.php @@ -12,5 +12,6 @@ class DatabaseSeeder extends Seeder public function run(): void { $this->call(UserSeeder::class); + $this->call(SetSeeder::class); } } diff --git a/backend/database/seeders/SetSeeder.php b/backend/database/seeders/SetSeeder.php new file mode 100644 index 0000000..d1ecf95 --- /dev/null +++ b/backend/database/seeders/SetSeeder.php @@ -0,0 +1,45 @@ +findByEmail( + new EmailAddress(UserSeeder::EMAIL), + ); + if ($user === null) { + throw new RuntimeException('seeded user not found'); + } + + $repository = app(SetRepository::class); + $existingNames = array_map(function ($set): string { + return $set->getName(); + }, $repository->all()); + + foreach (self::NAMES as $name) { + if (in_array($name, $existingNames, true)) { + continue; + } + + $repository->create(new CreateSetDto( + name: $name, + creator: $user, + )); + } + } +} From 2072166bdd9265aaaa27c4ee777ec0350cc8aeaa Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 3 Aug 2026 20:28:51 +0300 Subject: [PATCH 07/11] test sets dashboard --- .../website/cypress/e2e/sets-dashboard.cy.ts | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 frontend/website/cypress/e2e/sets-dashboard.cy.ts diff --git a/frontend/website/cypress/e2e/sets-dashboard.cy.ts b/frontend/website/cypress/e2e/sets-dashboard.cy.ts new file mode 100644 index 0000000..7e3bbca --- /dev/null +++ b/frontend/website/cypress/e2e/sets-dashboard.cy.ts @@ -0,0 +1,120 @@ +const authenticatedUser = { + id: 7, + email: 'user@example.com', +} + +function interceptAuthenticatedUser(): void { + cy.intercept('GET', '**/api/me', { + statusCode: 200, + body: { user: authenticatedUser }, + }).as('me') +} + +describe('sets dashboard', () => { + beforeEach(() => { + interceptAuthenticatedUser() + }) + + it('shows every available set by name', () => { + cy.intercept('GET', '**/api/sets', (request) => { + expect(request.headers.accept).to.equal('application/json') + request.reply({ + statusCode: 200, + body: { + sets: [ + { id: 41, name: 'Bible' }, + { id: 58, name: 'Course' }, + { id: 92, name: 'Fitness Program' }, + ], + }, + }) + }).as('sets') + + cy.visit('/dashboard') + cy.wait('@me') + cy.wait('@sets') + + cy.get('h1').should('have.text', 'Available sets') + cy.get('ul[aria-label="Available sets"] h2').then(($headings) => { + expect([...$headings].map((heading) => heading.textContent)).to.deep.equal([ + 'Bible', + 'Course', + 'Fitness Program', + ]) + }) + cy.get('ul[aria-label="Available sets"]') + .should('not.contain.text', '41') + .and('not.contain.text', '58') + .and('not.contain.text', '92') + .find('a, button') + .should('not.exist') + }) + + it('shows loading and empty catalog states', () => { + cy.intercept('GET', '**/api/sets', { + delay: 500, + statusCode: 200, + body: { sets: [] }, + }).as('sets') + + cy.visit('/dashboard') + cy.wait('@me') + cy.get('[role="status"]').should('have.text', 'Loading sets...') + cy.wait('@sets') + + cy.get('[role="status"]').should( + 'have.text', + 'No sets are available yet.', + ) + }) + + it('shows malformed catalog responses as errors', () => { + cy.intercept('GET', '**/api/sets', { + statusCode: 200, + body: { sets: [{ id: 41, name: 12 }] }, + }).as('sets') + + cy.visit('/dashboard') + cy.wait('@me') + cy.wait('@sets') + + cy.get('[role="alert"]').should( + 'contain.text', + "We couldn't load the available sets.", + ) + }) + + it('retries after a catalog request fails', () => { + let requestCount = 0 + cy.intercept('GET', '**/api/sets', (request) => { + requestCount += 1 + request.alias = `sets${requestCount}` + + if (requestCount === 1) { + request.reply({ statusCode: 500 }) + return + } + + request.reply({ + statusCode: 200, + body: { sets: [{ id: 41, name: 'Bible' }] }, + }) + }) + + cy.visit('/dashboard') + cy.wait('@me') + cy.wait('@sets1') + + cy.get('[role="alert"]').should( + 'contain.text', + "We couldn't load the available sets.", + ) + cy.contains('button', 'Try again').click() + cy.wait('@sets2') + + cy.get('ul[aria-label="Available sets"] h2').should( + 'have.text', + 'Bible', + ) + }) +}) From 321c1b7bb01f0c1123b14e128d6e528b34e18371 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 3 Aug 2026 20:31:59 +0300 Subject: [PATCH 08/11] show sets on dashboard --- .../website/cypress/e2e/session-auth.cy.ts | 9 +- .../website/cypress/e2e/sets-dashboard.cy.ts | 2 +- frontend/website/src/stores/sets.ts | 65 +++++++ frontend/website/src/views/DashboardView.vue | 166 ++++++++++++++++-- 4 files changed, 221 insertions(+), 21 deletions(-) create mode 100644 frontend/website/src/stores/sets.ts diff --git a/frontend/website/cypress/e2e/session-auth.cy.ts b/frontend/website/cypress/e2e/session-auth.cy.ts index cd35dfd..defb942 100644 --- a/frontend/website/cypress/e2e/session-auth.cy.ts +++ b/frontend/website/cypress/e2e/session-auth.cy.ts @@ -38,6 +38,13 @@ function visitDashboardAndLogout(): void { } describe('session authentication', () => { + beforeEach(() => { + cy.intercept('GET', '**/api/sets', { + statusCode: 200, + body: { sets: [] }, + }) + }) + it('restores an authenticated session on a protected route', () => { cy.intercept('GET', '**/api/me', { statusCode: 200, @@ -48,7 +55,7 @@ describe('session authentication', () => { cy.wait('@me') cy.location('pathname').should('equal', '/dashboard') - cy.get('h1').should('have.text', 'Your next step starts here.') + cy.get('h1').should('have.text', 'Available sets') }) it('redirects an unauthenticated protected route to login', () => { diff --git a/frontend/website/cypress/e2e/sets-dashboard.cy.ts b/frontend/website/cypress/e2e/sets-dashboard.cy.ts index 7e3bbca..3872b10 100644 --- a/frontend/website/cypress/e2e/sets-dashboard.cy.ts +++ b/frontend/website/cypress/e2e/sets-dashboard.cy.ts @@ -63,7 +63,7 @@ describe('sets dashboard', () => { cy.wait('@sets') cy.get('[role="status"]').should( - 'have.text', + 'contain.text', 'No sets are available yet.', ) }) diff --git a/frontend/website/src/stores/sets.ts b/frontend/website/src/stores/sets.ts new file mode 100644 index 0000000..57b0bcc --- /dev/null +++ b/frontend/website/src/stores/sets.ts @@ -0,0 +1,65 @@ +import { ref } from 'vue' +import { defineStore } from 'pinia' +import { z } from 'zod' + +import { API_BASE } from '@/utils/apiBase' + +export const setSummarySchema = z.object({ + id: z.number().int().positive(), + name: z.string().min(1), +}) + +const setsResponseSchema = z.object({ + sets: z.array(setSummarySchema), +}) + +export type SetSummary = z.infer + +const LOAD_ERROR = "We couldn't load the available sets." + +export const useSetsStore = defineStore('sets', () => { + const sets = ref([]) + const loading = ref(false) + const error = ref(null) + + async function fetchSets(): Promise { + loading.value = true + error.value = null + + try { + const response = await fetch(`${API_BASE}/api/sets`, { + method: 'GET', + credentials: 'include', + headers: { + Accept: 'application/json', + }, + }) + + if (response.status !== 200) { + sets.value = [] + error.value = LOAD_ERROR + + return false + } + + const responseBody: unknown = await response.json() + sets.value = setsResponseSchema.parse(responseBody).sets + + return true + } catch { + sets.value = [] + error.value = LOAD_ERROR + + return false + } finally { + loading.value = false + } + } + + return { + sets, + loading, + error, + fetchSets, + } +}) diff --git a/frontend/website/src/views/DashboardView.vue b/frontend/website/src/views/DashboardView.vue index c61783f..182def5 100644 --- a/frontend/website/src/views/DashboardView.vue +++ b/frontend/website/src/views/DashboardView.vue @@ -1,12 +1,21 @@