This is the recruiter-facing heart of youteacher_web: an employer or recruiter searches teacher profiles, spends quota to unlock a teacher's gated details, and then reaches out by contact or job invitation. The whole feature is built on the app's hexagonal layering — pure use cases, a gateway port, and concrete API adapters wired in by a React hook.
The shape of it
The feature separates what happens from how it talks to the backend:
- Use cases (application layer) each hold one operation and one dependency.
SearchTalentsUseCasetakes filters, a page, a limit, and an optionalAbortSignal, then delegates to the gateway.UnlockTalentUseCase,FetchViewerQuotaUseCase,FetchTalentDetailUseCase,SendContactUseCase, andSendInviteUseCasefollow the same one-method pattern. - The port
TalentSearchGatewayis the contract the use cases depend on. It declaressearch,fetchDetail,sendContact,sendInvite,unlock, andfetchUnlocked. A separateViewerGatewaydeclaresfetchQuota. The concrete implementations live in the infrastructure layer and are the only thing that knows about HTTP. - Dependency injection is a hook, not a framework.
useTalentSearchServicememoizes the concrete API gateways and constructs each use case around them, exposing a stableuseCasesobject. Swapping the adapter never touches a use case.
Searching
useTalentSearch holds the page's state: the results list, pagination, and the search form (free-text query, availability, skill, location, and a relevance/recent sort). buildFilters collects only the fields the user actually set into a filters object.
Search runs only when the viewer is authorized. handleSearch resets to page 1, runs the use case, stores the results and total page count, and — on a wide desktop viewport — auto-selects the first result so the detail pane isn't empty. handleLoadMore fetches the next page and appends. A rate-limit response is caught specifically and surfaced as a friendly toast rather than a raw error.
Unlocking, quota, and reaching out
A teacher's contact details are gated. UnlockTalentUseCase takes a talent id and the viewer type (employer or recruiter); by its contract, the first unlock spends one unit of quota and later calls for the same teacher are idempotent — you are not charged twice. FetchViewerQuotaUseCase reads the viewer's remaining quota, and the gateway's fetchUnlocked returns which teachers are already open, so the UI can show unlocked state without re-spending.
Once unlocked, two outbound actions exist: sendContact carries a message to a teacher, and sendInvite attaches a job (id, and optionally title and a message) to the outreach. The TalentDetailPane renders the selected teacher — avatar, name, headline, availability, bio, skills, preferred locations, years of experience, and verifiable credentials — and hands its unlocked / contacted / invited state to a TalentActions block that drives the unlock, contact, and invite buttons per viewer type.
Selecting a teacher re-fetches fresh detail; a gone profile is dropped from the list, and a forbidden one clears the pane — the feature treats "no longer visible" as a normal state, not a crash.
Inside this node
- part of ddd-hexagonal-architecture — the layering this feature is built on
- the profile-service side of the same gate lives in
youteacher_profile(talent unlock & quota enforcement)