mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Add guest auth and save-account flow
Introduce anonymous sign-in with in-app saving, guest recovery warnings, paywall prompts, and admin guest labeling.
This commit is contained in:
1 parent
ce68d57a49
commit
349f460e15
14 files changed
+1236
-176
No files matched your search
@@ -539,10 +539,19 @@ export function UserDetailPage() {
|
||||
{profile.is_premium && <Badge className="bg-amber-500"><Crown className="h-3 w-3 mr-1" />Premium</Badge>}
|
||||
{!profile.is_premium && hasPermission(PERMISSION_KEYS.GIFT_PREMIUM) && <Dialog open={upgradeOpen} onOpenChange={setUpgradeOpen}><DialogTrigger asChild><Button size="sm" variant="outline" className="ml-2 gap-1 h-7"><Gift className="h-3.5 w-3.5" />Gift Premium</Button></DialogTrigger><DialogContent><DialogHeader><DialogTitle>Gift Premium Access</DialogTitle><DialogDescription>Manually upgrade this user to premium status.</DialogDescription></DialogHeader><div className="grid gap-4 py-4"><div className="grid gap-2"><Label htmlFor="expiry-type">Duration</Label><Select value={expiryType} onValueChange={setExpiryType}><SelectTrigger id="expiry-type"><SelectValue placeholder="Select duration" /></SelectTrigger><SelectContent><SelectItem value="forever">Lifetime (No Expiry)</SelectItem><SelectItem value="1_month">1 Month</SelectItem><SelectItem value="1_year">1 Year</SelectItem><SelectItem value="custom">Custom Date</SelectItem></SelectContent></Select></div>{expiryType === 'custom' && (<div className="grid gap-2"><Label htmlFor="custom-date">Expiry Date</Label><Input id="custom-date" type="datetime-local" value={customDate} onChange={(e) => setCustomDate(e.target.value)} /></div>)}</div><DialogFooter><Button variant="outline" onClick={() => setUpgradeOpen(false)}>Cancel</Button><Button onClick={handleUpgrade} disabled={upgrading}>{upgrading ? 'Upgrading...' : 'Confirm Upgrade'}</Button></DialogFooter></DialogContent></Dialog>}
|
||||
</div>
|
||||
<p className="text-muted-foreground mb-4">{profile.email || 'No email'}</p>
|
||||
<div className="text-muted-foreground mb-4 flex items-center gap-2">
|
||||
{profile.email ? (
|
||||
<span>{profile.email}</span>
|
||||
) : (
|
||||
<Badge variant="secondary" className="bg-slate-100 text-slate-500 hover:bg-slate-200">Guest</Badge>
|
||||
)}
|
||||
{!profile.email && (
|
||||
<span className="text-xs">Not recoverable unless saved in-app</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-6 text-sm">
|
||||
<div><span className="text-muted-foreground">Joined: </span><span>{profile.created_at ? format(new Date(profile.created_at), 'MMMM d, yyyy') : '—'}</span></div>
|
||||
{partner ? <div className="flex items-center gap-1"><Users className="h-4 w-4 text-muted-foreground" /><span className="text-muted-foreground">Paired with: </span><Link to={`/users/${partner.id}`} className="text-primary hover:underline">{partner.name || 'Unnamed Partner'}</Link></div> : <div className="text-muted-foreground">Not paired</div>}
|
||||
{partner ? <div className="flex items-center gap-1"><Users className="h-4 w-4 text-muted-foreground" /><span className="text-muted-foreground">Paired with: </span><Link to={`/users/${partner.id}`} className="text-primary hover:underline">{partner.name || 'Unnamed Partner'}</Link></div> : profile.couple_id ? <div className="text-amber-600 dark:text-amber-400 flex items-center gap-1"><Users className="h-4 w-4" />Waiting for partner</div> : <div className="text-muted-foreground">Not paired</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div></CardContent></Card>
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Search, Crown, Users, ChevronRight, HardDrive, MailWarning } from 'lucide-react';
|
||||
import { Search, Crown, Users, ChevronRight, HardDrive, MailWarning, Hourglass } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
interface Profile {
|
||||
@@ -32,6 +32,7 @@ interface Profile {
|
||||
email_confirmed_at: string | null;
|
||||
storage_bytes?: number;
|
||||
partner_is_premium?: boolean;
|
||||
partner_count?: number;
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
@@ -87,11 +88,13 @@ export function UsersPage() {
|
||||
}
|
||||
});
|
||||
|
||||
// Merge storage data and partner premium status with profiles
|
||||
// Merge storage data, partner premium status, and partner count with profiles
|
||||
const profilesWithExtras = profilesData.map(profile => {
|
||||
// Check if partner has premium
|
||||
// Check if partner has premium and count partners in couple
|
||||
let partnerIsPremium = false;
|
||||
let partnerCount = 0;
|
||||
if (profile.couple_id && couplePartners[profile.couple_id]) {
|
||||
partnerCount = couplePartners[profile.couple_id].length;
|
||||
const partner = couplePartners[profile.couple_id].find(p => p.id !== profile.id);
|
||||
partnerIsPremium = partner?.is_premium || false;
|
||||
}
|
||||
@@ -99,6 +102,7 @@ export function UsersPage() {
|
||||
...profile,
|
||||
storage_bytes: storageMap[profile.id] || 0,
|
||||
partner_is_premium: partnerIsPremium,
|
||||
partner_count: partnerCount,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -126,9 +130,12 @@ export function UsersPage() {
|
||||
setProfiles(prev => prev.filter(p => p.id !== deleted.id));
|
||||
setFilteredProfiles(prev => prev.filter(p => p.id !== deleted.id));
|
||||
}, []),
|
||||
insertToast: {
|
||||
insertToast: {
|
||||
enabled: true,
|
||||
message: (payload) => `New user registered: ${payload.name || payload.email || 'Unknown'}`,
|
||||
message: (payload) => {
|
||||
const label = payload.name || payload.email || 'Guest';
|
||||
return payload.email ? `New user registered: ${label}` : `New guest user: ${label}`;
|
||||
},
|
||||
type: 'success',
|
||||
},
|
||||
debounceMs: 1000, // Debounce to avoid excessive refetches
|
||||
@@ -191,7 +198,15 @@ export function UsersPage() {
|
||||
<RealtimeStatusIndicator status={realtimeStatus} showLabel />
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{profiles.length} registered user{profiles.length !== 1 ? 's' : ''}
|
||||
{profiles.length} account{profiles.length !== 1 ? 's' : ''}
|
||||
{(() => {
|
||||
const guestCount = profiles.filter(p => !p.email).length;
|
||||
const savedCount = profiles.length - guestCount;
|
||||
if (guestCount === 0) return null;
|
||||
return (
|
||||
<span> • {savedCount} saved • {guestCount} guest</span>
|
||||
);
|
||||
})()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative w-full sm:w-64">
|
||||
@@ -245,11 +260,19 @@ export function UsersPage() {
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{profile.email || '—'}
|
||||
{profile.email && !profile.email_confirmed_at && (
|
||||
<span title="Email not verified">
|
||||
<MailWarning className="h-3.5 w-3.5 text-amber-500" />
|
||||
</span>
|
||||
{profile.email ? (
|
||||
<>
|
||||
{profile.email}
|
||||
{!profile.email_confirmed_at && (
|
||||
<span title="Email not verified">
|
||||
<MailWarning className="h-3.5 w-3.5 text-amber-500" />
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Badge variant="secondary" className="bg-slate-100 text-slate-500 hover:bg-slate-200">
|
||||
Guest
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -272,10 +295,17 @@ export function UsersPage() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{profile.couple_id ? (
|
||||
<Badge variant="secondary">
|
||||
<Users className="h-3 w-3 mr-1" />
|
||||
Paired
|
||||
</Badge>
|
||||
profile.partner_count && profile.partner_count >= 2 ? (
|
||||
<Badge variant="secondary" className="bg-green-100 text-green-700 hover:bg-green-200">
|
||||
<Users className="h-3 w-3 mr-1" />
|
||||
Paired
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary" className="bg-amber-100 text-amber-700 hover:bg-amber-200">
|
||||
<Hourglass className="h-3 w-3 mr-1" />
|
||||
Pairing
|
||||
</Badge>
|
||||
)
|
||||
) : (
|
||||
<span className="text-muted-foreground text-sm">—</span>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user