Tier model: registered=basic (recipes, 12 photos, QR), PRO=verified or admin-activated (HSL, tools/gradient mask, RAW, export > source longest edge); admin users table gets Activated Pro column

This commit is contained in:
2026-09-30 11:50:28 +07:00
parent 35d8d57984
commit 74c0b2747f
10 changed files with 264 additions and 122 deletions
+20 -3
View File
@@ -183,9 +183,10 @@ export function Admin() {
setAlbum(userId);
};
// Block/unblock and remove/restore share one route; the list is reloaded
// because either flag also changes what the built-in viewer would show.
const setUserState = (id: number, patch: { blocked?: boolean; removed?: boolean }) =>
// Block/unblock, remove/restore and the PRO grant share one route; the list
// is reloaded because any of the flags also changes what the built-in viewer
// would show.
const setUserState = (id: number, patch: { blocked?: boolean; removed?: boolean; pro?: boolean }) =>
run(async () => {
await api.adminSetUser(id, patch);
await refreshPhotos();
@@ -604,6 +605,7 @@ export function Admin() {
<th>{t('adm.userName')}</th>
<th>{t('adm.userEmail')}</th>
<th>{t('adm.userRole')}</th>
<th className="adm-pro">{t('adm.userPro')}</th>
<th>{t('adm.userPhotos')}</th>
<th>{t('adm.userJoined')}</th>
<th />
@@ -644,6 +646,21 @@ export function Admin() {
{u.admin ? t('adm.roleAdmin') : t('adm.roleMember')}
</span>
</td>
<td className="adm-pro">
{/* The PRO grant. Ticking it hands this account the
studio's PRO tier on its next request; the API
reads the row, not the session, so nothing has
to be signed in again. An admin account is PRO
by allowlist, so its box only reports that. */}
<input
type="checkbox"
data-key={`adm-user-pro-${u.id}`}
aria-label={t('adm.userPro')}
checked={u.pro}
disabled={busy || u.admin}
onChange={() => void setUserState(u.id, { pro: !u.pro })}
/>
</td>
<td>{u.photos}</td>
<td>{new Date(u.createdAt).toLocaleDateString()}</td>
<td>