From a74bb853d4fc1f6717a3abe8be8655652cbbe5fd Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 25 May 2026 19:58:32 +0300 Subject: [PATCH 01/17] test sets endpoint --- backend/tests/Feature/SetsEndpointTest.php | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 backend/tests/Feature/SetsEndpointTest.php diff --git a/backend/tests/Feature/SetsEndpointTest.php b/backend/tests/Feature/SetsEndpointTest.php new file mode 100644 index 0000000..a53d5da --- /dev/null +++ b/backend/tests/Feature/SetsEndpointTest.php @@ -0,0 +1,40 @@ +create(new CreateSetDto( + name: 'Baderech HaAvodah', + )); + $dailyLearningSet = $setRepository->create(new CreateSetDto( + name: 'Daily Learning', + )); + + $response = $this->getJson('/api/sets'); + + $response->assertOk(); + $response->assertExactJson([ + 'sets' => [ + [ + 'id' => $baderechSet->getId(), + 'name' => $baderechSet->getName(), + ], + [ + 'id' => $dailyLearningSet->getId(), + 'name' => $dailyLearningSet->getName(), + ], + ], + ]); + } +} From c4b95137e59d30eb9d2a267b2662c438cbdbad5c Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 25 May 2026 19:59:40 +0300 Subject: [PATCH 02/17] add sets endpoint --- backend/app/Controllers/SetController.php | 35 +++++++++++++++++++++++ backend/routes/api.php | 2 ++ 2 files changed, 37 insertions(+) create mode 100644 backend/app/Controllers/SetController.php diff --git a/backend/app/Controllers/SetController.php b/backend/app/Controllers/SetController.php new file mode 100644 index 0000000..4e57326 --- /dev/null +++ b/backend/app/Controllers/SetController.php @@ -0,0 +1,35 @@ +setRepository->getAll() as $set) { + $sets[] = $this->buildSetPayload($set); + } + + return new JsonResponse([ + 'sets' => $sets, + ], 200); + } + + /** + * @return array{id: int, name: string} + */ + private function buildSetPayload(DomainSet $set): array + { + return [ + 'id' => $set->getId(), + 'name' => $set->getName(), + ]; + } +} diff --git a/backend/routes/api.php b/backend/routes/api.php index 9e1c55e..92cd42d 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -1,6 +1,7 @@ middleware(AuthMiddleware::class); +Route::get('/sets', [SetController::class, 'index']); From d385a372669f9d15542dbae463b2a33712c5376e Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 25 May 2026 20:00:46 +0300 Subject: [PATCH 03/17] test media set cards --- frontend/rabbi_gerzi/cypress/e2e/media.cy.ts | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 frontend/rabbi_gerzi/cypress/e2e/media.cy.ts diff --git a/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts new file mode 100644 index 0000000..7cfd885 --- /dev/null +++ b/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts @@ -0,0 +1,22 @@ +describe('media page sets', () => { + it('fetches and renders set cards', () => { + cy.intercept('GET', '**/api/sets', { + statusCode: 200, + body: { + sets: [ + { + id: 1, + name: 'Baderech HaAvodah', + }, + ], + }, + }).as('fetchSets') + + cy.visit('/media') + + cy.wait('@fetchSets') + cy.get('[data-cy="media-set-card"]').should('have.length', 1) + cy.contains('[data-cy="media-set-card"]', 'Baderech HaAvodah') + .should('be.visible') + }) +}) From 6e73d64c2096699f0d8d20952c014e19702d5c1e Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 25 May 2026 20:05:13 +0300 Subject: [PATCH 04/17] test real media sets --- frontend/rabbi_gerzi/cypress/e2e/media.cy.ts | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts index 7cfd885..11ed22c 100644 --- a/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts +++ b/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts @@ -1,21 +1,9 @@ describe('media page sets', () => { - it('fetches and renders set cards', () => { - cy.intercept('GET', '**/api/sets', { - statusCode: 200, - body: { - sets: [ - { - id: 1, - name: 'Baderech HaAvodah', - }, - ], - }, - }).as('fetchSets') - + it('fetches and renders seeded set cards', () => { cy.visit('/media') - cy.wait('@fetchSets') - cy.get('[data-cy="media-set-card"]').should('have.length', 1) + cy.get('[data-cy="media-set-card"]', { timeout: 10000 }) + .should('have.length', 1) cy.contains('[data-cy="media-set-card"]', 'Baderech HaAvodah') .should('be.visible') }) From 535fb29e112abc88b41879e7905389fdfa29f3ab Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 25 May 2026 20:09:21 +0300 Subject: [PATCH 05/17] render media set cards --- frontend/rabbi_gerzi/src/stores/mediaSets.ts | 44 +++++++ frontend/rabbi_gerzi/src/views/MediaPage.vue | 116 ++++++++++++++++++- 2 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 frontend/rabbi_gerzi/src/stores/mediaSets.ts diff --git a/frontend/rabbi_gerzi/src/stores/mediaSets.ts b/frontend/rabbi_gerzi/src/stores/mediaSets.ts new file mode 100644 index 0000000..c524fcb --- /dev/null +++ b/frontend/rabbi_gerzi/src/stores/mediaSets.ts @@ -0,0 +1,44 @@ +import { ref } from 'vue' +import { defineStore } from 'pinia' + +export interface MediaSet { + id: number + name: string +} + +interface SetsResponse { + sets: MediaSet[] +} + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string + +export const useMediaSetsStore = defineStore('mediaSets', () => { + const sets = ref([]) + const isLoading = ref(false) + const error = ref(null) + + async function fetchSets(): Promise { + error.value = null + isLoading.value = true + + try { + const response = await fetch(`${API_BASE_URL}/api/sets`) + + if (!response.ok) { + sets.value = [] + error.value = 'Could not load media sets' + return + } + + const data: SetsResponse = await response.json() + sets.value = data.sets + } catch { + sets.value = [] + error.value = 'Network error - could not load media sets' + } finally { + isLoading.value = false + } + } + + return { sets, isLoading, error, fetchSets } +}) diff --git a/frontend/rabbi_gerzi/src/views/MediaPage.vue b/frontend/rabbi_gerzi/src/views/MediaPage.vue index b405d36..74ea6e9 100644 --- a/frontend/rabbi_gerzi/src/views/MediaPage.vue +++ b/frontend/rabbi_gerzi/src/views/MediaPage.vue @@ -1,5 +1,15 @@ @@ -24,7 +64,7 @@ import SiteHeader from '@/components/SiteHeader.vue' } .media-page__main { - padding: 5rem 2rem; + padding: 5rem 2rem 6rem; } .media-page__hero { @@ -64,13 +104,83 @@ import SiteHeader from '@/components/SiteHeader.vue' line-height: 1.7; } +.media-page__sets { + max-width: 1100px; + margin: 3rem auto 0; +} + +.media-page__sets-header { + margin-bottom: 1.5rem; +} + +.media-page__section-heading { + margin: 0; + color: var(--color-slate); + font-family: var(--font-serif); + font-size: clamp(1.8rem, 3vw, 2.6rem); + font-weight: 400; + line-height: 1.2; +} + +.media-page__status { + margin: 0; + padding: 1rem 1.25rem; + color: var(--color-text-muted); + background: var(--color-white); + border: 1px solid var(--color-border); + border-radius: 8px; + font-size: 0.95rem; +} + +.media-page__status--error { + color: #7c2d2d; + border-color: #e5b8b8; +} + +.media-page__grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 1rem; +} + +.media-page__card { + min-height: 170px; + padding: 1.5rem; + background: var(--color-white); + border: 1px solid var(--color-border); + border-top: 4px solid var(--color-olive); + border-radius: 8px; +} + +.media-page__card-kicker { + margin: 0 0 0.8rem; + color: var(--color-olive); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.media-page__card-title { + margin: 0; + color: var(--color-slate-dark); + font-family: var(--font-serif); + font-size: 1.45rem; + font-weight: 400; + line-height: 1.35; +} + @media (max-width: 768px) { .media-page__main { - padding: 3rem 1rem; + padding: 3rem 1rem 4rem; } .media-page__hero { padding: 3rem 1.5rem; } + + .media-page__sets { + margin-top: 2rem; + } } From 95773676224a96524f048c3c945496d9fea73c20 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 25 May 2026 20:18:25 +0300 Subject: [PATCH 06/17] document nix shell commands --- AGENTS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 73e2226..3a4a6c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,3 +23,9 @@ Before responding to the first user message in a session, you MUST: Skipping this protocol caused real bugs and rework in past sessions (work landed on master, TDD order was lost, formatter not run, banned constructs slipped in). Treat the protocol as non-negotiable. + +## Command execution + +When running commands that need the Nix devshell, set `login: false`. +The login shell resets `PATH` and can hide tools like `phpcs`, Cypress, +and the devshell Node version. From 67dd376a2ff116440572490a34f2f045ab4ec1c9 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 25 May 2026 20:24:59 +0300 Subject: [PATCH 07/17] format backend code --- backend/app/Auth/CreateSessionDto.php | 3 ++- .../app/Auth/EloquentSessionRepository.php | 4 +++- backend/app/Auth/Session.php | 3 ++- .../AuthenticateUser/AuthenticateUser.php | 3 ++- .../AuthenticateUserRequest.php | 3 ++- .../UseCases/CreateSession/CreateSession.php | 3 ++- backend/app/Auth/UseCases/Logout/Logout.php | 3 ++- backend/app/Controllers/AuthController.php | 11 ++++++---- backend/app/Controllers/SetController.php | 4 +++- backend/app/Element/CreateElementDto.php | 3 ++- backend/app/Element/Element.php | 3 ++- .../app/Element/EloquentElementRepository.php | 4 +++- .../UseCases/CreateElement/CreateElement.php | 3 ++- .../CreateElement/CreateElementRequest.php | 3 ++- .../app/Exceptions/BadRequestException.php | 4 +++- .../app/Exceptions/UnauthorizedException.php | 4 +++- .../app/Http/Middleware/AuthMiddleware.php | 3 ++- backend/app/Set/CreateSetDto.php | 3 ++- backend/app/Set/Set.php | 3 ++- .../app/Shared/ValueObject/EmailAddress.php | 6 +++--- backend/app/User/CreateUserDto.php | 3 ++- backend/app/User/EloquentUserRepository.php | 2 +- backend/app/User/User.php | 3 ++- backend/app/User/UserModel.php | 6 ------ backend/config/auth.php | 5 ++++- backend/config/cache.php | 5 ++++- backend/config/database.php | 21 +++++++++++++++---- backend/config/filesystems.php | 10 +++++++-- backend/config/logging.php | 5 ++++- backend/config/mail.php | 8 ++++++- backend/config/queue.php | 5 ++++- backend/config/session.php | 4 ++-- backend/public/index.php | 9 +++++--- backend/tests/Fakes/FakeClock.php | 4 +++- backend/tests/Fakes/FakeTokenGenerator.php | 4 +++- .../Auth/Middleware/AuthMiddlewareTest.php | 10 ++++----- .../Auth/UseCases/AuthenticateUserTest.php | 5 ++++- .../Unit/Auth/UseCases/CreateSessionTest.php | 10 +++++++-- .../tests/Unit/Auth/UseCases/LogoutTest.php | 6 +++--- .../Unit/Controllers/AuthControllerTest.php | 16 +++++++------- 40 files changed, 146 insertions(+), 71 deletions(-) diff --git a/backend/app/Auth/CreateSessionDto.php b/backend/app/Auth/CreateSessionDto.php index 2e5e2f7..d22b708 100644 --- a/backend/app/Auth/CreateSessionDto.php +++ b/backend/app/Auth/CreateSessionDto.php @@ -12,5 +12,6 @@ class CreateSessionDto public User $user, public DateTimeImmutable $createdAt, public DateTimeImmutable $expiresAt, - ) {} + ) { + } } diff --git a/backend/app/Auth/EloquentSessionRepository.php b/backend/app/Auth/EloquentSessionRepository.php index 01cd4be..92689b7 100644 --- a/backend/app/Auth/EloquentSessionRepository.php +++ b/backend/app/Auth/EloquentSessionRepository.php @@ -8,7 +8,9 @@ use DateTimeZone; class EloquentSessionRepository implements SessionRepository { - public function __construct(private UserRepository $userRepo) {} + public function __construct(private UserRepository $userRepo) + { + } public function create(CreateSessionDto $dto): Session { diff --git a/backend/app/Auth/Session.php b/backend/app/Auth/Session.php index b433114..b95905b 100644 --- a/backend/app/Auth/Session.php +++ b/backend/app/Auth/Session.php @@ -12,7 +12,8 @@ class Session private User $user, private DateTimeImmutable $createdAt, private DateTimeImmutable $expiresAt, - ) {} + ) { + } public function getToken(): string { diff --git a/backend/app/Auth/UseCases/AuthenticateUser/AuthenticateUser.php b/backend/app/Auth/UseCases/AuthenticateUser/AuthenticateUser.php index 7e8c92c..e91d487 100644 --- a/backend/app/Auth/UseCases/AuthenticateUser/AuthenticateUser.php +++ b/backend/app/Auth/UseCases/AuthenticateUser/AuthenticateUser.php @@ -14,7 +14,8 @@ class AuthenticateUser public function __construct( private UserRepository $userRepo, private PasswordHasher $hasher, - ) {} + ) { + } /** * @throws BadRequestException diff --git a/backend/app/Auth/UseCases/AuthenticateUser/AuthenticateUserRequest.php b/backend/app/Auth/UseCases/AuthenticateUser/AuthenticateUserRequest.php index aa8b1df..d7cf6dd 100644 --- a/backend/app/Auth/UseCases/AuthenticateUser/AuthenticateUserRequest.php +++ b/backend/app/Auth/UseCases/AuthenticateUser/AuthenticateUserRequest.php @@ -7,5 +7,6 @@ class AuthenticateUserRequest public function __construct( public ?string $email, public ?string $password, - ) {} + ) { + } } diff --git a/backend/app/Auth/UseCases/CreateSession/CreateSession.php b/backend/app/Auth/UseCases/CreateSession/CreateSession.php index db6403f..83014a6 100644 --- a/backend/app/Auth/UseCases/CreateSession/CreateSession.php +++ b/backend/app/Auth/UseCases/CreateSession/CreateSession.php @@ -17,7 +17,8 @@ class CreateSession private SessionRepository $sessionRepo, private TokenGenerator $tokenGenerator, private Clock $clock, - ) {} + ) { + } public function execute(User $user): Session { diff --git a/backend/app/Auth/UseCases/Logout/Logout.php b/backend/app/Auth/UseCases/Logout/Logout.php index 31b16de..793666b 100644 --- a/backend/app/Auth/UseCases/Logout/Logout.php +++ b/backend/app/Auth/UseCases/Logout/Logout.php @@ -8,7 +8,8 @@ class Logout { public function __construct( private SessionRepository $sessionRepo, - ) {} + ) { + } public function execute(string $token): void { diff --git a/backend/app/Controllers/AuthController.php b/backend/app/Controllers/AuthController.php index 841b4c4..43c9964 100644 --- a/backend/app/Controllers/AuthController.php +++ b/backend/app/Controllers/AuthController.php @@ -20,7 +20,8 @@ class AuthController private AuthenticateUser $authenticateUser, private CreateSession $createSession, private Logout $logout, - ) {} + ) { + } public function login(Request $request): JsonResponse { @@ -33,11 +34,13 @@ class AuthController ); } catch (BadRequestException $exception) { return new JsonResponse( - ['error' => $exception->getMessage()], 400 + ['error' => $exception->getMessage()], + 400 ); } catch (UnauthorizedException $exception) { return new JsonResponse( - ['error' => $exception->getMessage()], 401 + ['error' => $exception->getMessage()], + 401 ); } @@ -71,7 +74,7 @@ class AuthController } /** - * @return array{id: int, email: string, firstname: string, lastname: string} + * @return array{id: int, email: string} */ private function buildUserPayload(User $user): array { diff --git a/backend/app/Controllers/SetController.php b/backend/app/Controllers/SetController.php index 4e57326..c7f34e0 100644 --- a/backend/app/Controllers/SetController.php +++ b/backend/app/Controllers/SetController.php @@ -8,7 +8,9 @@ use Illuminate\Http\JsonResponse; class SetController { - public function __construct(private SetRepository $setRepository) {} + public function __construct(private SetRepository $setRepository) + { + } public function index(): JsonResponse { diff --git a/backend/app/Element/CreateElementDto.php b/backend/app/Element/CreateElementDto.php index db77dd7..b815300 100644 --- a/backend/app/Element/CreateElementDto.php +++ b/backend/app/Element/CreateElementDto.php @@ -10,5 +10,6 @@ class CreateElementDto public Set $set, public string $title, public ?Element $parentElement, - ) {} + ) { + } } diff --git a/backend/app/Element/Element.php b/backend/app/Element/Element.php index 9ef75f6..143df26 100644 --- a/backend/app/Element/Element.php +++ b/backend/app/Element/Element.php @@ -11,7 +11,8 @@ class Element private string $title, private Set $set, private ?Element $parentElement, - ) {} + ) { + } public function getId(): int { diff --git a/backend/app/Element/EloquentElementRepository.php b/backend/app/Element/EloquentElementRepository.php index 56ec2e6..61e1433 100644 --- a/backend/app/Element/EloquentElementRepository.php +++ b/backend/app/Element/EloquentElementRepository.php @@ -8,7 +8,9 @@ use DomainException; class EloquentElementRepository implements ElementRepository { - public function __construct(private SetRepository $setRepo) {} + public function __construct(private SetRepository $setRepo) + { + } public function create(CreateElementDto $dto): Element { diff --git a/backend/app/Element/UseCases/CreateElement/CreateElement.php b/backend/app/Element/UseCases/CreateElement/CreateElement.php index 6656338..9b32cd7 100644 --- a/backend/app/Element/UseCases/CreateElement/CreateElement.php +++ b/backend/app/Element/UseCases/CreateElement/CreateElement.php @@ -15,7 +15,8 @@ class CreateElement public function __construct( private ElementRepository $elementRepo, private SetRepository $setRepo, - ) {} + ) { + } /** * @throws BadRequestException diff --git a/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php b/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php index b9f0dbd..eef7e7e 100644 --- a/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php +++ b/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php @@ -8,5 +8,6 @@ class CreateElementRequest public ?int $setId, public ?string $title, public ?int $parentElementId, - ) {} + ) { + } } diff --git a/backend/app/Exceptions/BadRequestException.php b/backend/app/Exceptions/BadRequestException.php index b900f47..1670d31 100644 --- a/backend/app/Exceptions/BadRequestException.php +++ b/backend/app/Exceptions/BadRequestException.php @@ -4,4 +4,6 @@ namespace App\Exceptions; use DomainException; -class BadRequestException extends DomainException {} +class BadRequestException extends DomainException +{ +} diff --git a/backend/app/Exceptions/UnauthorizedException.php b/backend/app/Exceptions/UnauthorizedException.php index f5d406e..84f3f71 100644 --- a/backend/app/Exceptions/UnauthorizedException.php +++ b/backend/app/Exceptions/UnauthorizedException.php @@ -4,4 +4,6 @@ namespace App\Exceptions; use DomainException; -class UnauthorizedException extends DomainException {} +class UnauthorizedException extends DomainException +{ +} diff --git a/backend/app/Http/Middleware/AuthMiddleware.php b/backend/app/Http/Middleware/AuthMiddleware.php index 8e21ece..0589cdb 100644 --- a/backend/app/Http/Middleware/AuthMiddleware.php +++ b/backend/app/Http/Middleware/AuthMiddleware.php @@ -16,7 +16,8 @@ class AuthMiddleware public function __construct( private SessionRepository $sessionRepo, private Clock $clock, - ) {} + ) { + } /** * @param Closure(Request): Response $next diff --git a/backend/app/Set/CreateSetDto.php b/backend/app/Set/CreateSetDto.php index 2999a88..841b0a4 100644 --- a/backend/app/Set/CreateSetDto.php +++ b/backend/app/Set/CreateSetDto.php @@ -6,5 +6,6 @@ class CreateSetDto { public function __construct( public string $name, - ) {} + ) { + } } diff --git a/backend/app/Set/Set.php b/backend/app/Set/Set.php index d045e70..bb6f51a 100644 --- a/backend/app/Set/Set.php +++ b/backend/app/Set/Set.php @@ -7,7 +7,8 @@ class Set public function __construct( private int $id, private string $name, - ) {} + ) { + } public function getId(): int { diff --git a/backend/app/Shared/ValueObject/EmailAddress.php b/backend/app/Shared/ValueObject/EmailAddress.php index a744918..00ed5cf 100644 --- a/backend/app/Shared/ValueObject/EmailAddress.php +++ b/backend/app/Shared/ValueObject/EmailAddress.php @@ -18,15 +18,15 @@ final readonly class EmailAddress $trimmed = trim($email); if ($trimmed === '' || ! str_contains($trimmed, '@')) { - throw new InvalidArgumentException(self::ERROR_MESSAGE." $email"); + throw new InvalidArgumentException(self::ERROR_MESSAGE . " $email"); } [$local, $domain] = explode('@', $trimmed, 2); $this->domain = mb_strtolower($domain); - $normalized = $local.'@'.$this->domain; + $normalized = $local . '@' . $this->domain; if (filter_var($normalized, FILTER_VALIDATE_EMAIL) === false) { - throw new InvalidArgumentException(self::ERROR_MESSAGE." $email"); + throw new InvalidArgumentException(self::ERROR_MESSAGE . " $email"); } $this->normalized = $normalized; diff --git a/backend/app/User/CreateUserDto.php b/backend/app/User/CreateUserDto.php index d10b373..848d9f7 100644 --- a/backend/app/User/CreateUserDto.php +++ b/backend/app/User/CreateUserDto.php @@ -9,5 +9,6 @@ class CreateUserDto public function __construct( public EmailAddress $email, public string $passwordHash, - ) {} + ) { + } } diff --git a/backend/app/User/EloquentUserRepository.php b/backend/app/User/EloquentUserRepository.php index f69ec4a..8a1a2c9 100644 --- a/backend/app/User/EloquentUserRepository.php +++ b/backend/app/User/EloquentUserRepository.php @@ -25,7 +25,7 @@ class EloquentUserRepository implements UserRepository public function findByEmailDomain(string $domain): array { - $models = UserModel::where('email', 'like', '%@'.$domain)->get(); + $models = UserModel::where('email', 'like', '%@' . $domain)->get(); $users = []; foreach ($models as $model) { $users[] = $this->toDomain($model); diff --git a/backend/app/User/User.php b/backend/app/User/User.php index 3d8ed63..e73b5aa 100644 --- a/backend/app/User/User.php +++ b/backend/app/User/User.php @@ -10,7 +10,8 @@ class User private int $id, private EmailAddress $email, private string $passwordHash, - ) {} + ) { + } public function getId(): int { diff --git a/backend/app/User/UserModel.php b/backend/app/User/UserModel.php index 7d8c09d..6bde5dd 100644 --- a/backend/app/User/UserModel.php +++ b/backend/app/User/UserModel.php @@ -9,12 +9,6 @@ use Illuminate\Database\Eloquent\Model; * @property string $email * @property string $password_hash * - * @method static \Illuminate\Database\Eloquent\Builder|UserModel newModelQuery() - * @method static \Illuminate\Database\Eloquent\Builder|UserModel newQuery() - * @method static \Illuminate\Database\Eloquent\Builder|UserModel query() - * @method static \Illuminate\Database\Eloquent\Builder|UserModel whereEmail($value) - * @method static \Illuminate\Database\Eloquent\Builder|UserModel whereId($value) - * * @mixin \Eloquent */ class UserModel extends Model diff --git a/backend/config/auth.php b/backend/config/auth.php index d7568ff..3f8d685 100644 --- a/backend/config/auth.php +++ b/backend/config/auth.php @@ -95,7 +95,10 @@ return [ 'passwords' => [ 'users' => [ 'provider' => 'users', - 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'table' => env( + 'AUTH_PASSWORD_RESET_TOKEN_TABLE', + 'password_reset_tokens' + ), 'expire' => 60, 'throttle' => 60, ], diff --git a/backend/config/cache.php b/backend/config/cache.php index c68acdf..8047361 100644 --- a/backend/config/cache.php +++ b/backend/config/cache.php @@ -112,7 +112,10 @@ return [ | */ - 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), + 'prefix' => env( + 'CACHE_PREFIX', + Str::slug((string) env('APP_NAME', 'laravel')) . '-cache-' + ), /* |-------------------------------------------------------------------------- diff --git a/backend/config/database.php b/backend/config/database.php index abbb88e..aed38f9 100644 --- a/backend/config/database.php +++ b/backend/config/database.php @@ -111,7 +111,10 @@ return [ 'prefix' => '', 'prefix_indexes' => true, // 'encrypt' => env('DB_ENCRYPT', 'yes'), - // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + // 'trust_server_certificate' => env( + // 'DB_TRUST_SERVER_CERTIFICATE', + // 'false' + // ), ], ], @@ -149,7 +152,11 @@ return [ 'options' => [ 'cluster' => env('REDIS_CLUSTER', 'redis'), - 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'), + 'prefix' => env( + 'REDIS_PREFIX', + Str::slug((string) env('APP_NAME', 'laravel')) + . '-database-' + ), 'persistent' => env('REDIS_PERSISTENT', false), ], @@ -161,7 +168,10 @@ return [ 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_DB', '0'), 'max_retries' => env('REDIS_MAX_RETRIES', 3), - 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_algorithm' => env( + 'REDIS_BACKOFF_ALGORITHM', + 'decorrelated_jitter' + ), 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), ], @@ -174,7 +184,10 @@ return [ 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_CACHE_DB', '1'), 'max_retries' => env('REDIS_MAX_RETRIES', 3), - 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_algorithm' => env( + 'REDIS_BACKOFF_ALGORITHM', + 'decorrelated_jitter' + ), 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), ], diff --git a/backend/config/filesystems.php b/backend/config/filesystems.php index 37d8fca..9d9f625 100644 --- a/backend/config/filesystems.php +++ b/backend/config/filesystems.php @@ -41,7 +41,10 @@ return [ 'public' => [ 'driver' => 'local', 'root' => storage_path('app/public'), - 'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage', + 'url' => rtrim( + env('APP_URL', 'http://localhost'), + '/' + ) . '/storage', 'visibility' => 'public', 'throw' => false, 'report' => false, @@ -55,7 +58,10 @@ return [ 'bucket' => env('AWS_BUCKET'), 'url' => env('AWS_URL'), 'endpoint' => env('AWS_ENDPOINT'), - 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'use_path_style_endpoint' => env( + 'AWS_USE_PATH_STYLE_ENDPOINT', + false + ), 'throw' => false, 'report' => false, ], diff --git a/backend/config/logging.php b/backend/config/logging.php index b09cb25..fe06e47 100644 --- a/backend/config/logging.php +++ b/backend/config/logging.php @@ -89,7 +89,10 @@ return [ 'handler_with' => [ 'host' => env('PAPERTRAIL_URL'), 'port' => env('PAPERTRAIL_PORT'), - 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://' + . env('PAPERTRAIL_URL') + . ':' + . env('PAPERTRAIL_PORT'), ], 'processors' => [PsrLogMessageProcessor::class], ], diff --git a/backend/config/mail.php b/backend/config/mail.php index e32e88d..8d18020 100644 --- a/backend/config/mail.php +++ b/backend/config/mail.php @@ -46,7 +46,13 @@ return [ 'username' => env('MAIL_USERNAME'), 'password' => env('MAIL_PASSWORD'), 'timeout' => null, - 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + 'local_domain' => env( + 'MAIL_EHLO_DOMAIN', + parse_url( + (string) env('APP_URL', 'http://localhost'), + PHP_URL_HOST + ) + ), ], 'ses' => [ diff --git a/backend/config/queue.php b/backend/config/queue.php index 79c2c0a..e0f7e11 100644 --- a/backend/config/queue.php +++ b/backend/config/queue.php @@ -57,7 +57,10 @@ return [ 'driver' => 'sqs', 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'prefix' => env( + 'SQS_PREFIX', + 'https://sqs.us-east-1.amazonaws.com/your-account-id' + ), 'queue' => env('SQS_QUEUE', 'default'), 'suffix' => env('SQS_SUFFIX'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), diff --git a/backend/config/session.php b/backend/config/session.php index f574482..34a19a2 100644 --- a/backend/config/session.php +++ b/backend/config/session.php @@ -129,7 +129,7 @@ return [ 'cookie' => env( 'SESSION_COOKIE', - Str::slug((string) env('APP_NAME', 'laravel')).'-session' + Str::slug((string) env('APP_NAME', 'laravel')) . '-session' ), /* @@ -193,7 +193,7 @@ return [ | take place, and can be used to mitigate CSRF attacks. By default, we | will set this value to "lax" to permit secure cross-site requests. | - | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | See MDN Set-Cookie SameSite documentation. | | Supported: "lax", "strict", "none", null | diff --git a/backend/public/index.php b/backend/public/index.php index ee8f07e..f1aabdc 100644 --- a/backend/public/index.php +++ b/backend/public/index.php @@ -1,20 +1,23 @@ handleRequest(Request::capture()); diff --git a/backend/tests/Fakes/FakeClock.php b/backend/tests/Fakes/FakeClock.php index f112836..7b14d8d 100644 --- a/backend/tests/Fakes/FakeClock.php +++ b/backend/tests/Fakes/FakeClock.php @@ -7,7 +7,9 @@ use DateTimeImmutable; class FakeClock implements Clock { - public function __construct(private DateTimeImmutable $currentTime) {} + public function __construct(private DateTimeImmutable $currentTime) + { + } public function now(): DateTimeImmutable { diff --git a/backend/tests/Fakes/FakeTokenGenerator.php b/backend/tests/Fakes/FakeTokenGenerator.php index e10bbcf..4788ab3 100644 --- a/backend/tests/Fakes/FakeTokenGenerator.php +++ b/backend/tests/Fakes/FakeTokenGenerator.php @@ -12,7 +12,9 @@ class FakeTokenGenerator implements TokenGenerator /** * @param string[] $tokens */ - public function __construct(private array $tokens) {} + public function __construct(private array $tokens) + { + } public function generate(): string { diff --git a/backend/tests/Unit/Auth/Middleware/AuthMiddlewareTest.php b/backend/tests/Unit/Auth/Middleware/AuthMiddlewareTest.php index 45aaf3d..33689a5 100644 --- a/backend/tests/Unit/Auth/Middleware/AuthMiddlewareTest.php +++ b/backend/tests/Unit/Auth/Middleware/AuthMiddlewareTest.php @@ -31,7 +31,7 @@ class AuthMiddlewareTest extends TestCase '2026-04-29T12:00:00', new DateTimeZone('UTC') ); - $this->sessionRepo = new FakeSessionRepository; + $this->sessionRepo = new FakeSessionRepository(); $this->clock = new FakeClock($this->now); $this->middleware = new AuthMiddleware( $this->sessionRepo, @@ -58,7 +58,7 @@ class AuthMiddlewareTest extends TestCase }; } - public function test_missing_cookie_returns_unauthorized_json(): void + public function testMissingCookieReturnsUnauthorizedJson(): void { $captured = null; $response = $this->middleware->handle( @@ -74,7 +74,7 @@ class AuthMiddlewareTest extends TestCase $this->assertNull($captured); } - public function test_unknown_token_returns_unauthorized(): void + public function testUnknownTokenReturnsUnauthorized(): void { $captured = null; $response = $this->middleware->handle( @@ -86,7 +86,7 @@ class AuthMiddlewareTest extends TestCase $this->assertNull($captured); } - public function test_expired_session_returns_unauthorized_and_is_deleted(): void + public function testExpiredSessionReturnsUnauthorizedAndIsDeleted(): void { $user = new User( id: 7, @@ -113,7 +113,7 @@ class AuthMiddlewareTest extends TestCase ); } - public function test_valid_session_attaches_user_and_calls_next(): void + public function testValidSessionAttachesUserAndCallsNext(): void { $user = new User( id: 7, diff --git a/backend/tests/Unit/Auth/UseCases/AuthenticateUserTest.php b/backend/tests/Unit/Auth/UseCases/AuthenticateUserTest.php index b39dbff..df43f2c 100644 --- a/backend/tests/Unit/Auth/UseCases/AuthenticateUserTest.php +++ b/backend/tests/Unit/Auth/UseCases/AuthenticateUserTest.php @@ -25,7 +25,10 @@ class AuthenticateUserTest extends TestCase $this->userRepo = new FakeUserRepository(); $this->hasher = new FakeHasher(); - $this->authenticateUser = new AuthenticateUser($this->userRepo, $this->hasher); + $this->authenticateUser = new AuthenticateUser( + $this->userRepo, + $this->hasher + ); } public function testAuthenticatesValidUser(): void diff --git a/backend/tests/Unit/Auth/UseCases/CreateSessionTest.php b/backend/tests/Unit/Auth/UseCases/CreateSessionTest.php index 034d7c5..d9fba39 100644 --- a/backend/tests/Unit/Auth/UseCases/CreateSessionTest.php +++ b/backend/tests/Unit/Auth/UseCases/CreateSessionTest.php @@ -44,8 +44,14 @@ class CreateSessionTest extends TestCase $this->assertSame('fake-token-123', $session->getToken()); $this->assertSame($user, $session->getUser()); $this->assertFalse($session->isExpired($this->clock->now())); - $this->assertSame('2026-05-18 12:00:00', $session->getCreatedAt()->format('Y-m-d H:i:s')); - $this->assertSame('2026-05-25 12:00:00', $session->getExpiresAt()->format('Y-m-d H:i:s')); + $this->assertSame( + '2026-05-18 12:00:00', + $session->getCreatedAt()->format('Y-m-d H:i:s') + ); + $this->assertSame( + '2026-05-25 12:00:00', + $session->getExpiresAt()->format('Y-m-d H:i:s') + ); $stored = $this->sessionRepo->findByToken($session->getToken()); $this->assertNotNull($stored); diff --git a/backend/tests/Unit/Auth/UseCases/LogoutTest.php b/backend/tests/Unit/Auth/UseCases/LogoutTest.php index 5dda63f..84f6c28 100644 --- a/backend/tests/Unit/Auth/UseCases/LogoutTest.php +++ b/backend/tests/Unit/Auth/UseCases/LogoutTest.php @@ -25,11 +25,11 @@ class LogoutTest extends TestCase '2026-04-29T12:00:00', new DateTimeZone('UTC') ); - $this->sessionRepo = new FakeSessionRepository; + $this->sessionRepo = new FakeSessionRepository(); $this->useCase = new Logout($this->sessionRepo); } - public function test_existing_token_session_is_removed(): void + public function testExistingTokenSessionIsRemoved(): void { $this->sessionRepo->create(new CreateSessionDto( token: 'token-abc', @@ -47,7 +47,7 @@ class LogoutTest extends TestCase $this->assertNull($this->sessionRepo->findByToken('token-abc')); } - public function test_unknown_token_does_not_throw(): void + public function testUnknownTokenDoesNotThrow(): void { $this->useCase->execute('unknown-token'); diff --git a/backend/tests/Unit/Controllers/AuthControllerTest.php b/backend/tests/Unit/Controllers/AuthControllerTest.php index 1855c59..29ce7de 100644 --- a/backend/tests/Unit/Controllers/AuthControllerTest.php +++ b/backend/tests/Unit/Controllers/AuthControllerTest.php @@ -73,7 +73,7 @@ class AuthControllerTest extends TestCase ); } - public function test_login_returns_200_and_sets_cookie_on_success(): void + public function testLoginReturns200AndSetsCookieOnSuccess(): void { $email = 'user@example.com'; $password = 'password'; @@ -104,21 +104,21 @@ class AuthControllerTest extends TestCase ); } - public function test_login_returns_400_when_email_missing(): void + public function testLoginReturns400WhenEmailMissing(): void { $request = new Request(['password' => 'correctpassword']); $response = $this->controller->login($request); $this->assertEquals(400, $response->getStatusCode()); } - public function test_login_returns_400_when_password_missing(): void + public function testLoginReturns400WhenPasswordMissing(): void { $request = new Request(['email' => 'user@example.com']); $response = $this->controller->login($request); $this->assertEquals(400, $response->getStatusCode()); } - public function test_login_returns_401_when_credentials_invalid(): void + public function testLoginReturns401WhenCredentialsInvalid(): void { $this->seedStartupUser('user@example.com', 'correctpassword'); @@ -130,7 +130,7 @@ class AuthControllerTest extends TestCase $this->assertEquals(401, $response->getStatusCode()); } - public function test_logout_returns_204_and_clears_cookie(): void + public function testLogoutReturns204AndClearsCookie(): void { $this->seedStartupUser('user@example.com', 'correctpassword'); $loginRequest = new Request([ @@ -139,7 +139,7 @@ class AuthControllerTest extends TestCase ]); $this->controller->login($loginRequest); - $logoutRequest = new Request; + $logoutRequest = new Request(); $logoutRequest->cookies->set( AuthMiddleware::COOKIE_NAME, 'session-token-1' @@ -160,7 +160,7 @@ class AuthControllerTest extends TestCase $this->assertSame('', $cookies[0]->getValue()); } - public function test_me_returns_200_with_user_when_authenticated(): void + public function testMeReturns200WithUserWhenAuthenticated(): void { $email = 'me@example.com'; $user = $this->userRepo->create( @@ -170,7 +170,7 @@ class AuthControllerTest extends TestCase ) ); - $request = new Request; + $request = new Request(); $request->attributes->set('user', $user); $response = $this->controller->me($request); From c7b55b64ca90b3cb966d00f703acfb2ad6f4a740 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 25 May 2026 20:39:14 +0300 Subject: [PATCH 08/17] test media card layout --- frontend/rabbi_gerzi/cypress/e2e/media.cy.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts index 11ed22c..a194099 100644 --- a/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts +++ b/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts @@ -4,7 +4,12 @@ describe('media page sets', () => { cy.get('[data-cy="media-set-card"]', { timeout: 10000 }) .should('have.length', 1) - cy.contains('[data-cy="media-set-card"]', 'Baderech HaAvodah') - .should('be.visible') + .first() + .within(() => { + cy.get('[data-cy="media-set-icon"]').should('be.visible') + cy.contains('h2', 'Baderech HaAvodah').should('be.visible') + cy.contains('a structured path for inner and outer growth') + .should('be.visible') + }) }) }) From 1ce7688d333bf168034de48977cb3ac9f11d76b5 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 25 May 2026 20:42:59 +0300 Subject: [PATCH 09/17] test set media fields --- backend/tests/Feature/SetsEndpointTest.php | 8 ++++++++ backend/tests/Unit/Set/SetTest.php | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/backend/tests/Feature/SetsEndpointTest.php b/backend/tests/Feature/SetsEndpointTest.php index a53d5da..3cdb2cd 100644 --- a/backend/tests/Feature/SetsEndpointTest.php +++ b/backend/tests/Feature/SetsEndpointTest.php @@ -16,9 +16,13 @@ class SetsEndpointTest extends TestCase $setRepository = app(SetRepository::class); $baderechSet = $setRepository->create(new CreateSetDto( name: 'Baderech HaAvodah', + description: 'Baderech HaAvodah is a way of living', + iconImageUrl: '/assets/baderech-haavodah-icon.svg', )); $dailyLearningSet = $setRepository->create(new CreateSetDto( name: 'Daily Learning', + description: 'Daily learning for steady growth', + iconImageUrl: '/assets/daily-learning-icon.svg', )); $response = $this->getJson('/api/sets'); @@ -29,10 +33,14 @@ class SetsEndpointTest extends TestCase [ 'id' => $baderechSet->getId(), 'name' => $baderechSet->getName(), + 'description' => $baderechSet->getDescription(), + 'iconImageUrl' => $baderechSet->getIconImageUrl(), ], [ 'id' => $dailyLearningSet->getId(), 'name' => $dailyLearningSet->getName(), + 'description' => $dailyLearningSet->getDescription(), + 'iconImageUrl' => $dailyLearningSet->getIconImageUrl(), ], ], ]); diff --git a/backend/tests/Unit/Set/SetTest.php b/backend/tests/Unit/Set/SetTest.php index a98ac5f..b3159c2 100644 --- a/backend/tests/Unit/Set/SetTest.php +++ b/backend/tests/Unit/Set/SetTest.php @@ -9,9 +9,22 @@ class SetTest extends TestCase { public function testCreatesSetWithName(): void { - $set = new DomainSet(1, 'Daily learning'); + $set = new DomainSet( + id: 1, + name: 'Daily learning', + description: 'A structured path for daily growth', + iconImageUrl: '/assets/daily-learning-icon.svg', + ); $this->assertSame(1, $set->getId()); $this->assertSame('Daily learning', $set->getName()); + $this->assertSame( + 'A structured path for daily growth', + $set->getDescription() + ); + $this->assertSame( + '/assets/daily-learning-icon.svg', + $set->getIconImageUrl() + ); } } From fa8efd765fa143808c5d4e3d761086d57d3a0382 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 25 May 2026 20:46:00 +0300 Subject: [PATCH 10/17] add set media fields --- backend/app/Controllers/SetController.php | 9 ++++- backend/app/Set/CreateSetDto.php | 2 ++ backend/app/Set/EloquentSetRepository.php | 4 +++ backend/app/Set/Set.php | 12 +++++++ backend/app/Set/SetModel.php | 4 ++- .../2026_05_24_000000_sets_table.php | 2 ++ backend/database/seeders/SetSeeder.php | 4 +++ backend/tests/Fakes/FakeSetRepository.php | 4 +++ backend/tests/Unit/Element/ElementTest.php | 7 +++- .../Element/UseCases/CreateElementTest.php | 34 +++++++++---------- 10 files changed, 61 insertions(+), 21 deletions(-) diff --git a/backend/app/Controllers/SetController.php b/backend/app/Controllers/SetController.php index c7f34e0..1a035c9 100644 --- a/backend/app/Controllers/SetController.php +++ b/backend/app/Controllers/SetController.php @@ -25,13 +25,20 @@ class SetController } /** - * @return array{id: int, name: string} + * @return array{ + * id: int, + * name: string, + * description: string, + * iconImageUrl: string + * } */ private function buildSetPayload(DomainSet $set): array { return [ 'id' => $set->getId(), 'name' => $set->getName(), + 'description' => $set->getDescription(), + 'iconImageUrl' => $set->getIconImageUrl(), ]; } } diff --git a/backend/app/Set/CreateSetDto.php b/backend/app/Set/CreateSetDto.php index 841b0a4..934a410 100644 --- a/backend/app/Set/CreateSetDto.php +++ b/backend/app/Set/CreateSetDto.php @@ -6,6 +6,8 @@ class CreateSetDto { public function __construct( public string $name, + public string $description, + public string $iconImageUrl, ) { } } diff --git a/backend/app/Set/EloquentSetRepository.php b/backend/app/Set/EloquentSetRepository.php index ef8976e..d7db606 100644 --- a/backend/app/Set/EloquentSetRepository.php +++ b/backend/app/Set/EloquentSetRepository.php @@ -8,6 +8,8 @@ class EloquentSetRepository implements SetRepository { $model = SetModel::create([ 'name' => $dto->name, + 'description' => $dto->description, + 'icon_image_url' => $dto->iconImageUrl, ]); return $this->toDomain($model); @@ -36,6 +38,8 @@ class EloquentSetRepository implements SetRepository return new Set( id: $model->id, name: $model->name, + description: $model->description, + iconImageUrl: $model->icon_image_url, ); } } diff --git a/backend/app/Set/Set.php b/backend/app/Set/Set.php index bb6f51a..bf44461 100644 --- a/backend/app/Set/Set.php +++ b/backend/app/Set/Set.php @@ -7,6 +7,8 @@ class Set public function __construct( private int $id, private string $name, + private string $description, + private string $iconImageUrl, ) { } @@ -19,4 +21,14 @@ class Set { return $this->name; } + + public function getDescription(): string + { + return $this->description; + } + + public function getIconImageUrl(): string + { + return $this->iconImageUrl; + } } diff --git a/backend/app/Set/SetModel.php b/backend/app/Set/SetModel.php index c30e2c9..d80a6df 100644 --- a/backend/app/Set/SetModel.php +++ b/backend/app/Set/SetModel.php @@ -8,6 +8,8 @@ use Illuminate\Database\Eloquent\Model; /** * @property int $id * @property string $name + * @property string $description + * @property string $icon_image_url * * @method static Builder|SetModel newModelQuery() * @method static Builder|SetModel newQuery() @@ -23,5 +25,5 @@ class SetModel extends Model public $timestamps = false; - protected $fillable = ['name']; + protected $fillable = ['name', 'description', 'icon_image_url']; } diff --git a/backend/database/migrations/2026_05_24_000000_sets_table.php b/backend/database/migrations/2026_05_24_000000_sets_table.php index a39348b..f3ab093 100644 --- a/backend/database/migrations/2026_05_24_000000_sets_table.php +++ b/backend/database/migrations/2026_05_24_000000_sets_table.php @@ -11,6 +11,8 @@ return new class extends Migration Schema::create('sets', function (Blueprint $table) { $table->id(); $table->string('name'); + $table->text('description'); + $table->string('icon_image_url'); }); } diff --git a/backend/database/seeders/SetSeeder.php b/backend/database/seeders/SetSeeder.php index 2f2975c..5c70d6d 100644 --- a/backend/database/seeders/SetSeeder.php +++ b/backend/database/seeders/SetSeeder.php @@ -15,6 +15,10 @@ class SetSeeder extends Seeder $set = $setRepository->create(new CreateSetDto( name: $title, + description: 'Baderech HaAvodah is a way of living - ' + . 'a structured path for inner and outer growth, ' + . 'spiritual refinement, and personal development.', + iconImageUrl: '/assets/baderech-haavodah-icon.svg', )); } } diff --git a/backend/tests/Fakes/FakeSetRepository.php b/backend/tests/Fakes/FakeSetRepository.php index 295e619..079ea93 100644 --- a/backend/tests/Fakes/FakeSetRepository.php +++ b/backend/tests/Fakes/FakeSetRepository.php @@ -19,6 +19,8 @@ class FakeSetRepository implements SetRepository $set = new DomainSet( id: $id, name: $dto->name, + description: $dto->description, + iconImageUrl: $dto->iconImageUrl, ); $this->setsById[$id] = $set; @@ -52,6 +54,8 @@ class FakeSetRepository implements SetRepository return new DomainSet( id: $set->getId(), name: $set->getName(), + description: $set->getDescription(), + iconImageUrl: $set->getIconImageUrl(), ); } } diff --git a/backend/tests/Unit/Element/ElementTest.php b/backend/tests/Unit/Element/ElementTest.php index 3e914d9..5d7ded2 100644 --- a/backend/tests/Unit/Element/ElementTest.php +++ b/backend/tests/Unit/Element/ElementTest.php @@ -10,7 +10,12 @@ class ElementTest extends TestCase { public function testCreatesElementWithNullableParent(): void { - $set = new DomainSet(1, 'Daily learning'); + $set = new DomainSet( + id: 1, + name: 'Daily learning', + description: 'Daily learning description', + iconImageUrl: '/assets/daily-learning-icon.svg', + ); $rootElement = new Element( id: 1, title: 'Root', diff --git a/backend/tests/Unit/Element/UseCases/CreateElementTest.php b/backend/tests/Unit/Element/UseCases/CreateElementTest.php index 2d01fc6..340f49e 100644 --- a/backend/tests/Unit/Element/UseCases/CreateElementTest.php +++ b/backend/tests/Unit/Element/UseCases/CreateElementTest.php @@ -7,6 +7,7 @@ use App\Element\UseCases\CreateElement\CreateElement; use App\Element\UseCases\CreateElement\CreateElementRequest; use App\Exceptions\BadRequestException; use App\Set\CreateSetDto; +use App\Set\Set as DomainSet; use DomainException; use Tests\Fakes\FakeElementRepository; use Tests\Fakes\FakeSetRepository; @@ -30,11 +31,18 @@ class CreateElementTest extends TestCase ); } + private function createSet(string $name): DomainSet + { + return $this->setRepo->create(new CreateSetDto( + name: $name, + description: "$name description", + iconImageUrl: '/assets/test-set-icon.svg', + )); + } + public function testCreatesRootElement(): void { - $set = $this->setRepo->create( - new CreateSetDto('Daily learning') - ); + $set = $this->createSet('Daily learning'); $element = $this->createElement->execute(new CreateElementRequest( setId: $set->getId(), @@ -50,9 +58,7 @@ class CreateElementTest extends TestCase public function testCreatesChildElement(): void { - $set = $this->setRepo->create( - new CreateSetDto('Daily learning') - ); + $set = $this->createSet('Daily learning'); $rootElement = $this->createElement->execute( new CreateElementRequest( setId: $set->getId(), @@ -114,9 +120,7 @@ class CreateElementTest extends TestCase public function testThrowsWhenParentElementDoesNotExist(): void { - $set = $this->setRepo->create( - new CreateSetDto('Daily learning') - ); + $set = $this->createSet('Daily learning'); $this->expectException(DomainException::class); $this->expectExceptionMessage( @@ -132,9 +136,7 @@ class CreateElementTest extends TestCase public function testThrowsWhenRootElementAlreadyExists(): void { - $set = $this->setRepo->create( - new CreateSetDto('Daily learning') - ); + $set = $this->createSet('Daily learning'); $this->createElement->execute(new CreateElementRequest( setId: $set->getId(), title: 'Root', @@ -155,12 +157,8 @@ class CreateElementTest extends TestCase public function testThrowsWhenParentBelongsToAnotherSet(): void { - $parentSet = $this->setRepo->create( - new CreateSetDto('Parent set') - ); - $childSet = $this->setRepo->create( - new CreateSetDto('Child set') - ); + $parentSet = $this->createSet('Parent set'); + $childSet = $this->createSet('Child set'); $parentElement = $this->createElement->execute( new CreateElementRequest( setId: $parentSet->getId(), From 0aeeed6f2e2b70bc3c1b793f2b07d092d8cd6375 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 25 May 2026 20:46:54 +0300 Subject: [PATCH 11/17] add set media migration --- .../2026_05_24_000000_sets_table.php | 2 -- ..._000000_add_media_fields_to_sets_table.php | 33 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 backend/database/migrations/2026_05_25_000000_add_media_fields_to_sets_table.php diff --git a/backend/database/migrations/2026_05_24_000000_sets_table.php b/backend/database/migrations/2026_05_24_000000_sets_table.php index f3ab093..a39348b 100644 --- a/backend/database/migrations/2026_05_24_000000_sets_table.php +++ b/backend/database/migrations/2026_05_24_000000_sets_table.php @@ -11,8 +11,6 @@ return new class extends Migration Schema::create('sets', function (Blueprint $table) { $table->id(); $table->string('name'); - $table->text('description'); - $table->string('icon_image_url'); }); } diff --git a/backend/database/migrations/2026_05_25_000000_add_media_fields_to_sets_table.php b/backend/database/migrations/2026_05_25_000000_add_media_fields_to_sets_table.php new file mode 100644 index 0000000..5c08bac --- /dev/null +++ b/backend/database/migrations/2026_05_25_000000_add_media_fields_to_sets_table.php @@ -0,0 +1,33 @@ +text('description')->default(''); + $table->string('icon_image_url')->default(''); + }); + + DB::table('sets') + ->where('name', 'Baderech HaAvodah') + ->update([ + 'description' => 'Baderech HaAvodah is a way of living - ' + . 'a structured path for inner and outer growth, ' + . 'spiritual refinement, and personal development.', + 'icon_image_url' => '/assets/baderech-haavodah-icon.svg', + ]); + } + + public function down(): void + { + Schema::table('sets', function (Blueprint $table) { + $table->dropColumn(['description', 'icon_image_url']); + }); + } +}; From 28f0e87386f11d1d0f8eff2ee2e2f136199db155 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 25 May 2026 20:47:46 +0300 Subject: [PATCH 12/17] test media set icon --- frontend/rabbi_gerzi/cypress/e2e/media.cy.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts index a194099..d520b11 100644 --- a/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts +++ b/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts @@ -6,7 +6,10 @@ describe('media page sets', () => { .should('have.length', 1) .first() .within(() => { - cy.get('[data-cy="media-set-icon"]').should('be.visible') + cy.get('img[data-cy="media-set-icon"]') + .should('be.visible') + .and('have.attr', 'src') + .and('include', '/assets/baderech-haavodah-icon.svg') cy.contains('h2', 'Baderech HaAvodah').should('be.visible') cy.contains('a structured path for inner and outer growth') .should('be.visible') From a4d7c4fafd1996b8722c4e78ef0b52602bae34c9 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Mon, 25 May 2026 21:01:33 +0300 Subject: [PATCH 13/17] style media set cards --- .../public/assets/baderech-haavodah-icon.svg | 18 ++ frontend/rabbi_gerzi/src/stores/mediaSets.ts | 2 + frontend/rabbi_gerzi/src/views/MediaPage.vue | 160 ++++++++---------- 3 files changed, 95 insertions(+), 85 deletions(-) create mode 100644 frontend/rabbi_gerzi/public/assets/baderech-haavodah-icon.svg diff --git a/frontend/rabbi_gerzi/public/assets/baderech-haavodah-icon.svg b/frontend/rabbi_gerzi/public/assets/baderech-haavodah-icon.svg new file mode 100644 index 0000000..9a03f05 --- /dev/null +++ b/frontend/rabbi_gerzi/public/assets/baderech-haavodah-icon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/frontend/rabbi_gerzi/src/stores/mediaSets.ts b/frontend/rabbi_gerzi/src/stores/mediaSets.ts index c524fcb..0ed97dd 100644 --- a/frontend/rabbi_gerzi/src/stores/mediaSets.ts +++ b/frontend/rabbi_gerzi/src/stores/mediaSets.ts @@ -4,6 +4,8 @@ import { defineStore } from 'pinia' export interface MediaSet { id: number name: string + description: string + iconImageUrl: string } interface SetsResponse { diff --git a/frontend/rabbi_gerzi/src/views/MediaPage.vue b/frontend/rabbi_gerzi/src/views/MediaPage.vue index 74ea6e9..4e3be4d 100644 --- a/frontend/rabbi_gerzi/src/views/MediaPage.vue +++ b/frontend/rabbi_gerzi/src/views/MediaPage.vue @@ -1,7 +1,6 @@