refactor(web): RegistrationPanel sibling parity — Edit/Reinstate redesign + Redemptions header
This commit is contained in:
@@ -294,7 +294,6 @@ interface ExpirySelectorProps {
|
|||||||
function ExpirySelector({ value, customDateTime, onChange, showKeep, disabled }: ExpirySelectorProps) {
|
function ExpirySelector({ value, customDateTime, onChange, showKeep, disabled }: ExpirySelectorProps) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-txt-secondary mb-1.5">Expires</label>
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
{showKeep && (
|
{showKeep && (
|
||||||
<button
|
<button
|
||||||
@@ -702,18 +701,32 @@ function EditInviteModal({ invite, onClose, onUpdated }: EditInviteModalProps) {
|
|||||||
className="relative w-full max-w-md mx-4 glass-modal rounded-lg animate-slide-up overflow-hidden"
|
className="relative w-full max-w-md mx-4 glass-modal rounded-lg animate-slide-up overflow-hidden"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="px-5 pt-5 pb-4 border-b border-white/[0.06] flex items-start gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-accent-lavender/15 flex items-center justify-center flex-shrink-0">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-accent-lavender">
|
||||||
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||||
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h3 className="text-[16px] font-bold text-txt-primary">Edit "{invite.name}"</h3>
|
||||||
|
<p className="text-[13px] text-txt-secondary leading-snug mt-0.5">
|
||||||
|
Adjust the limits on this invite link. The URL stays the same — anyone who already has it can still redeem under the new constraints.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSave();
|
handleSave();
|
||||||
}}
|
}}
|
||||||
className="p-6 space-y-5"
|
className="px-5 pt-4 pb-5 space-y-5"
|
||||||
>
|
>
|
||||||
<h3 className="text-lg font-semibold text-txt-primary">Edit "{invite.name}"</h3>
|
|
||||||
|
|
||||||
{/* Name */}
|
{/* Name */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-txt-secondary mb-1.5">Name</label>
|
<div className="text-[11px] font-semibold text-txt-tertiary uppercase tracking-wider mb-2">Name</div>
|
||||||
<input
|
<input
|
||||||
ref={nameInputRef}
|
ref={nameInputRef}
|
||||||
type="text"
|
type="text"
|
||||||
@@ -727,10 +740,9 @@ function EditInviteModal({ invite, onClose, onUpdated }: EditInviteModalProps) {
|
|||||||
|
|
||||||
{/* Max uses */}
|
{/* Max uses */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-txt-secondary mb-1.5">
|
<div className="text-[11px] font-semibold text-txt-tertiary uppercase tracking-wider mb-2">
|
||||||
Max uses{' '}
|
Max uses <span className="normal-case font-normal text-txt-tertiary">({invite.usedCount} used)</span>
|
||||||
<span className="text-txt-tertiary">({invite.usedCount} used)</span>
|
</div>
|
||||||
</label>
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||||
<input
|
<input
|
||||||
@@ -770,6 +782,8 @@ function EditInviteModal({ invite, onClose, onUpdated }: EditInviteModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Expiry */}
|
{/* Expiry */}
|
||||||
|
<div>
|
||||||
|
<div className="text-[11px] font-semibold text-txt-tertiary uppercase tracking-wider mb-2">Expires</div>
|
||||||
<ExpirySelector
|
<ExpirySelector
|
||||||
value={expiryId}
|
value={expiryId}
|
||||||
customDateTime={customDateTime}
|
customDateTime={customDateTime}
|
||||||
@@ -780,21 +794,22 @@ function EditInviteModal({ invite, onClose, onUpdated }: EditInviteModalProps) {
|
|||||||
showKeep={true}
|
showKeep={true}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex justify-end gap-2 pt-1">
|
<div className="flex gap-2 mt-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="px-3 py-1.5 text-sm text-txt-secondary hover:text-txt-primary transition-colors disabled:opacity-50"
|
className="py-2.5 px-4 rounded-lg text-[13px] font-medium text-txt-tertiary border border-white/[0.06] hover:bg-white/[0.06] transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="px-4 py-1.5 bg-accent-primary hover:bg-accent-primary/80 text-white text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
|
className="flex-1 py-2.5 rounded-lg text-[13px] font-semibold text-white bg-accent-primary hover:bg-accent-primary/80 transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{submitting ? 'Saving…' : 'Save changes'}
|
{submitting ? 'Saving…' : 'Save changes'}
|
||||||
</button>
|
</button>
|
||||||
@@ -907,6 +922,10 @@ function ReinstateInviteModal({ invite, onClose, onReinstated }: ReinstateInvite
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const subtitle = isRevoked
|
||||||
|
? 'This invite was revoked. Reinstating generates a new link with a different URL — the old URL stays inactive.'
|
||||||
|
: 'This invite has lapsed. Reinstating reactivates the same URL — anyone who saved it will be able to use it again.';
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-[210] flex items-center justify-center animate-fade-in"
|
className="fixed inset-0 z-[210] flex items-center justify-center animate-fade-in"
|
||||||
@@ -918,36 +937,39 @@ function ReinstateInviteModal({ invite, onClose, onReinstated }: ReinstateInvite
|
|||||||
className="relative w-full max-w-md mx-4 glass-modal rounded-lg animate-slide-up overflow-hidden"
|
className="relative w-full max-w-md mx-4 glass-modal rounded-lg animate-slide-up overflow-hidden"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="px-5 pt-5 pb-4 border-b border-white/[0.06] flex items-start gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-accent-lavender/15 flex items-center justify-center flex-shrink-0">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-accent-lavender">
|
||||||
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||||
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h3 className="text-[16px] font-bold text-txt-primary">Reinstate "{invite.name}"</h3>
|
||||||
|
<p className="text-[13px] text-txt-secondary leading-snug mt-0.5">{subtitle}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleReinstate();
|
handleReinstate();
|
||||||
}}
|
}}
|
||||||
className="p-6 space-y-5"
|
className="px-5 pt-4 pb-5 space-y-5"
|
||||||
>
|
>
|
||||||
<h3 className="text-lg font-semibold text-txt-primary">Reinstate "{invite.name}"</h3>
|
{/* Amber callout — only for revoked variant to reinforce the "new URL" consequence */}
|
||||||
|
{isRevoked && (
|
||||||
{isRevoked ? (
|
<div className="p-3 rounded-lg bg-accent-amber/10 border border-accent-amber/20 text-[13px] text-accent-amber">
|
||||||
<p className="text-sm text-txt-secondary leading-relaxed">
|
A new link will be generated. Anyone who had the old URL will not be able to use it.
|
||||||
<strong className="text-txt-primary">This will generate a new link.</strong>{' '}
|
</div>
|
||||||
The previously revoked URL stays inactive — anyone who had the old link will
|
|
||||||
not be able to use it.
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-txt-secondary leading-relaxed">
|
|
||||||
<strong className="text-txt-primary">The same link will start working again.</strong>{' '}
|
|
||||||
Anyone who saved the URL will be able to use it.
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Max uses */}
|
{/* Max uses */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-txt-secondary mb-1.5">
|
<div className="text-[11px] font-semibold text-txt-tertiary uppercase tracking-wider mb-2">
|
||||||
Max uses{' '}
|
Max uses <span className="normal-case font-normal text-txt-tertiary">(current: {invite.maxUses ?? '∞'}, used: {invite.usedCount})</span>
|
||||||
<span className="text-txt-tertiary">
|
</div>
|
||||||
(current: {invite.maxUses ?? '∞'}, used: {invite.usedCount})
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||||
<input
|
<input
|
||||||
@@ -987,6 +1009,8 @@ function ReinstateInviteModal({ invite, onClose, onReinstated }: ReinstateInvite
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Expiry */}
|
{/* Expiry */}
|
||||||
|
<div>
|
||||||
|
<div className="text-[11px] font-semibold text-txt-tertiary uppercase tracking-wider mb-2">Expires</div>
|
||||||
<ExpirySelector
|
<ExpirySelector
|
||||||
value={expiryId}
|
value={expiryId}
|
||||||
customDateTime={customDateTime}
|
customDateTime={customDateTime}
|
||||||
@@ -998,21 +1022,22 @@ function ReinstateInviteModal({ invite, onClose, onReinstated }: ReinstateInvite
|
|||||||
showKeep={false}
|
showKeep={false}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex justify-end gap-2 pt-1">
|
<div className="flex gap-2 mt-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="px-3 py-1.5 text-sm text-txt-secondary hover:text-txt-primary transition-colors disabled:opacity-50"
|
className="py-2.5 px-4 rounded-lg text-[13px] font-medium text-txt-tertiary border border-white/[0.06] hover:bg-white/[0.06] transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="px-4 py-1.5 bg-accent-primary hover:bg-accent-primary/80 text-white text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
|
className="flex-1 py-2.5 rounded-lg text-[13px] font-semibold text-white bg-accent-primary hover:bg-accent-primary/80 transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{submitting
|
{submitting
|
||||||
? 'Reinstating…'
|
? 'Reinstating…'
|
||||||
@@ -1090,21 +1115,30 @@ function RedemptionsModal({ invite, onClose }: RedemptionsModalProps) {
|
|||||||
className="relative w-full max-w-lg mx-4 glass-modal rounded-lg animate-slide-up overflow-hidden flex flex-col max-h-[80vh]"
|
className="relative w-full max-w-lg mx-4 glass-modal rounded-lg animate-slide-up overflow-hidden flex flex-col max-h-[80vh]"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div className="p-6 pb-3 flex items-start justify-between gap-3">
|
{/* Header */}
|
||||||
<h3 className="text-lg font-semibold text-txt-primary truncate">
|
<div className="px-5 pt-5 pb-4 border-b border-white/[0.06] flex items-start gap-3 flex-shrink-0">
|
||||||
Redemptions for "{invite.name}"
|
<div className="w-10 h-10 rounded-lg bg-accent-sky/15 flex items-center justify-center flex-shrink-0">
|
||||||
</h3>
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" className="text-accent-sky">
|
||||||
|
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h3 className="text-[16px] font-bold text-txt-primary">Redemptions for "{invite.name}"</h3>
|
||||||
|
<p className="text-[13px] text-txt-secondary leading-snug mt-0.5">
|
||||||
|
Users who registered using this invite link, in the order they signed up.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
className="text-txt-tertiary hover:text-txt-primary text-xl leading-none px-1 -mt-1"
|
className="w-8 h-8 rounded-md text-txt-tertiary hover:text-txt-primary hover:bg-white/[0.06] flex items-center justify-center flex-shrink-0 transition-colors"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-6 pb-3 space-y-3">
|
<div className="px-5 pt-4 pb-3 space-y-3">
|
||||||
{invite.status === 'revoked' && (
|
{invite.status === 'revoked' && (
|
||||||
<div className="bg-accent-rose/10 border border-accent-rose/30 rounded p-2.5 text-xs text-accent-rose leading-relaxed">
|
<div className="bg-accent-rose/10 border border-accent-rose/30 rounded p-2.5 text-xs text-accent-rose leading-relaxed">
|
||||||
This invite was revoked
|
This invite was revoked
|
||||||
|
|||||||
Reference in New Issue
Block a user