feat(admin): add NSFW flag to packs, remove intensity

Re-add is_explicit toggle on pack and dare pack forms labeled as
"NSFW". Show NSFW badge on pack cards and user enabled packs.

Remove all intensity/comfort level references from admin UI including
form fields, analytics charts, review dialog columns, AI generation
prompts/scoring, cherry-pick balancing, and settings toggles. The
database columns remain unchanged.
This commit is contained in:
jjbrunton committed 2026-01-27 22:50:46 +00:00
1 parent 14b0199625
commit e380c0a31c
32 files changed
+948 -1075

No files matched your search

+9 -21
View File
@@ -37,7 +37,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Crown, Users, MessageCircle, ChevronRight, ThumbsUp, ThumbsDown, Minus, Gift, Image, Video as VideoIcon, Target, User, Eye, EyeOff, CheckCircle, Package, Heart, Sparkles, Flame, Trophy, Calendar } from 'lucide-react';
import { Crown, Users, MessageCircle, ChevronRight, ThumbsUp, ThumbsDown, Minus, Gift, Image, Video as VideoIcon, Target, User, Eye, EyeOff, CheckCircle, Package, Sparkles, Flame, Trophy, Calendar } from 'lucide-react';
import { format } from 'date-fns';
import { IconPreview } from '@/components/ui/icon-picker';
@@ -46,8 +46,6 @@ type Gender = 'male' | 'female' | 'non-binary' | 'prefer-not-to-say';
// ... (interfaces and constants are the same)
type IntensityLevel = 1 | 2 | 3 | 4 | 5;
interface Profile {
id: string;
name: string | null;
@@ -58,9 +56,8 @@ interface Profile {
created_at: string | null;
gender: Gender | null;
usage_reason: UsageReason | null;
show_explicit_content: boolean | null;
hide_nsfw: boolean | null;
onboarding_completed: boolean | null;
max_intensity: IntensityLevel | null;
}
const usageReasonLabels: Record<UsageReason, { label: string; icon: string }> = {
@@ -78,14 +75,6 @@ const genderLabels: Record<Gender, string> = {
'prefer-not-to-say': 'Prefer not to say',
};
const intensityLabels: Record<IntensityLevel, { label: string; emoji: string }> = {
1: { label: 'Gentle', emoji: '💭' },
2: { label: 'Warm', emoji: '💕' },
3: { label: 'Playful', emoji: '😏' },
4: { label: 'Steamy', emoji: '🔥' },
5: { label: 'Intense', emoji: '🌶️' },
};
interface Partner {
id: string;
name: string | null;
@@ -556,11 +545,10 @@ export function UserDetailPage() {
</div>
</div></CardContent></Card>
{/* Onboarding Details */}
<Card><CardHeader className="pb-3"><CardTitle className="text-lg flex items-center gap-2"><CheckCircle className="h-5 w-5 text-primary" />Onboarding Details</CardTitle><CardDescription>Information collected during user onboarding</CardDescription></CardHeader><CardContent><div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5">
<Card><CardHeader className="pb-3"><CardTitle className="text-lg flex items-center gap-2"><CheckCircle className="h-5 w-5 text-primary" />Onboarding Details</CardTitle><CardDescription>Information collected during user onboarding</CardDescription></CardHeader><CardContent><div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<div className="flex items-start gap-3"><div className="p-2 rounded-lg bg-blue-100 dark:bg-blue-900"><User className="h-4 w-4 text-blue-600 dark:text-blue-400" /></div><div><p className="text-sm font-medium text-muted-foreground">Gender</p><p className="font-medium">{profile.gender ? genderLabels[profile.gender] : 'Not set'}</p></div></div>
<div className="flex items-start gap-3"><div className="p-2 rounded-lg bg-purple-100 dark:bg-purple-900"><Target className="h-4 w-4 text-purple-600 dark:text-purple-400" /></div><div><p className="text-sm font-medium text-muted-foreground">Why using Sauci</p><p className="font-medium">{profile.usage_reason ? usageReasonLabels[profile.usage_reason]?.label : 'Not set'}</p></div></div>
<div className="flex items-start gap-3"><div className="p-2 rounded-lg bg-pink-100 dark:bg-pink-900"><Heart className="h-4 w-4 text-pink-600 dark:text-pink-400" /></div><div><p className="text-sm font-medium text-muted-foreground">Comfort Zone</p><p className="font-medium">{profile.max_intensity ? `${intensityLabels[profile.max_intensity].emoji} ${intensityLabels[profile.max_intensity].label} (${profile.max_intensity})` : 'Not set'}</p></div></div>
<div className="flex items-start gap-3"><div className={`p-2 rounded-lg ${profile.show_explicit_content ? 'bg-amber-100 dark:bg-amber-900' : 'bg-gray-100 dark:bg-gray-800'}`}>{profile.show_explicit_content ? <Eye className="h-4 w-4 text-amber-600 dark:text-amber-400" /> : <EyeOff className="h-4 w-4 text-gray-500 dark:text-gray-400" />}</div><div><p className="text-sm font-medium text-muted-foreground">Explicit Content</p><p className="font-medium">{profile.show_explicit_content === null ? 'Not set' : profile.show_explicit_content ? 'Enabled' : 'Disabled'}</p></div></div>
<div className="flex items-start gap-3"><div className={`p-2 rounded-lg ${profile.hide_nsfw ? 'bg-gray-100 dark:bg-gray-800' : 'bg-amber-100 dark:bg-amber-900'}`}>{profile.hide_nsfw ? <EyeOff className="h-4 w-4 text-gray-500 dark:text-gray-400" /> : <Eye className="h-4 w-4 text-amber-600 dark:text-amber-400" />}</div><div><p className="text-sm font-medium text-muted-foreground">Hide NSFW</p><p className="font-medium">{profile.hide_nsfw === null ? 'Not set' : profile.hide_nsfw ? 'Yes' : 'No'}</p></div></div>
<div className="flex items-start gap-3"><div className={`p-2 rounded-lg ${profile.onboarding_completed ? 'bg-green-100 dark:bg-green-900' : 'bg-red-100 dark:bg-red-900'}`}><CheckCircle className={`h-4 w-4 ${profile.onboarding_completed ? 'text-green-600 dark:text-green-400' : 'text-red-500 dark:text-red-400'}`} /></div><div><p className="text-sm font-medium text-muted-foreground">Onboarding</p><p className="font-medium">{profile.onboarding_completed ? 'Completed' : 'Not completed'}</p></div></div>
</div></CardContent></Card>
{/* Streak Info */}
@@ -674,17 +662,17 @@ export function UserDetailPage() {
<CardTitle className="text-base">{ep.pack.name}</CardTitle>
</div>
<div className="flex gap-1">
{ep.pack.is_explicit && (
<Badge variant="destructive" className="text-xs">
NSFW
</Badge>
)}
{ep.pack.is_premium && (
<Badge className="bg-amber-500 text-xs">
<Crown className="h-3 w-3 mr-1" />
Premium
</Badge>
)}
{ep.pack.is_explicit && (
<Badge variant="destructive" className="text-xs">
18+
</Badge>
)}
</div>
</div>
{ep.pack.description && (
+136 -101
View File
@@ -226,13 +226,13 @@ export function UsersPage() {
<TableHeader>
<TableRow>
<TableHead>User</TableHead>
<TableHead>Email</TableHead>
<TableHead>Status</TableHead>
<TableHead>Couple</TableHead>
<TableHead>Storage</TableHead>
<TableHead>Joined</TableHead>
<TableHead>Last Active</TableHead>
<TableHead className="w-12"></TableHead>
<TableHead className="hidden md:table-cell">Email</TableHead>
<TableHead className="hidden md:table-cell">Status</TableHead>
<TableHead className="hidden lg:table-cell">Couple</TableHead>
<TableHead className="hidden lg:table-cell">Storage</TableHead>
<TableHead className="hidden xl:table-cell">Joined</TableHead>
<TableHead className="hidden xl:table-cell">Last Active</TableHead>
<TableHead className="hidden md:table-cell w-12"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -243,102 +243,137 @@ export function UsersPage() {
</TableCell>
</TableRow>
) : (
paginatedProfiles.map((profile) => (
<TableRow key={profile.id}>
<TableCell>
<Link to={`/users/${profile.id}`} className="flex items-center gap-3 hover:opacity-80 transition-opacity">
<Avatar className="h-9 w-9">
<AvatarImage src={profile.avatar_url || undefined} />
<AvatarFallback>
{profile.name?.charAt(0).toUpperCase() || 'U'}
</AvatarFallback>
</Avatar>
<span className="font-medium hover:text-primary transition-colors">
{profile.name || 'Unnamed User'}
</span>
</Link>
</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>
)}
</>
) : (
<Badge variant="secondary" className="bg-slate-100 text-slate-500 hover:bg-slate-200">
Guest
</Badge>
)}
</div>
</TableCell>
<TableCell>
{profile.is_premium ? (
<Badge className="bg-amber-500">
<Crown className="h-3 w-3 mr-1" />
Premium
</Badge>
) : profile.partner_is_premium ? (
<Badge className="bg-amber-500/70">
<Crown className="h-3 w-3 mr-1" />
Partner
</Badge>
) : (
<Badge variant="secondary" className="bg-slate-100 text-slate-500 hover:bg-slate-200">
Free
</Badge>
paginatedProfiles.map((profile) => {
const statusBadge = profile.is_premium ? (
<Badge className="bg-amber-500">
<Crown className="h-3 w-3 mr-1" />
Premium
</Badge>
) : profile.partner_is_premium ? (
<Badge className="bg-amber-500/70">
<Crown className="h-3 w-3 mr-1" />
Partner
</Badge>
) : (
<Badge variant="secondary" className="bg-slate-100 text-slate-500 hover:bg-slate-200">
Free
</Badge>
);
const coupleBadge = profile.couple_id ? (
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-xs">Not paired</span>
);
const emailContent = profile.email ? (
<span className="flex items-center gap-1.5 truncate max-w-[220px]">
<span className="truncate">{profile.email}</span>
{!profile.email_confirmed_at && (
<MailWarning className="h-3.5 w-3.5 text-amber-500" aria-label="Email not verified" />
)}
</TableCell>
<TableCell>
{profile.couple_id ? (
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>
)}
</TableCell>
<TableCell>
{profile.storage_bytes && profile.storage_bytes > 0 ? (
<div className="flex items-center gap-1 text-sm">
<HardDrive className="h-3 w-3 text-muted-foreground" />
{formatBytes(profile.storage_bytes)}
</span>
) : (
<Badge variant="secondary" className="bg-slate-100 text-slate-500 hover:bg-slate-200">
Guest
</Badge>
);
const storageLabel = profile.storage_bytes && profile.storage_bytes > 0
? formatBytes(profile.storage_bytes)
: '—';
const joinedLabel = profile.created_at
? format(new Date(profile.created_at), 'MMM d, yyyy')
: '—';
const lastActiveLabel = profile.last_sign_in_at
? format(new Date(profile.last_sign_in_at), 'MMM d, yyyy h:mm a')
: 'Never';
return (
<TableRow key={profile.id}>
<TableCell>
<div className="flex items-center gap-3">
<Link to={`/users/${profile.id}`} className="flex items-center gap-3 min-w-0 hover:opacity-80 transition-opacity">
<Avatar className="h-9 w-9">
<AvatarImage src={profile.avatar_url || undefined} />
<AvatarFallback>
{profile.name?.charAt(0).toUpperCase() || 'U'}
</AvatarFallback>
</Avatar>
<span className="font-medium truncate hover:text-primary transition-colors">
{profile.name || 'Unnamed User'}
</span>
</Link>
<div className="ml-auto md:hidden">
<Link to={`/users/${profile.id}`}>
<Button variant="ghost" size="icon">
<ChevronRight className="h-4 w-4" />
</Button>
</Link>
</div>
</div>
) : (
<span className="text-muted-foreground text-sm">—</span>
)}
</TableCell>
<TableCell className="text-muted-foreground text-sm">
{profile.created_at
? format(new Date(profile.created_at), 'MMM d, yyyy')
: '—'}
</TableCell>
<TableCell className="text-muted-foreground text-sm">
{profile.last_sign_in_at
? format(new Date(profile.last_sign_in_at), 'MMM d, yyyy h:mm a')
: 'Never'}
</TableCell>
<TableCell>
<Link to={`/users/${profile.id}`}>
<Button variant="ghost" size="icon">
<ChevronRight className="h-4 w-4" />
</Button>
</Link>
</TableCell>
</TableRow>
))
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground md:hidden">
{emailContent}
{statusBadge}
{coupleBadge}
{storageLabel !== '—' && (
<span className="flex items-center gap-1">
<HardDrive className="h-3 w-3 text-muted-foreground" />
{storageLabel}
</span>
)}
<span className="text-muted-foreground">Active {lastActiveLabel}</span>
</div>
</TableCell>
<TableCell className="hidden md:table-cell text-muted-foreground">
<div className="flex items-center gap-1.5">
{emailContent}
</div>
</TableCell>
<TableCell className="hidden md:table-cell">
{statusBadge}
</TableCell>
<TableCell className="hidden lg:table-cell">
{profile.couple_id ? coupleBadge : <span className="text-muted-foreground text-sm">—</span>}
</TableCell>
<TableCell className="hidden lg:table-cell">
{profile.storage_bytes && profile.storage_bytes > 0 ? (
<div className="flex items-center gap-1 text-sm">
<HardDrive className="h-3 w-3 text-muted-foreground" />
{storageLabel}
</div>
) : (
<span className="text-muted-foreground text-sm">—</span>
)}
</TableCell>
<TableCell className="hidden xl:table-cell text-muted-foreground text-sm">
{joinedLabel}
</TableCell>
<TableCell className="hidden xl:table-cell text-muted-foreground text-sm">
{lastActiveLabel}
</TableCell>
<TableCell className="hidden md:table-cell">
<Link to={`/users/${profile.id}`}>
<Button variant="ghost" size="icon">
<ChevronRight className="h-4 w-4" />
</Button>
</Link>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>