import { and, asc, count, desc, eq, gt, inArray, or } from "drizzle-orm"; import Image from "next/image"; import Link from "next/link"; import { redirect } from "next/navigation"; import type { CSSProperties, ReactNode } from "react"; import { claimReferral } from "@/actions/referral"; import { Reveal } from "@/components/motion-reveal"; import { auth } from "@/lib/auth"; import { db, MessengerFriendships, MessengerOffline, Rooms, User, UserReferrals, UsersBadges, UsersSettings, WebsiteLoginLogs, WebsiteSetting, } from "@/lib/db"; import { avatarImageUrl } from "@/lib/format"; import { resolveHotelName } from "@/lib/hotel-name"; import { siteSettings } from "@/lib/services/site-settings"; import CopyReferralButton from "./CopyReferralButton"; // TODO: Cache Components adoption. Refactor this route so this opt-out can be removed. // See: https://nextjs.org/docs/app/guides/migrating-to-cache-components export const instant = false; export const metadata = { title: "My dashboard" }; type SearchParams = Promise<{ claimed?: string; error?: string }>; function feedbackStyle(tone: "success" | "error" | "warning"): CSSProperties { const accent = tone === "error" ? "var(--color-danger)" : tone === "warning" ? "var(--color-accent)" : "var(--color-primary)"; return { margin: 0, padding: "0.85rem 1rem", borderRadius: "var(--radius-md)", border: `1px solid ${accent}`, color: "var(--color-text-readable, var(--color-text))", fontSize: "0.9rem", fontWeight: 600, background: "var(--color-surface)", borderLeft: `4px solid ${accent}`, }; } const ERROR_MESSAGES: Record = { not_enough: "You do not have enough referrals to claim your reward yet.", no_referrals: "You haven't referred anyone yet — share your link to get started.", bad_config: "The referral reward is not configured correctly. Please contact a staff member.", error: "Something went wrong while claiming your reward. Please try again.", }; function getErrorMessage(error: string): string { if (error === "not_enough") return ERROR_MESSAGES.not_enough; if (error === "no_referrals") return ERROR_MESSAGES.no_referrals; if (error === "bad_config") return ERROR_MESSAGES.bad_config; return ERROR_MESSAGES.error; } export default async function MePage({ searchParams, }: { searchParams: SearchParams; }) { const session = await auth(); if (!session?.user?.id) redirect("/login"); const userId = Number(session.user.id); if (!Number.isFinite(userId) || userId <= 0) redirect("/login"); const { claimed, error } = await searchParams; let content: ReactNode; try { const BADGE_IMG_BASE = "https://images.habbo.com/c_images/album1584"; const [ userRows, imagerBase, hotelName, neededRows, amountRows, currencyRows, alertRaw, recentRooms, badges, lastWebLoginRows, userSettingsRows, friendCountRows, unreadCountRows, ] = await Promise.all([ db .select({ id: User.id, username: User.username, look: User.look, motto: User.motto, rank: User.rank, credits: User.credits, accountCreated: User.accountCreated, lastLogin: User.lastLogin, lastOnline: User.lastOnline, }) .from(User) .where(eq(User.id, userId)) .limit(1), siteSettings.get( "habbo_imaging_url", "https://www.habbo.com/habbo-imaging/avatarimage", ), resolveHotelName(), db .select({ value: WebsiteSetting.value }) .from(WebsiteSetting) .where(eq(WebsiteSetting.key, "referrals_needed")) .limit(1) .catch(() => []), db .select({ value: WebsiteSetting.value }) .from(WebsiteSetting) .where(eq(WebsiteSetting.key, "referral_reward_amount")) .limit(1) .catch(() => []), db .select({ value: WebsiteSetting.value }) .from(WebsiteSetting) .where( inArray(WebsiteSetting.key, [ "referral_reward_currency_type", "referral_reward_currency", ]), ) .limit(1) .catch(() => []), siteSettings.get("hotel_alert_banner", ""), db .select({ id: Rooms.id, name: Rooms.name, ownerName: Rooms.ownerName, users: Rooms.users, usersMax: Rooms.usersMax, }) .from(Rooms) .orderBy(desc(Rooms.id)) .limit(6) .catch(() => []), db .select({ badgeCode: UsersBadges.badgeCode, slotId: UsersBadges.slotId, }) .from(UsersBadges) .where(and(eq(UsersBadges.userId, userId), gt(UsersBadges.slotId, 0))) .orderBy(asc(UsersBadges.slotId)) .catch(() => []), db .select({ createdAt: WebsiteLoginLogs.createdAt }) .from(WebsiteLoginLogs) .where(eq(WebsiteLoginLogs.userId, userId)) .orderBy(desc(WebsiteLoginLogs.id)) .limit(1) .catch(() => []), db .select({ achievementScore: UsersSettings.achievementScore, respectsReceived: UsersSettings.respectsReceived, }) .from(UsersSettings) .where(eq(UsersSettings.userId, userId)) .limit(1) .catch(() => []), db .select({ value: count() }) .from(MessengerFriendships) .where( or( eq(MessengerFriendships.userOneId, userId), eq(MessengerFriendships.userTwoId, userId), ), ) .catch(() => [{ value: 0 }]), db .select({ value: count() }) .from(MessengerOffline) .where(eq(MessengerOffline.userId, userId)) .catch(() => [{ value: 0 }]), ]); const user = userRows[0] ?? null; const neededRaw = neededRows[0] ?? null; const amountRaw = amountRows[0] ?? null; const currencyRaw = currencyRows[0] ?? null; const lastWebLogin = lastWebLoginRows[0] ?? null; const userSettings = userSettingsRows[0] ?? null; const friendCount = Number(friendCountRows[0]?.value ?? 0); const unreadCount = Number(unreadCountRows[0]?.value ?? 0); void lastWebLogin; if (!user) throw new Error("user-not-found"); const needed = Number.parseInt(neededRaw?.value ?? "5", 10) || 5; const rewardAmount = Number.parseInt(amountRaw?.value ?? "30", 10) || 0; const rewardCurrency = (currencyRaw?.value ?? "diamonds") .trim() .toLowerCase(); const [referrals] = await db .select({ referralsTotal: UserReferrals.referralsTotal }) .from(UserReferrals) .where(eq(UserReferrals.userId, userId)) .orderBy(desc(UserReferrals.id)) .limit(1) .catch(() => []); const referralTotal = referrals ? Number(referrals.referralsTotal) : 0; const canClaim = referralTotal >= needed; const remaining = Math.max(0, needed - referralTotal); const friendships = await db .select({ userOneId: MessengerFriendships.userOneId, userTwoId: MessengerFriendships.userTwoId, }) .from(MessengerFriendships) .where( or( eq(MessengerFriendships.userOneId, userId), eq(MessengerFriendships.userTwoId, userId), ), ) .catch(() => []); const friendIds = Array.from( new Set( friendships .map((f) => (f.userOneId === userId ? f.userTwoId : f.userOneId)) .filter((id) => id && id !== userId), ), ); const friends = friendIds.length ? await db .select({ id: User.id, username: User.username, look: User.look, motto: User.motto, online: User.online, }) .from(User) .where(inArray(User.id, friendIds)) .catch(() => []) : []; const onlineFriends = friends.filter((f) => f.online === "1"); const registered = new Date(user.accountCreated * 1000) .toISOString() .slice(0, 10); const lastLoginDate = user.lastLogin ? new Date(user.lastLogin * 1000).toISOString().slice(0, 10) : "Never"; const lastOnlineDate = user.lastOnline ? new Date(user.lastOnline * 1000).toISOString().slice(0, 10) : "Never"; const referralLink = `/register?ref=${encodeURIComponent(user.username)}`; const imager = imagerBase ?? "https://www.habbo.com/habbo-imaging/avatarimage"; const avatarFull = avatarImageUrl(imager, user.look, { size: "l", direction: 2, }); content = (
{claimed ? (
Woah! You have successfully claimed your reward — keep up the good work!
) : null} {error ? (
{getErrorMessage(error)}
) : null}
Enter {hotelName}
Rank {user.rank}

