Improvements: remove dead config, fix ESM, add URL validation, unify types, add missing logging
- Remove .prettierrc (dead config, Biome replaces Prettier) - Rename lighthouserc.json to lighthouserc.cjs with module.exports for ESM compat - Add logger.warn to empty catch blocks in auth, register, site-settings, prisma-cache, redis, security, rate-limit - Unify ActionResult type: action-helper.ts uses 'ok' consistent with safe-action-shared.ts - Add noUnusedLocals + noUnusedParameters to tsconfig + fix 25 pre-existing unused vars - Replace barrel export src/types/index.ts with direct @/types/common imports - Make trustHost conditional (development only) in auth.ts - Add pre-flight URL validation to update-Nitrov3.sh to catch image.library.url misconfigurations - Improve NITRO_IMAGE_LIBRARY_URL content validation in pre-flight & post-compute checks
This commit is contained in:
1 parent
b922f6d49f
commit
17847545dd
292 files changed
+69195
-67915
No files matched your search
+438
-153
@@ -3,7 +3,7 @@ import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import type { CSSProperties, ReactNode } from "react";
|
||||
import { claimReferral } from "@/actions/referral";
|
||||
import { ContentCard, EmptyState, OnlineBadge } from "@/components/public/ui";
|
||||
|
||||
import { Reveal } from "@/components/motion-reveal";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { avatarImageUrl } from "@/lib/format";
|
||||
@@ -71,89 +71,105 @@ export default async function MePage({
|
||||
try {
|
||||
const BADGE_IMG_BASE = "https://images.habbo.com/c_images/album1584";
|
||||
|
||||
const [user, imagerBase, hotelName, neededRaw, amountRaw, currencyRaw, alertRaw, recentRooms, badges, lastWebLogin, userSettings, friendCount, unreadCount] =
|
||||
await Promise.all([
|
||||
prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
const [
|
||||
user,
|
||||
imagerBase,
|
||||
hotelName,
|
||||
neededRaw,
|
||||
amountRaw,
|
||||
currencyRaw,
|
||||
alertRaw,
|
||||
recentRooms,
|
||||
badges,
|
||||
lastWebLogin,
|
||||
userSettings,
|
||||
friendCount,
|
||||
unreadCount,
|
||||
] = await Promise.all([
|
||||
prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
look: true,
|
||||
motto: true,
|
||||
rank: true,
|
||||
credits: true,
|
||||
accountCreated: true,
|
||||
lastLogin: true,
|
||||
lastOnline: true,
|
||||
},
|
||||
}),
|
||||
siteSettings.get(
|
||||
"habbo_imaging_url",
|
||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||
),
|
||||
resolveHotelName(),
|
||||
prisma.websiteSetting
|
||||
.findUnique({
|
||||
where: { key: "referrals_needed" },
|
||||
select: { value: true },
|
||||
})
|
||||
.catch(() => null),
|
||||
prisma.websiteSetting
|
||||
.findUnique({
|
||||
where: { key: "referral_reward_amount" },
|
||||
select: { value: true },
|
||||
})
|
||||
.catch(() => null),
|
||||
prisma.websiteSetting
|
||||
.findFirst({
|
||||
where: {
|
||||
key: {
|
||||
in: ["referral_reward_currency_type", "referral_reward_currency"],
|
||||
},
|
||||
},
|
||||
select: { value: true },
|
||||
})
|
||||
.catch(() => null),
|
||||
siteSettings.get("hotel_alert_banner", ""),
|
||||
prisma.rooms
|
||||
.findMany({
|
||||
orderBy: { id: "desc" },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
look: true,
|
||||
motto: true,
|
||||
rank: true,
|
||||
credits: true,
|
||||
accountCreated: true,
|
||||
lastLogin: true,
|
||||
lastOnline: true,
|
||||
name: true,
|
||||
ownerName: true,
|
||||
users: true,
|
||||
usersMax: true,
|
||||
},
|
||||
}),
|
||||
siteSettings.get(
|
||||
"habbo_imaging_url",
|
||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||
),
|
||||
resolveHotelName(),
|
||||
prisma.websiteSetting
|
||||
.findUnique({
|
||||
where: { key: "referrals_needed" },
|
||||
select: { value: true },
|
||||
})
|
||||
.catch(() => null),
|
||||
prisma.websiteSetting
|
||||
.findUnique({
|
||||
where: { key: "referral_reward_amount" },
|
||||
select: { value: true },
|
||||
})
|
||||
.catch(() => null),
|
||||
prisma.websiteSetting
|
||||
.findFirst({
|
||||
where: {
|
||||
key: {
|
||||
in: [
|
||||
"referral_reward_currency_type",
|
||||
"referral_reward_currency",
|
||||
],
|
||||
},
|
||||
},
|
||||
select: { value: true },
|
||||
})
|
||||
.catch(() => null),
|
||||
siteSettings.get("hotel_alert_banner", ""),
|
||||
prisma.rooms
|
||||
.findMany({
|
||||
orderBy: { id: "desc" },
|
||||
select: { id: true, name: true, ownerName: true, users: true, usersMax: true },
|
||||
take: 6,
|
||||
})
|
||||
.catch(() => []),
|
||||
prisma.usersBadges
|
||||
.findMany({
|
||||
where: { userId, slotId: { gt: 0 } },
|
||||
orderBy: { slotId: "asc" },
|
||||
select: { badgeCode: true, slotId: true },
|
||||
})
|
||||
.catch(() => []),
|
||||
prisma.websiteLoginLogs
|
||||
.findFirst({
|
||||
where: { userId },
|
||||
orderBy: { id: "desc" },
|
||||
select: { createdAt: true },
|
||||
})
|
||||
.catch(() => null),
|
||||
prisma.usersSettings
|
||||
.findUnique({
|
||||
where: { userId },
|
||||
select: { achievementScore: true, respectsReceived: true },
|
||||
})
|
||||
.catch(() => null),
|
||||
prisma.messengerFriendships
|
||||
.count({
|
||||
where: { OR: [{ userOneId: userId }, { userTwoId: userId }] },
|
||||
})
|
||||
.catch(() => 0),
|
||||
prisma.messengerOffline
|
||||
.count({ where: { userId } })
|
||||
.catch(() => 0),
|
||||
]);
|
||||
take: 6,
|
||||
})
|
||||
.catch(() => []),
|
||||
prisma.usersBadges
|
||||
.findMany({
|
||||
where: { userId, slotId: { gt: 0 } },
|
||||
orderBy: { slotId: "asc" },
|
||||
select: { badgeCode: true, slotId: true },
|
||||
})
|
||||
.catch(() => []),
|
||||
prisma.websiteLoginLogs
|
||||
.findFirst({
|
||||
where: { userId },
|
||||
orderBy: { id: "desc" },
|
||||
select: { createdAt: true },
|
||||
})
|
||||
.catch(() => null),
|
||||
prisma.usersSettings
|
||||
.findUnique({
|
||||
where: { userId },
|
||||
select: { achievementScore: true, respectsReceived: true },
|
||||
})
|
||||
.catch(() => null),
|
||||
prisma.messengerFriendships
|
||||
.count({
|
||||
where: { OR: [{ userOneId: userId }, { userTwoId: userId }] },
|
||||
})
|
||||
.catch(() => 0),
|
||||
prisma.messengerOffline.count({ where: { userId } }).catch(() => 0),
|
||||
]);
|
||||
|
||||
void lastWebLogin;
|
||||
|
||||
if (!user) throw new Error("user-not-found");
|
||||
|
||||
@@ -212,23 +228,23 @@ export default async function MePage({
|
||||
const lastLoginDate = user.lastLogin
|
||||
? new Date(user.lastLogin * 1000).toISOString().slice(0, 10)
|
||||
: "Never";
|
||||
const lastWebLoginDate = lastWebLogin?.createdAt
|
||||
? new Date(lastWebLogin.createdAt).toISOString().slice(0, 10)
|
||||
: null;
|
||||
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 });
|
||||
const avatarHead = avatarImageUrl(imager, user.look, { headOnly: true, direction: 2, size: "s" });
|
||||
|
||||
const imager =
|
||||
imagerBase ?? "https://www.habbo.com/habbo-imaging/avatarimage";
|
||||
const avatarFull = avatarImageUrl(imager, user.look, {
|
||||
size: "l",
|
||||
direction: 2,
|
||||
});
|
||||
content = (
|
||||
<div className="flex flex-col gap-8 pb-8">
|
||||
{claimed ? (
|
||||
<div role="status" style={feedbackStyle("success")}>
|
||||
Woah! You have successfully claimed your reward — keep up the good work!
|
||||
Woah! You have successfully claimed your reward — keep up the good
|
||||
work!
|
||||
</div>
|
||||
) : null}
|
||||
{error ? (
|
||||
@@ -242,24 +258,47 @@ export default async function MePage({
|
||||
className="relative overflow-hidden rounded-2xl border p-6 flex items-start gap-6"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, color-mix(in srgb, var(--color-primary) 12%, var(--color-surface)), var(--color-surface))`,
|
||||
borderColor: "color-mix(in srgb, var(--color-primary) 20%, transparent)",
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-primary) 20%, transparent)",
|
||||
}}
|
||||
>
|
||||
<div className="absolute -top-10 -right-10 w-40 h-40 rounded-full opacity-10" style={{ background: "var(--color-primary)" }} />
|
||||
<div
|
||||
className="absolute -top-10 -right-10 w-40 h-40 rounded-full opacity-10"
|
||||
style={{ background: "var(--color-primary)" }}
|
||||
/>
|
||||
<div className="relative shrink-0 flex flex-col items-center gap-3">
|
||||
<div className="absolute -inset-3 rounded-full opacity-[0.15] blur-[40px]" style={{ background: "var(--color-primary)" }} />
|
||||
<Image src={avatarFull} alt="" width={120} height={160} className="object-contain drop-shadow-xl relative" unoptimized priority />
|
||||
<div
|
||||
className="absolute -inset-3 rounded-full opacity-[0.15] blur-[40px]"
|
||||
style={{ background: "var(--color-primary)" }}
|
||||
/>
|
||||
<Image
|
||||
src={avatarFull}
|
||||
alt=""
|
||||
width={120}
|
||||
height={160}
|
||||
className="object-contain drop-shadow-xl relative"
|
||||
unoptimized
|
||||
priority
|
||||
/>
|
||||
<Link
|
||||
href="/client"
|
||||
className="btn-shine flex items-center justify-center gap-1.5 rounded-xl border py-2 px-4 font-extrabold text-xs transition-all duration-200 hover:scale-[1.02] active:scale-95 shadow-lg"
|
||||
style={{
|
||||
background: "var(--color-primary)",
|
||||
color: "var(--color-primary-foreground)",
|
||||
borderColor: "color-mix(in srgb, var(--color-primary) 20%, transparent)",
|
||||
boxShadow: "0 4px 24px color-mix(in srgb, var(--color-primary) 35%, transparent)",
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-primary) 20%, transparent)",
|
||||
boxShadow:
|
||||
"0 4px 24px color-mix(in srgb, var(--color-primary) 35%, transparent)",
|
||||
}}
|
||||
>
|
||||
<Image src="/assets/images/EnterHubbly.png" alt="" width={16} height={16} unoptimized />
|
||||
<Image
|
||||
src="/assets/images/EnterHubbly.png"
|
||||
alt=""
|
||||
width={16}
|
||||
height={16}
|
||||
unoptimized
|
||||
/>
|
||||
Enter {hotelName}
|
||||
</Link>
|
||||
</div>
|
||||
@@ -267,25 +306,39 @@ export default async function MePage({
|
||||
<div
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[11px] font-bold uppercase tracking-wider mb-3 border"
|
||||
style={{
|
||||
borderColor: "color-mix(in srgb, var(--color-primary) 18%, transparent)",
|
||||
background: "color-mix(in srgb, var(--color-primary) 10%, transparent)",
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-primary) 18%, transparent)",
|
||||
background:
|
||||
"color-mix(in srgb, var(--color-primary) 10%, transparent)",
|
||||
color: "var(--color-primary-readable, var(--color-primary))",
|
||||
}}
|
||||
>
|
||||
Rank {user.rank}
|
||||
</div>
|
||||
<h1 className="text-2xl md:text-3xl font-black tracking-tight" style={{ color: "var(--color-text-readable)" }}>
|
||||
<h1
|
||||
className="text-2xl md:text-3xl font-black tracking-tight"
|
||||
style={{ color: "var(--color-text-readable)" }}
|
||||
>
|
||||
{user.username}
|
||||
</h1>
|
||||
<p className="text-sm mt-1" style={{ color: "var(--color-text-muted)" }}>
|
||||
<p
|
||||
className="text-sm mt-1"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{user.motto || "No motto"}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-x-6 gap-y-1 mt-4 text-xs">
|
||||
<span style={{ color: "var(--color-text-muted)" }}>
|
||||
Registered: <strong style={{ color: "var(--color-text-readable)" }}>{registered}</strong>
|
||||
Registered:{" "}
|
||||
<strong style={{ color: "var(--color-text-readable)" }}>
|
||||
{registered}
|
||||
</strong>
|
||||
</span>
|
||||
<span style={{ color: "var(--color-text-muted)" }}>
|
||||
Last online: <strong style={{ color: "var(--color-text-readable)" }}>{lastOnlineDate}</strong>
|
||||
Last online:{" "}
|
||||
<strong style={{ color: "var(--color-text-readable)" }}>
|
||||
{lastOnlineDate}
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -294,8 +347,21 @@ export default async function MePage({
|
||||
|
||||
{alertRaw ? (
|
||||
<Reveal delay={0.05}>
|
||||
<div className="rounded-2xl border px-6 py-4 text-center" style={{ background: "color-mix(in srgb, var(--color-accent) 12%, transparent)", borderColor: "color-mix(in srgb, var(--color-accent) 25%, transparent)" }}>
|
||||
<p className="text-sm font-bold" style={{ color: "var(--color-accent)" }}>{alertRaw}</p>
|
||||
<div
|
||||
className="rounded-2xl border px-6 py-4 text-center"
|
||||
style={{
|
||||
background:
|
||||
"color-mix(in srgb, var(--color-accent) 12%, transparent)",
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-accent) 25%, transparent)",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-sm font-bold"
|
||||
style={{ color: "var(--color-accent)" }}
|
||||
>
|
||||
{alertRaw}
|
||||
</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
) : null}
|
||||
@@ -305,8 +371,14 @@ export default async function MePage({
|
||||
{[
|
||||
{ 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: (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) => (
|
||||
@@ -315,13 +387,20 @@ export default async function MePage({
|
||||
className="relative rounded-2xl border p-5 text-center transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
||||
style={{
|
||||
background: "var(--color-surface)",
|
||||
borderColor: "color-mix(in srgb, var(--color-primary) 12%, transparent)",
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-primary) 12%, transparent)",
|
||||
}}
|
||||
>
|
||||
<div className="text-xl md:text-2xl font-black tracking-tight mb-0.5" style={{ color: "var(--color-primary)" }}>
|
||||
<div
|
||||
className="text-xl md:text-2xl font-black tracking-tight mb-0.5"
|
||||
style={{ color: "var(--color-primary)" }}
|
||||
>
|
||||
{s.value}
|
||||
</div>
|
||||
<div className="text-[11px] font-bold uppercase tracking-widest" style={{ color: "var(--color-text-muted)" }}>
|
||||
<div
|
||||
className="text-[11px] font-bold uppercase tracking-widest"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{s.label}
|
||||
</div>
|
||||
</div>
|
||||
@@ -330,19 +409,47 @@ export default async function MePage({
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.15}>
|
||||
<div className="rounded-2xl border p-5" style={{ background: "var(--color-surface)", borderColor: "color-mix(in srgb, var(--color-primary) 12%, transparent)" }}>
|
||||
<div
|
||||
className="rounded-2xl border p-5"
|
||||
style={{
|
||||
background: "var(--color-surface)",
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-primary) 12%, transparent)",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-black tracking-tight" style={{ color: "var(--color-text-readable)" }}>Online friends</h2>
|
||||
<p className="text-xs mt-0.5" style={{ color: "var(--color-text-muted)" }}>
|
||||
{onlineFriends.length === 0 ? "None of your friends are online right now" : `${onlineFriends.length} of your ${friends.length} friends online`}
|
||||
<h2
|
||||
className="text-lg font-black tracking-tight"
|
||||
style={{ color: "var(--color-text-readable)" }}
|
||||
>
|
||||
Online friends
|
||||
</h2>
|
||||
<p
|
||||
className="text-xs mt-0.5"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{onlineFriends.length === 0
|
||||
? "None of your friends are online right now"
|
||||
: `${onlineFriends.length} of your ${friends.length} friends online`}
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/friends" className="text-xs font-bold hover:underline shrink-0" style={{ color: "var(--color-primary)" }}>View all</Link>
|
||||
<Link
|
||||
href="/friends"
|
||||
className="text-xs font-bold hover:underline shrink-0"
|
||||
style={{ color: "var(--color-primary)" }}
|
||||
>
|
||||
View all
|
||||
</Link>
|
||||
</div>
|
||||
{onlineFriends.length === 0 ? (
|
||||
<div className="text-center py-6">
|
||||
<p className="text-sm" style={{ color: "var(--color-text-muted)" }}>No friends online right now. Check back later!</p>
|
||||
<p
|
||||
className="text-sm"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
No friends online right now. Check back later!
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||
@@ -351,14 +458,45 @@ export default async function MePage({
|
||||
key={friend.id}
|
||||
href={`/u/${friend.username}`}
|
||||
className="flex items-center gap-3 p-3 rounded-xl transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md"
|
||||
style={{ background: "color-mix(in srgb, var(--color-primary) 4%, transparent)", textDecoration: "none" }}
|
||||
style={{
|
||||
background:
|
||||
"color-mix(in srgb, var(--color-primary) 4%, transparent)",
|
||||
textDecoration: "none",
|
||||
}}
|
||||
>
|
||||
<Image src={avatarImageUrl(imager, friend.look, { size: "s", headOnly: true })} alt="" width={40} height={50} className="rounded-lg shrink-0" unoptimized />
|
||||
<Image
|
||||
src={avatarImageUrl(imager, friend.look, {
|
||||
size: "s",
|
||||
headOnly: true,
|
||||
})}
|
||||
alt=""
|
||||
width={40}
|
||||
height={50}
|
||||
className="rounded-lg shrink-0"
|
||||
unoptimized
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-extrabold truncate" style={{ color: "var(--color-text-readable)" }}>{friend.username}</p>
|
||||
<p className="text-[10px] font-semibold truncate mt-0.5" style={{ color: "var(--color-text-muted)" }}>{friend.motto || "No motto"}</p>
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-bold mt-1" style={{ color: "var(--color-success)" }}>
|
||||
<span className="w-1.5 h-1.5 rounded-full" style={{ background: "var(--color-success)" }} /> Online
|
||||
<p
|
||||
className="text-sm font-extrabold truncate"
|
||||
style={{ color: "var(--color-text-readable)" }}
|
||||
>
|
||||
{friend.username}
|
||||
</p>
|
||||
<p
|
||||
className="text-[10px] font-semibold truncate mt-0.5"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{friend.motto || "No motto"}
|
||||
</p>
|
||||
<span
|
||||
className="inline-flex items-center gap-1 text-[10px] font-bold mt-1"
|
||||
style={{ color: "var(--color-success)" }}
|
||||
>
|
||||
<span
|
||||
className="w-1.5 h-1.5 rounded-full"
|
||||
style={{ background: "var(--color-success)" }}
|
||||
/>{" "}
|
||||
Online
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
@@ -369,41 +507,92 @@ export default async function MePage({
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.2}>
|
||||
<div className="rounded-2xl border p-5" style={{ background: "var(--color-surface)", borderColor: "color-mix(in srgb, var(--color-primary) 12%, transparent)" }}>
|
||||
<div
|
||||
className="rounded-2xl border p-5"
|
||||
style={{
|
||||
background: "var(--color-surface)",
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-primary) 12%, transparent)",
|
||||
}}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<h2 className="text-lg font-black tracking-tight" style={{ color: "var(--color-text-readable)" }}>Refer a friend ({referralTotal}/{needed})</h2>
|
||||
<p className="text-xs mt-0.5" style={{ color: "var(--color-text-muted)" }}>Refer new users and earn in-game rewards</p>
|
||||
<h2
|
||||
className="text-lg font-black tracking-tight"
|
||||
style={{ color: "var(--color-text-readable)" }}
|
||||
>
|
||||
Refer a friend ({referralTotal}/{needed})
|
||||
</h2>
|
||||
<p
|
||||
className="text-xs mt-0.5"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
Refer new users and earn in-game rewards
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-sm" style={{ color: "var(--color-text-muted)" }}>
|
||||
<p
|
||||
className="text-sm"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{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!`}
|
||||
</p>
|
||||
<div role="note" style={feedbackStyle("warning")}>
|
||||
Boosting referrals by making your own accounts will lead to removal of all progress, currency, inventory and a potential ban.
|
||||
Boosting referrals by making your own accounts will lead to
|
||||
removal of all progress, currency, inventory and a potential
|
||||
ban.
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="referral-link" className="text-xs font-bold mb-1.5 block" style={{ color: "var(--color-text-muted)" }}>Your referral link</label>
|
||||
<label
|
||||
htmlFor="referral-link"
|
||||
className="text-xs font-bold mb-1.5 block"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
Your referral link
|
||||
</label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<input id="referral-link" type="text" readOnly defaultValue={referralLink}
|
||||
<input
|
||||
id="referral-link"
|
||||
type="text"
|
||||
readOnly
|
||||
defaultValue={referralLink}
|
||||
className="input-glow flex-1 min-w-0 rounded-xl border-2 px-4 py-2.5 text-sm font-medium"
|
||||
style={{ backgroundColor: "var(--color-background)", color: "var(--color-text-readable, var(--color-text))", borderColor: "color-mix(in srgb, var(--color-text-muted) 25%, transparent)" }}
|
||||
style={{
|
||||
backgroundColor: "var(--color-background)",
|
||||
color: "var(--color-text-readable, var(--color-text))",
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
|
||||
}}
|
||||
/>
|
||||
<CopyReferralButton value={referralLink} />
|
||||
</div>
|
||||
</div>
|
||||
{canClaim ? (
|
||||
<form action={claimReferral}>
|
||||
<button type="submit" className="btn-shine w-full rounded-xl font-extrabold text-sm py-3.5 transition-all duration-200 hover:scale-[1.02] active:scale-95 shadow-lg"
|
||||
style={{ background: "var(--color-primary)", color: "var(--color-primary-foreground)", boxShadow: "0 4px 24px color-mix(in srgb, var(--color-primary) 35%, transparent)" }}
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-shine w-full rounded-xl font-extrabold text-sm py-3.5 transition-all duration-200 hover:scale-[1.02] active:scale-95 shadow-lg"
|
||||
style={{
|
||||
background: "var(--color-primary)",
|
||||
color: "var(--color-primary-foreground)",
|
||||
boxShadow:
|
||||
"0 4px 24px color-mix(in srgb, var(--color-primary) 35%, transparent)",
|
||||
}}
|
||||
>
|
||||
Claim your referral reward!
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<button type="button" disabled className="w-full rounded-xl font-extrabold text-sm py-3.5 opacity-60 cursor-not-allowed"
|
||||
style={{ background: "color-mix(in srgb, var(--color-text-muted) 15%, transparent)", color: "var(--color-text-muted)" }}
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className="w-full rounded-xl font-extrabold text-sm py-3.5 opacity-60 cursor-not-allowed"
|
||||
style={{
|
||||
background:
|
||||
"color-mix(in srgb, var(--color-text-muted) 15%, transparent)",
|
||||
color: "var(--color-text-muted)",
|
||||
}}
|
||||
>
|
||||
{`Refer ${remaining} more ${remaining === 1 ? "user" : "users"} to unlock your reward`}
|
||||
</button>
|
||||
@@ -414,22 +603,62 @@ export default async function MePage({
|
||||
|
||||
{recentRooms.length > 0 && (
|
||||
<Reveal delay={0.25}>
|
||||
<div className="rounded-2xl border p-5" style={{ background: "var(--color-surface)", borderColor: "color-mix(in srgb, var(--color-primary) 12%, transparent)" }}>
|
||||
<div
|
||||
className="rounded-2xl border p-5"
|
||||
style={{
|
||||
background: "var(--color-surface)",
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-primary) 12%, transparent)",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-black tracking-tight" style={{ color: "var(--color-text-readable)" }}>Recent rooms</h2>
|
||||
<p className="text-xs mt-0.5" style={{ color: "var(--color-text-muted)" }}>Newest rooms in the hotel</p>
|
||||
<h2
|
||||
className="text-lg font-black tracking-tight"
|
||||
style={{ color: "var(--color-text-readable)" }}
|
||||
>
|
||||
Recent rooms
|
||||
</h2>
|
||||
<p
|
||||
className="text-xs mt-0.5"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
Newest rooms in the hotel
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/community" className="text-xs font-bold hover:underline shrink-0" style={{ color: "var(--color-primary)" }}>View all</Link>
|
||||
<Link
|
||||
href="/community"
|
||||
className="text-xs font-bold hover:underline shrink-0"
|
||||
style={{ color: "var(--color-primary)" }}
|
||||
>
|
||||
View all
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||
{recentRooms.map((room) => (
|
||||
<Link key={room.id} href={`/room/${room.id}`}
|
||||
<Link
|
||||
key={room.id}
|
||||
href={`/room/${room.id}`}
|
||||
className="block rounded-xl border p-3 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md"
|
||||
style={{ background: "color-mix(in srgb, var(--color-primary) 4%, transparent)", borderColor: "color-mix(in srgb, var(--color-primary) 10%, transparent)" }}
|
||||
style={{
|
||||
background:
|
||||
"color-mix(in srgb, var(--color-primary) 4%, transparent)",
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-primary) 10%, transparent)",
|
||||
}}
|
||||
>
|
||||
<p className="text-sm font-extrabold truncate" style={{ color: "var(--color-text-readable)" }}>{room.name}</p>
|
||||
<p className="text-[10px] font-semibold mt-0.5" style={{ color: "var(--color-text-muted)" }}>{room.ownerName} · {room.users}/{room.usersMax}</p>
|
||||
<p
|
||||
className="text-sm font-extrabold truncate"
|
||||
style={{ color: "var(--color-text-readable)" }}
|
||||
>
|
||||
{room.name}
|
||||
</p>
|
||||
<p
|
||||
className="text-[10px] font-semibold mt-0.5"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{room.ownerName} · {room.users}/{room.usersMax}
|
||||
</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
@@ -439,14 +668,39 @@ export default async function MePage({
|
||||
|
||||
{badges.length > 0 && (
|
||||
<Reveal delay={0.3}>
|
||||
<div className="rounded-2xl border p-5" style={{ background: "var(--color-surface)", borderColor: "color-mix(in srgb, var(--color-primary) 12%, transparent)" }}>
|
||||
<div
|
||||
className="rounded-2xl border p-5"
|
||||
style={{
|
||||
background: "var(--color-surface)",
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-primary) 12%, transparent)",
|
||||
}}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<h2 className="text-lg font-black tracking-tight" style={{ color: "var(--color-text-readable)" }}>Badges</h2>
|
||||
<p className="text-xs mt-0.5" style={{ color: "var(--color-text-muted)" }}>{badges.length} badge{badges.length !== 1 ? "s" : ""} equipped</p>
|
||||
<h2
|
||||
className="text-lg font-black tracking-tight"
|
||||
style={{ color: "var(--color-text-readable)" }}
|
||||
>
|
||||
Badges
|
||||
</h2>
|
||||
<p
|
||||
className="text-xs mt-0.5"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{badges.length} badge{badges.length !== 1 ? "s" : ""} equipped
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{badges.map((badge) => (
|
||||
<Image key={badge.badgeCode} src={`${BADGE_IMG_BASE}/${badge.badgeCode}.gif`} alt={badge.badgeCode} width={40} height={40} className="rounded-md transition-transform hover:scale-110" unoptimized />
|
||||
<Image
|
||||
key={badge.badgeCode}
|
||||
src={`${BADGE_IMG_BASE}/${badge.badgeCode}.gif`}
|
||||
alt={badge.badgeCode}
|
||||
width={40}
|
||||
height={40}
|
||||
className="rounded-md transition-transform hover:scale-110"
|
||||
unoptimized
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -454,8 +708,20 @@ export default async function MePage({
|
||||
)}
|
||||
|
||||
<Reveal delay={0.35}>
|
||||
<div className="rounded-2xl border p-5" style={{ background: "var(--color-surface)", borderColor: "color-mix(in srgb, var(--color-primary) 12%, transparent)" }}>
|
||||
<h2 className="text-lg font-black tracking-tight mb-4" style={{ color: "var(--color-text-readable)" }}>Quick links</h2>
|
||||
<div
|
||||
className="rounded-2xl border p-5"
|
||||
style={{
|
||||
background: "var(--color-surface)",
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-primary) 12%, transparent)",
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
className="text-lg font-black tracking-tight mb-4"
|
||||
style={{ color: "var(--color-text-readable)" }}
|
||||
>
|
||||
Quick links
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
{[
|
||||
{ href: "/messages", label: "Messages", badge: unreadCount },
|
||||
@@ -463,13 +729,27 @@ export default async function MePage({
|
||||
{ href: "/settings", label: "Settings", badge: 0 },
|
||||
{ href: "/shop", label: "Shop", badge: 0 },
|
||||
].map((link) => (
|
||||
<Link key={link.href} href={link.href}
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="relative flex items-center justify-center rounded-xl border p-3 text-sm font-bold transition-all hover:-translate-y-0.5 hover:shadow-md"
|
||||
style={{ background: "color-mix(in srgb, var(--color-primary) 6%, transparent)", borderColor: "color-mix(in srgb, var(--color-primary) 18%, transparent)", color: "var(--color-text-readable)" }}
|
||||
style={{
|
||||
background:
|
||||
"color-mix(in srgb, var(--color-primary) 6%, transparent)",
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-primary) 18%, transparent)",
|
||||
color: "var(--color-text-readable)",
|
||||
}}
|
||||
>
|
||||
{link.label}
|
||||
{link.badge > 0 && (
|
||||
<span className="absolute -top-1.5 -right-1.5 flex min-w-[18px] h-[18px] items-center justify-center rounded-full px-1 text-[10px] font-bold leading-none" style={{ background: "var(--color-danger)", color: "white" }}>
|
||||
<span
|
||||
className="absolute -top-1.5 -right-1.5 flex min-w-[18px] h-[18px] items-center justify-center rounded-full px-1 text-[10px] font-bold leading-none"
|
||||
style={{
|
||||
background: "var(--color-danger)",
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
{link.badge > 99 ? "99+" : link.badge}
|
||||
</span>
|
||||
)}
|
||||
@@ -486,11 +766,16 @@ export default async function MePage({
|
||||
className="rounded-2xl border p-6 text-center"
|
||||
style={{
|
||||
background: "var(--color-surface)",
|
||||
borderColor: "color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
|
||||
}}
|
||||
>
|
||||
<p className="text-sm font-bold" style={{ color: "var(--color-text-muted)" }}>
|
||||
We couldn't load your dashboard right now. Please try again shortly.
|
||||
<p
|
||||
className="text-sm font-bold"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
We couldn't load your dashboard right now. Please try again
|
||||
shortly.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user