125 lines
5.7 KiB
Markdown
125 lines
5.7 KiB
Markdown
# Users
|
|
|
|
- **Route:** `/users`
|
|
- **API:** GET /v1/user/ · POST /v1/user/invite
|
|
- **Visual reference:** `source/RenewCMS Screens Back-office.dc.html`, group "Users" (open it in a browser; each state is drawn at desktop and mobile, with a light/dark toggle).
|
|
|
|
One shared workspace: every user sees the same articles and this same list. Rows have no actions yet, so nobody can be removed, promoted or have an invite revoked from here. Pending and expired invites show the email address, because a username only exists once the invite is accepted.
|
|
|
|
An invite is **Pending** until its link is used, **Expired** after 3 days, and **Accepted** once the account is created. The list is sorted by created date, newest first.
|
|
|
|
## The list
|
|
|
|
| Column | Content |
|
|
|---|---|
|
|
| Username | An accepted user: the display name if there is one (medium weight, highlighted) with the username beside it in muted text; otherwise the username alone. A pending or expired invite: the email address, regular weight, toned. The current user's row carries a muted mono "you" tag. |
|
|
| Invite status | A badge: Accepted (circle-check, success), Pending (mail, warning), Expired (clock-alert, outline, muted). Shown by icon and word as well as color. |
|
|
| Created | The date as `27 Sep 2026`, or "Today" for the current day. For an invite it is the day the invite was sent, for a user the day the account was created. |
|
|
|
|
Desktop is a table: 36 px header, 44 px rows, columns `minmax(0,1fr) 160px 160px`. Below md it becomes a card list (68 px minimum row height): the name line, then the badge and "Created {date}". The page header is a title "Users" with the subtitle "Everyone who can sign in and work on your articles." and a primary "Invite user" button (user-plus icon) on its right. On mobile the button becomes a full-width 48 px button pinned to the bottom of the screen.
|
|
|
|
The admin who registered first is always listed, so the list is never empty.
|
|
|
|
## States
|
|
|
|
### Default
|
|
|
|
**Trigger:** The list loaded, newest first.
|
|
|
|
- Breadcrumbs: Users (mobile title "Users")
|
|
- Rows: `sam.oliveira@renew.example` Pending, 27 Sep 2026 · `p.huber@gmail.com` Expired, 3 Aug 2026 · `ines` (display name "Inès Duarte") Accepted, 18 Jun 2026 · `jonas` Accepted, 2 Apr 2026 · `marta` (you) Accepted, 12 Mar 2026
|
|
|
|
### Loading
|
|
|
|
**Trigger:** The first load, or a retry. Skeletons appear after 150 ms.
|
|
|
|
- Breadcrumbs: Users (mobile title "Users")
|
|
- Five skeleton rows
|
|
|
|
### Only you
|
|
|
|
**Trigger:** Nobody has been invited yet. The admin who registered is always listed.
|
|
|
|
- Breadcrumbs: Users (mobile title "Users")
|
|
- One row: `marta` (you) Accepted
|
|
- Hint under the list: "Invite someone so they can sign in with their own account instead of sharing yours."
|
|
|
|
### Error
|
|
|
|
**Trigger:** A 500 or network failure on the list request.
|
|
|
|
- Breadcrumbs: Users (mobile title "Users")
|
|
- Alert (error): **Couldn't load users** "Check your connection and try again."
|
|
- Action "Try again" (rotate-cw)
|
|
- No table or cards
|
|
|
|
### Invite: default
|
|
|
|
**Trigger:** "Invite user" in the page header, or the bottom button on mobile.
|
|
|
|
- Breadcrumbs: Users (mobile title "Users")
|
|
- Modal: **Invite a user**
|
|
- text "We'll email them a link to choose a username and password. The link expires after 3 days."
|
|
- field `Email`: placeholder "name@example.com"; focused
|
|
- close button
|
|
- cancel "Cancel"
|
|
- confirm "Send invite"
|
|
|
|
### Invite: validation
|
|
|
|
**Trigger:** On submit or blur. Nothing is sent.
|
|
|
|
- Breadcrumbs: Users (mobile title "Users")
|
|
- Modal: **Invite a user** (same text)
|
|
- field `Email`: value "sam.oliveira@renew"; **error "Enter a full email address, like name@example.com."**
|
|
- cancel "Cancel"
|
|
- confirm "Send invite"
|
|
|
|
### Invite: sending
|
|
|
|
**Trigger:** Request in flight. The field is disabled and the dialog can't be dismissed.
|
|
|
|
- Breadcrumbs: Users (mobile title "Users")
|
|
- Modal: **Invite a user** (same text)
|
|
- field `Email`: value "lea.martin@renew.example"; disabled
|
|
- cancel "Cancel"
|
|
- confirm "Sending…", loading, not dismissible
|
|
|
|
### Invite: already a user (409)
|
|
|
|
**Trigger:** The email belongs to an existing user. For a live pending invite the message reads "There's already a pending invite for this address." An expired invite can be sent again.
|
|
|
|
- Breadcrumbs: Users (mobile title "Users")
|
|
- Modal: **Invite a user** (same text)
|
|
- field `Email`: value "jonas@renew.example"; **error "This address already has an account."**
|
|
- cancel "Cancel"
|
|
- confirm "Send invite"
|
|
|
|
### Invite: failed
|
|
|
|
**Trigger:** 500, network failure, or the email couldn't be sent. The address stays in the field.
|
|
|
|
- Breadcrumbs: Users (mobile title "Users")
|
|
- Modal: **Invite a user** (same text)
|
|
- field `Email`: value "lea.martin@renew.example"
|
|
- Alert: "Couldn't send the invite. Try again in a moment."
|
|
- cancel "Cancel"
|
|
- confirm "Send invite"
|
|
|
|
### Invite sent
|
|
|
|
**Trigger:** 201: the dialog closes, the new invite appears at the top as Pending (highlighted row), and a toast confirms.
|
|
|
|
- Breadcrumbs: Users (mobile title "Users")
|
|
- New first row: `lea.martin@renew.example` Pending, created "Today", row background `--ui-bg-elevated`
|
|
- Toast (success, circle-check): **Invite sent** "lea.martin@renew.example has 3 days to accept."
|
|
|
|
The "Today" label makes this state clock-sensitive, so `task visual:check` skips it.
|
|
|
|
## Notes for implementation
|
|
|
|
- An accepted user is shown by username and display name only, never by email.
|
|
- How display names look in this list is the state "Display names in the Users list" of [profile.md](profile.md). The board draws it in the Profile group, and `task visual:check` finds states by group.
|
|
- 429 on the invite request is not drawn. Decide it in the spec before coding.
|
|
- The email the invite sends is specified in [emails.md](../emails.md). The screen the link opens is not designed yet: see [accept-invite.md](accept-invite.md).
|