{user.username}

{user.motto || "No motto"}

Registered:{" "} {registered} Last online:{" "} {lastOnlineDate}
{alertRaw ? (

{alertRaw}

) : null}
{[ { value: user.credits.toLocaleString(), label: "Credits" }, { value: `Rank ${user.rank}`, label: "Hotel rank" }, { value: (userSettings?.achievementScore ?? 0).toLocaleString(), label: "Achievement score", }, { value: (userSettings?.respectsReceived ?? 0).toLocaleString(), label: "Respects", }, { value: friendCount.toLocaleString(), label: "Friends" }, { value: lastLoginDate, label: "Last login" }, ].map((s) => (
{s.value}
{s.label}
))}

Online friends

{onlineFriends.length === 0 ? "None of your friends are online right now" : `${onlineFriends.length} of your ${friends.length} friends online`}

View all
{onlineFriends.length === 0 ? (

No friends online right now. Check back later!

) : (
{onlineFriends.map((friend) => (

{friend.username}

{friend.motto || "No motto"}

{" "} Online
))}
)}

Refer a friend ({referralTotal}/{needed})

Refer new users and earn in-game rewards

{rewardAmount > 0 ? `For every ${needed} users who register through your referral link you can claim a reward of ${rewardAmount.toLocaleString()} ${rewardCurrency}!` : `Invite ${needed} users through your referral link to claim a reward!`}

Boosting referrals by making your own accounts will lead to removal of all progress, currency, inventory and a potential ban.
{canClaim ? (
) : ( )}
{recentRooms.length > 0 && (

Recent rooms

Newest rooms in the hotel

View all
{recentRooms.map((room) => (

{room.name}

{room.ownerName} · {room.users}/{room.usersMax}

))}
)} {badges.length > 0 && (

Badges

{badges.length} badge{badges.length !== 1 ? "s" : ""} equipped

{badges.map((badge) => ( {badge.badgeCode} ))}
)}

Quick links

{[ { href: "/messages", label: "Messages", badge: unreadCount }, { href: `/u/${user.username}`, label: "My profile", badge: 0 }, { href: "/settings", label: "Settings", badge: 0 }, { href: "/shop", label: "Shop", badge: 0 }, ].map((link) => ( {link.label} {link.badge > 0 && ( {link.badge > 99 ? "99+" : link.badge} )} ))}
); } catch { content = (

We couldn't load your dashboard right now. Please try again shortly.

); } return
{content}
; }