- Add framer-motion and tailwindcss-animate for transitions - Add page transitions via AnimatePresence in the site layout - Convert nav dropdown and mobile menu to animated motion components - Add entry animation to the radio player widget - Add reveal/stagger animations to the home page views - Add shared motion utilities and reusable animated components
286 lines
8.5 KiB
TypeScript
286 lines
8.5 KiB
TypeScript
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { getTranslations } from "next-intl/server";
|
|
import { Reveal } from "@/components/motion-reveal";
|
|
import { avatarImageUrl, excerpt } from "@/lib/format";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { siteSettings } from "@/lib/services/site-settings";
|
|
|
|
interface UserViewProps {
|
|
userId: number;
|
|
username: string;
|
|
look: string;
|
|
motto?: string | null;
|
|
}
|
|
|
|
export default async function UserView({
|
|
userId,
|
|
username,
|
|
look,
|
|
motto,
|
|
}: UserViewProps) {
|
|
const t = await getTranslations("pages.home");
|
|
const hotelName = (await siteSettings.get("hotel_name", "Atom")) ?? "Atom";
|
|
const imager =
|
|
(await siteSettings.get(
|
|
"habbo_imaging_url",
|
|
"https://www.habbo.com/habbo-imaging/avatarimage",
|
|
)) ?? "";
|
|
|
|
let onlineFriends: Array<{ username: string; look: string }> = [];
|
|
try {
|
|
const friendships = await prisma.messengerFriendships.findMany({
|
|
where: { OR: [{ userOneId: userId }, { userTwoId: userId }] },
|
|
select: { userOneId: true, userTwoId: true },
|
|
});
|
|
const friendIds = friendships.map((f) =>
|
|
f.userOneId === userId ? f.userTwoId : f.userOneId,
|
|
);
|
|
if (friendIds.length > 0) {
|
|
onlineFriends = await prisma.user.findMany({
|
|
where: { id: { in: friendIds }, online: "1" },
|
|
take: 10,
|
|
select: { username: true, look: true },
|
|
});
|
|
}
|
|
} catch {}
|
|
|
|
const [latestArticle] = await prisma.websiteArticles
|
|
.findMany({
|
|
orderBy: { createdAt: "desc" },
|
|
take: 1,
|
|
select: { slug: true, title: true, shortStory: true, image: true },
|
|
})
|
|
.catch(() => []);
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6 md:flex-row">
|
|
<Reveal className="flex w-full flex-col gap-6 md:w-3/4">
|
|
<div className="relative flex h-[340px] items-start overflow-hidden rounded-lg">
|
|
<div
|
|
className="absolute inset-0"
|
|
style={{
|
|
backgroundImage: "url(/assets/images/kasja_mepage_header.png)",
|
|
backgroundSize: "cover",
|
|
backgroundPosition: "center 30%",
|
|
backgroundRepeat: "no-repeat",
|
|
}}
|
|
/>
|
|
<div
|
|
className="absolute inset-0"
|
|
style={{
|
|
background:
|
|
"linear-gradient(135deg, rgba(0,0,0,0.6) 0%, rgba(0,0,0,0.15) 40%, rgba(0,0,0,0.25) 65%, rgba(0,0,0,0.6) 100%)",
|
|
}}
|
|
/>
|
|
<div
|
|
className="absolute -bottom-10 -right-10 w-64 h-64 rounded-full opacity-[0.15] blur-[80px]"
|
|
style={{ background: "var(--color-primary)" }}
|
|
/>
|
|
<div
|
|
className="absolute -top-10 -left-10 w-48 h-48 rounded-full opacity-[0.08] blur-[60px]"
|
|
style={{ background: "var(--color-primary)" }}
|
|
/>
|
|
<div
|
|
className="relative z-10 mx-auto mt-8 flex w-[95%] max-w-3xl items-center justify-between rounded-2xl px-8 py-6"
|
|
style={{
|
|
background:
|
|
"linear-gradient(135deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.2) 100%)",
|
|
boxShadow: "0 8px 32px rgba(0,0,0,0.3)",
|
|
}}
|
|
>
|
|
<div className="flex items-center gap-6">
|
|
<Link
|
|
href={`/u/${username}`}
|
|
className="transition-transform duration-300 hover:scale-105"
|
|
>
|
|
<div
|
|
className="rounded-full p-[3px]"
|
|
style={{
|
|
border: "3px solid var(--panel-border-color)",
|
|
boxShadow:
|
|
"0 0 24px color-mix(in srgb, var(--panel-border-color) 50%, transparent)",
|
|
}}
|
|
>
|
|
<Image
|
|
src={avatarImageUrl(imager, look, {
|
|
size: "l",
|
|
direction: 2,
|
|
})}
|
|
alt={username}
|
|
width={110}
|
|
height={190}
|
|
className="drop-shadow-2xl"
|
|
style={{ imageRendering: "pixelated" }}
|
|
unoptimized
|
|
/>
|
|
</div>
|
|
</Link>
|
|
<div
|
|
className="flex flex-col gap-1.5 rounded-xl px-4 py-2"
|
|
style={{
|
|
background:
|
|
"color-mix(in srgb, var(--color-surface) 88%, transparent)",
|
|
backdropFilter: "blur(8px)",
|
|
}}
|
|
>
|
|
<Link
|
|
href={`/u/${username}`}
|
|
className="text-2xl font-bold drop-shadow-lg hover:underline"
|
|
style={{ color: "var(--color-text)" }}
|
|
>
|
|
{username}
|
|
</Link>
|
|
<p
|
|
className="text-sm drop-shadow-md max-w-[240px] leading-tight"
|
|
style={{
|
|
color:
|
|
"color-mix(in srgb, var(--color-text) 80%, transparent)",
|
|
}}
|
|
>
|
|
{motto || "Welcome back!"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center">
|
|
<Link
|
|
href="/client"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
prefetch={true}
|
|
>
|
|
<button
|
|
type="button"
|
|
className="group rounded-full px-7 py-3 text-sm font-bold tracking-wider transition-all duration-300 hover:scale-105 active:scale-95"
|
|
style={{
|
|
background:
|
|
"linear-gradient(135deg, color-mix(in srgb, var(--color-primary) 92%, white), color-mix(in srgb, var(--color-primary) 92%, black))",
|
|
color:
|
|
"var(--button-text-color-readable, var(--button-text-color))",
|
|
border:
|
|
"1px solid color-mix(in srgb, var(--color-primary) 60%, rgba(255,255,255,0.2))",
|
|
boxShadow:
|
|
"0 4px 24px color-mix(in srgb, var(--color-primary) 35%, transparent), inset 0 1px 0 rgba(255,255,255,0.2)",
|
|
}}
|
|
>
|
|
{t("enterHotel", { hotelName })}
|
|
<svg
|
|
className="inline-block ml-2 w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2.5}
|
|
d="M9 5l7 7-7 7"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className="flex flex-col gap-3 rounded-lg border p-1 lg:flex-row"
|
|
style={{
|
|
backgroundColor: "var(--color-surface)",
|
|
borderColor:
|
|
"color-mix(in srgb, var(--color-text-muted) 22%, transparent)",
|
|
}}
|
|
>
|
|
<div
|
|
className="relative flex items-center justify-center rounded p-2 text-sm font-semibold"
|
|
style={{ backgroundColor: "var(--panel-border-color)" }}
|
|
>
|
|
<span className="relative text-white">{t("onlineFriends")}</span>
|
|
</div>
|
|
<div className="flex items-center gap-3 px-3">
|
|
{onlineFriends.length === 0 ? (
|
|
<p
|
|
className="mb-3 block w-full text-center text-xs font-medium md:mb-0 md:text-left"
|
|
style={{
|
|
color:
|
|
"var(--color-text-muted-readable, var(--color-text-muted))",
|
|
}}
|
|
>
|
|
{t("noFriendsOnline")}
|
|
</p>
|
|
) : (
|
|
onlineFriends.map((f) => (
|
|
<Link
|
|
key={f.username}
|
|
href={`/u/${f.username}`}
|
|
className="transition-all duration-200 hover:scale-110"
|
|
>
|
|
<Image
|
|
src={avatarImageUrl(imager, f.look, { headOnly: true })}
|
|
alt={f.username}
|
|
width={40}
|
|
height={40}
|
|
className="h-10 w-10"
|
|
title={f.username}
|
|
unoptimized
|
|
/>
|
|
</Link>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Reveal>
|
|
|
|
<Reveal delay={0.15} className="flex w-full flex-col gap-6 md:w-1/4">
|
|
{latestArticle ? (
|
|
<div
|
|
className="overflow-hidden rounded-lg border"
|
|
style={{
|
|
backgroundColor: "var(--color-surface)",
|
|
borderColor:
|
|
"color-mix(in srgb, var(--color-text-muted) 22%, transparent)",
|
|
}}
|
|
>
|
|
<div
|
|
className="flex h-12 items-center justify-center"
|
|
style={{
|
|
background:
|
|
"linear-gradient(140deg, #1a3a5c 0%, #1e4a6e 20%, #2a5a80 40%, #3a6a90 60%, #4a7aa0 75%, #3a6a90 90%, #2a5a80 100%)",
|
|
}}
|
|
>
|
|
<h2 className="text-lg font-bold text-white">
|
|
{t("latestArticle")}
|
|
</h2>
|
|
</div>
|
|
<Link href={`/news/${latestArticle.slug}`} className="group block">
|
|
<div className="relative aspect-[16/9] overflow-hidden">
|
|
<Image
|
|
src={latestArticle.image}
|
|
alt={latestArticle.title}
|
|
fill
|
|
className="object-cover transition-transform duration-300 group-hover:scale-105"
|
|
unoptimized
|
|
/>
|
|
</div>
|
|
<div className="p-3">
|
|
<h3 className="text-sm font-semibold">{latestArticle.title}</h3>
|
|
{latestArticle.shortStory && (
|
|
<p
|
|
className="mt-1 text-xs"
|
|
style={{
|
|
color:
|
|
"var(--color-text-muted-readable, var(--color-text-muted))",
|
|
}}
|
|
>
|
|
{excerpt(latestArticle.shortStory, 100)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</Link>
|
|
</div>
|
|
) : null}
|
|
</Reveal>
|
|
</div>
|
|
);
|
|
}
|