𧬠Profielstructuur
Live voorbeeld β Creator profiel
Dutch creator π₯ | Erotic entertainment | Streaming daily 20:00β23:00 π₯
Anatomie
- βMood color ring β Kleurring rond avatar weerspiegelt huidige mood. Pulseert rood bij LIVE. CSS box-shadow gebaseerd op
user.currentMood. - βRole badge β CREATOR / MEMBER / VIP / STAFF. Kleur en gradient per rol via RBAC enum.
- βVerified checkmark β Blauw βοΈ na identity-verificatie. Opgeslagen in
users.isVerified. - βLive indicator β Rood puls-badge als
user.isLive = true. Realtime via Socket.IO. - βStats row β Followers, Following, Likes, Stream hours. EfficiΓ«nt via gedenormaliseerde counters in
user_statstabel.
π§ͺ Fantasy Profile β Erotic DNA & Kink Preferences
Het Fantasy Profile is het erotische hart van een TeezU-profiel. Alleen zichtbaar voor 18+-verified users met passend consent-level.
Erotic DNA Visualisatie
Scores worden ingevuld via de DNA-quiz (dna_quiz.html) en opgeslagen in fantasy_profiles.dna_scores (JSONB).
Kink Preferences & Intent
π€ AI Features β Teezer & AI DoppelgΓ€nger
Teezer Status (AI Clone)
Status opgeslagen in users.teezerActive (boolean). Toggle via PATCH /api/users/:id/teezer.
AI DoppelgΓ€nger Schedule
Schedule opgeslagen in teezer_schedules tabel als JSONB array van tijdssloten per dag.
π Gift Vault
Publiek ontvangen gifts worden prominent weergegeven op het profiel. Dient als sociaal bewijs en status-signaal.
Gifts worden gefetcht via GET /api/users/:id/gifts (top 10 meest ontvangen, gegroepeerd per type). Zichtbaar voor iedereen.
πΌοΈ Content Grid β Publiek, Premium & Locked
Grid toont maximaal 9 recente items. Locked preview heeft altijd thumbnail (geblurd) zichtbaar. Unlock via Freeze & Reveal flow.
π Creator Stats β RBAC
Earnings-stats, subscriber count en streaming schedule zijn alleen zichtbaar voor de creator zelf en admins. Nooit publiek.
Private Creator Dashboard (eigen profiel)
Public stats (iedereen zichtbaar)
π οΈ API Endpoints
/api/users/:id/profile
Volledig profiel incl. stats, fantasy profile (indien 18+ access), gift vault
// Drizzle ORM β profile query
const profile = await db.query.users.findFirst({
where: eq(users.id, id),
with: {
fantasyProfile: true, // enkel als req.user 18+ verified
giftVault: { limit: 10, orderBy: desc(gifts.count) },
contentGrid: { limit: 9, where: not(eq(content.deletedAt, null)) },
stats: true,
}
});
/api/users/:id/profile
Update bio, mood, kinks, teezer-status, schedule. Eigen profiel only.
Body: { bio?, currentMood?, teezerActive?, fantasyProfile?, streamingSchedule? }
/api/users/:id/follow
Toggle follow/unfollow. Triggert notification:new (new_follower) via Socket.IO.
Response: { following: boolean, followerCount: number }
ποΈ Database Schema β PostgreSQL 16 + Drizzle ORM
// schema/profiles.ts (Drizzle ORM)
export const users = pgTable('users', {
id: uuid('id').defaultRandom().primaryKey(),
username: varchar('username', { length: 32 }).notNull().unique(),
displayName: varchar('display_name', { length: 64 }),
bio: text('bio'),
avatarKey: text('avatar_key'),
coverKey: text('cover_key'),
role: userRoleEnum('role').default('member'),
currentMood: moodEnum('current_mood'),
isVerified: boolean('is_verified').default(false),
isLive: boolean('is_live').default(false),
teezerActive: boolean('teezer_active').default(false),
createdAt: timestamp('created_at').defaultNow(),
});
export const fantasyProfiles = pgTable('fantasy_profiles', {
id: uuid('id').defaultRandom().primaryKey(),
userId: uuid('user_id').notNull().unique().references(() => users.id),
dnaScores: jsonb('dna_scores').default({}),
// { dominant:78, exhibitionist:92, voyeur:45, romantic:61, kinky:84 }
kinks: text('kinks').array(),
intent: text('intent').array(),
consentLevel: varchar('consent_level', { length: 32 }),
});
export const userStats = pgTable('user_stats', {
userId: uuid('user_id').notNull().primaryKey().references(() => users.id),
followerCount: integer('follower_count').default(0),
followingCount: integer('following_count').default(0),
likeCount: integer('like_count').default(0),
streamHours: numeric('stream_hours', { precision:8, scale:1 }).default('0'),
tipTotal: integer('tip_total').default(0), // only visible to creator/admin
subscriberCount: integer('subscriber_count').default(0),
});