webx-ui / module-team
The team for the WebX UI admin panel: people with a photo, a name, a job title, a short text and their social links, shown on any page as a block — a grid, a slider or a list — and in templates through team().
Requires
- php: ^8.3
- illuminate/contracts: ^13.0
- illuminate/database: ^13.0
- illuminate/http: ^13.0
- illuminate/routing: ^13.0
- illuminate/support: ^13.0
- webx-ui/localization: ^0.49.2
- webx-ui/mcp: ^0.49.2
- webx-ui/module-admin: ^0.49.2
- webx-ui/module-blocks: ^0.49.2
- webx-ui/module-media: ^0.49.2
- webx-ui/routing: ^0.49.2
Requires (Dev)
- orchestra/testbench: ^11.0
- phpunit/phpunit: ^12.0 || ^13.0
- webx-ui/module-pages: ^0.49.2
- webx-ui/module-services: ^0.49.2
Suggests
- webx-ui/module-pages: A page with a team block is the site's page of the team.
- webx-ui/module-services: A person can be linked to the services they provide, and a service's page can show who does it.
Provides
None
Conflicts
None
Replaces
None
README
The team as a section of the WebX UI admin panel: people
with a photo, a name, a job title, a short text and their social links — and, when the site has
services, the services each of them provides. Shown on any page as a block (a grid, a slider or a
list) and in the site's own templates through team().
The module has no public route and no page of its own. A person reaches the site in a block: "our doctors" as a grid on the About page, "who does this" as a list on a service's page. The page brings the address, the SEO and the menu entry; the module brings the people.
Requirements
- PHP 8.3+, Laravel 13
webx-ui/module-admin,webx-ui/module-blocks,webx-ui/module-media,webx-ui/localization,webx-ui/routingwebx-ui/module-pagesfor a page to put the block on — suggested, not requiredwebx-ui/module-servicesto link people to services — suggested, not required
Install
composer require webx-ui/module-team php artisan migrate php artisan webx:blocks:offered --install --module=team
The last line puts the offered block type Team on the site and publishes it. A type the site
already has under the slug team is left alone: the site may have rewritten it. webx:setup
runs this line by itself for a new site.
Permissions: team.view, team.manage. The section is one entry of the panel's menu, Team,
at the top level.
A person
| Field | Stored as |
|---|---|
photo |
the value of a wx-media field — a library key, never an address |
name |
translatable; required in the default language — the one required field |
job_title |
translatable |
text |
translatable plain text; printed with its line breaks |
socials |
[{ network, url }] in the editor's order; http:// or https:// only |
services |
a wx-relations field on service; there only when module-services is |
published |
the whole of a person's life: no draft, no history; the bin brings them back |
position |
the one order there is — the team has no categories, so there is no second order |
Languages. Nobody is hidden over a language. A name or a job title that is not written in the language is taken from the default one; a text that is not written in it is simply not printed — the person is shown without it. Hiding a doctor from the Russian page over one untranslated paragraph would be losing them.
Anything else — experience, education, certificates, the days they see patients — is the
site's own field, patched onto the screen team.form into the card project-fields. It is
stored in extra and reaches the card under fields.
Social networks
The networks a link may point at are the config webx-team.networks, key → name:
// config/webx-team.php return [ 'networks' => [ 'facebook' => 'Facebook', 'instagram' => 'Instagram', 'linkedin' => 'LinkedIn', 'x' => 'X', 'telegram' => 'Telegram', 'youtube' => 'YouTube', 'tiktok' => 'TikTok', 'vk' => 'VK', // a site's own ], ];
The select of the form takes its options from here (the provider lays them over the screen with a patch), and the server checks a link against the same options — so a network the site adds is one line of config. Names are brands and are not translated. A network taken out of the list drops out of every card on the site and stays in the database: put it back, and the links come back.
A row of the form with neither a network nor an address is dropped on save; a row with only one of
the two, an address that is not http(s):// or a network that is not on the list is refused under
that row's field. A link the person already has to a network taken off the list is kept as it is:
the form sends it back the way it opened it, and a save is not refused over it.
Two roads into a template
The block. The offered type has a wx-collection field on the team source. The editor of
the page chooses a limit and, when the site has services, "only related to" — some services, or
the service of this page. The template gets the people already read:
@foreach ($team['items'] as $member) <article id="{{ $member['anchor'] }}"> <h3>{{ $member['name'] }}</h3> <p>{{ $member['job_title'] }}</p> </article> @endforeach
team(). A template of the site, or a block that wants something the field does not do, asks
for them itself:
@foreach (team()->relatedTo('service', $service)->take(3) as $member) … @endforeach
| Step | What it does |
|---|---|
relatedTo($t, $ids) |
only those related to these records ('service', an id or a model) |
only([12, 7]) |
these and no others, in this order |
except($member) |
all but these |
take(6) |
at most six; null or zero — all |
locale('uk') |
the language of the cards; by default the one the page is drawn in |
get(), first() |
a list of cards, or one; the query itself can be looped and counted |
relatedTo() with an empty list is nobody, not everybody. There is no in() and no
categories(): the team has no categories.
Both roads hand over the same card:
[
'id' => 12,
'anchor' => 'member-12',
'categories' => [], // the contract of a collection asks for the key
'name' => 'Anna Petrova',
'initials' => 'AP', // what stands in for a missing photo
'job_title' => 'Orthodontist', // '' when there is none
'text' => "…\n…", // '' when it is not written in the language
'photo' => ['url' => …, 'thumb' => …, 'width' => …, 'height' => …, 'alt' => …], // or null
'socials' => [['network' => 'instagram', 'label' => 'Instagram', 'url' => 'https://…']],
'service_links' => [['id' => 3, 'title' => 'Braces', 'url' => '/braces']], // [] without services
'fields' => ['experience' => '12'], // the project's own fields, by name
]
A list of any length is the same few queries. team() is declared only if the site has no
function of that name; php artisan webx:doctor says whose it is.
The Team block
The type is a document in resources/blocks/team.json, the same format as webx:blocks:export:
a heading, the team collection, a layout, columns for a grid and a slider, autoplay for a
slider, and hide_text — off, the untouched state, shows the text.
- Grid — up to
columnsacross (4 when not set), fewer when the block is narrow. Slider — the same card in a ribbon that snaps, with arrows and, if asked, autoplay. List — the photo on the left, the name, the job title, the text and the services beside it: the layout for biographies, and for "who does this" on a service's page. - A card is an
<article id="member-12">: the photo or the initials in a circle, the name, the job title, the text, links to the services and the social links. - Social links are icons for the seven networks of the default config — inline SVG from
Simple Icons (CC0), right in the template. A network the site adds is
printed as its name until the site's copy of the type gets an icon. Every link opens in a new tab
with
rel="noopener"and says whose it is to a screen reader. - Without JavaScript everything reads: the slider is a ribbon that scrolls sideways. The
script adds the arrows and autoplay;
prefers-reduced-motionturns autoplay off. - The styles are neutral —
currentColorandem— so the block stands in any site's design.
No markup. Person needs a page of the person to hang on, and there is none.
A target for other modules
A person is a relation target under the key team-member: a module that wants "the reviews of
this doctor" adds a wx-relations field with "target": "team-member" to its screen, and the
picker offers the team with their job titles and photos. The module itself points at nobody on the
team.
The panel's API
Under the panel's API path, behind team.view to read and team.manage to write:
GET team ?trashed=1&search= → { data: [row] }
POST team { values } → 201 { data: { member, values } }
GET team/{id} → { data: { member, values } }
PUT team/{id} { values } → 422 under the name of the field
DELETE team/{id}
POST team/{id}/restore → { data: row }
POST team/reorder { ids }
A row is { id, name, job_title, initials, photo: { thumb } | null, published, position, updated_at, deleted_at }; member is { id, name, published, deleted_at }.
MCP
With webx-ui/mcp serving the panel to an agent, the section is six tools behind team:read and
team:write — the same list, form and order code as the panel:
| Tool | What it does |
|---|---|
team_list |
Everybody in the order of the site, or words — or the bin |
team_get |
One person in full: every language, the links, the services, the extra |
team_create |
A person at the end of the team; unpublished unless asked |
team_update |
The values — on the site at once, the team has no draft |
team_delete |
To the bin |
team_reorder |
The one order — the people named first, the rest where they were |
A person is named by their id only — names repeat. A plain string in name, job_title or text
is the default language. photo is a library key; a key the library does not have is refused.
socials is the whole list of { network, url }; a network the site does not have is refused
with the list of those it has, and a link the person already has to a network taken off the list
goes back as it came. services takes ids or addresses and is offered only when
webx-ui/module-services is installed. Every change takes dry_run: true, and team_create is
one transaction: a refusal leaves nobody behind.
The resource team://catalog is what an agent reads first: the networks the site accepts, and
everybody in order — unpublished people included and marked — with the languages their text is
written in and the services they are linked to.
Demo
php artisan webx:demo seeds six people from resources/demo/team.json, in English and Russian as
far as the site has them: one not published, one without a Russian text (shown on the Russian page
without it), three with social links — one of them to a network the config does not have, stored
and left off the card. No photos: the block draws the initials. The offered block type is installed
if the site has not taken it, and then:
- with
webx-ui/module-pages— a page/teamwith everybody in a grid; - with
webx-ui/module-services— the people linked to the demo services, and a list of «who does it» (the service of this page) on the demo servicecompany-website.
The demo needs the library's demo first, and the pages' and services' when they are installed.
webx:demo --remove takes all of it back out. A team that already has anybody in it is left alone.
License
MIT