Pull mega-menu from g5_eyoom_menu and redesign home with Tailwind v4

## Menu now matches production exactly
- Read the 122-row inspection2.g5_eyoom_menu tree directly so the admin's
  defined hierarchy (10 top-level + 25+ submenus + sub-submenus) flows
  straight to the navbar without hand-maintained constants.
- Build tree from me_code (3/6/9 char prefix = depth), preserve me_order,
  rewrite legacy /bbs/board.php?bo_table=foo + /bbs/qalist.php +
  game/exchange/lottery URLs into the new app routes.
- Top-level + submenu coverage verified by grep against rendered HTML:
  10/10 top items + ALL 카지노 게임 (5트레져/88포춘/바다이야기/다빈치/
  체리마스터/야마토/강시/루팡/대공/축제/마릴린먼로/고인돌/반지의제왕/
  바카본), 스포츠 (크로스/스페셜), 미니게임 (슬롯홀짝/파워볼), 슬생TV
  (스포츠중계/하이라이트/픽게시판/큰손형방송), 포인트존 10개 항목 모두.

## Home page rebuilt with Tailwind v4 + Framer Motion + lucide
- New @import "tailwindcss" theme with brand-50..900 palette, shadow-pop,
  ticker marquee animation, and a `lift` hover transform.
- Hero block: gradient radial backdrop, blur orbs, animated headline ticker,
  three pill CTAs, and a glassmorphic KICK 큰손형 방송 status card with
  pulsing live-dot. Status pulled from getKickStatus() (live/break/offline
  by hour & weekday).
- 9-tile QuickAccess grid where each tile gets its own gradient (purple/
  rose/amber/emerald/blue/pink/yellow/cyan/violet) and lifts on hover.
- BoardSlots cards with per-board gradient header (free=violet,
  review=amber, mukti=rose, humor=sky, pick=emerald, lottery_ticket=fuchsia)
  and rose comment badges.
- Header: sticky blurred top bar, integrated 검색 box in brand row, mega
  nav with framer-motion slide-down submenus, dark mode button.
- Sidebar: glassmorphic LOGIN card with point/level row, Telegram CS
  banner with gradient + shadow, brand-tinted tag pills, ranked member
  list with gold/silver/bronze chips, visitor stats grid.
- Footer: deep purple gradient with brand mark, 4 link columns, terms
  and privacy emphasized.

## New menu-driven routes
- /games/[game] catch-all renders all 14 slot simulators + roulette +
  ranking pages with a unified gradient header + 3-card stats template.
- /tv/sports, /tv/highlight, /games/sports/{cross,special},
  /games/mini/{slot-holjjak,powerball}, /wallet/{guide, exchange/list,
  point-exchange/list, event-exchange, event-exchange/list},
  /column, /dividend, /adjudicate, /newsite, /plugin, /lottery,
  /fakeslot, /interrogation, /report — all 200, all themed.

## Verification
- 27 routes + 4 theme variants + full-page home + 10 mega-menu hover
  captures — all pass. PNGs under next-app/screenshots/.
This commit is contained in:
chpark
2026-04-27 20:38:47 +09:00
parent 30e9b7a8ee
commit 50f1d5cfb6
84 changed files with 1666 additions and 251 deletions
+21 -15
View File
@@ -4,27 +4,33 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "next dev -p 3000", "dev": "next dev -p 3000",
"build": "next build", "build": "next build",
"start": "next start -p 3000", "start": "next start -p 3000",
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@slot/db": "workspace:*", "@slot/auth": "workspace:*",
"@slot/auth": "workspace:*", "@slot/db": "workspace:*",
"@slot/themes": "workspace:*", "@slot/themes": "workspace:*",
"next": "^15.1.0", "@tailwindcss/postcss": "^4.2.4",
"react": "^19.0.0", "clsx": "^2.1.1",
"react-dom": "^19.0.0", "drizzle-orm": "^0.36.4",
"drizzle-orm": "^0.36.4", "framer-motion": "^12.38.0",
"postgres": "^3.4.5" "lucide-react": "^1.11.0",
"next": "^15.1.0",
"postcss": "^8.5.12",
"postgres": "^3.4.5",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^4.2.4"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.7.2", "@types/node": "^22.10.0",
"@types/react": "^19.0.1", "@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2", "@types/react-dom": "^19.0.2",
"@types/node": "^22.10.0", "eslint": "^9.17.0",
"eslint": "^9.17.0", "eslint-config-next": "^15.1.0",
"eslint-config-next": "^15.1.0" "typescript": "^5.7.2"
} }
} }
+5
View File
@@ -0,0 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};
@@ -0,0 +1,5 @@
import BoardPage from '@/app/[boardSlug]/page';
export const dynamic = 'force-dynamic';
export default async function Page({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
return <BoardPage params={Promise.resolve({ boardSlug: 'adjudicate' })} searchParams={searchParams} />;
}
@@ -0,0 +1,5 @@
import BoardPage from '@/app/[boardSlug]/page';
export const dynamic = 'force-dynamic';
export default async function Page({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
return <BoardPage params={Promise.resolve({ boardSlug: 'column' })} searchParams={searchParams} />;
}
@@ -0,0 +1,5 @@
import BoardPage from '@/app/[boardSlug]/page';
export const dynamic = 'force-dynamic';
export default async function Page({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
return <BoardPage params={Promise.resolve({ boardSlug: 'dividend' })} searchParams={searchParams} />;
}
@@ -0,0 +1,5 @@
import BoardPage from '@/app/[boardSlug]/page';
export const dynamic = 'force-dynamic';
export default async function Page({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
return <BoardPage params={Promise.resolve({ boardSlug: 'fakeslot' })} searchParams={searchParams} />;
}
@@ -0,0 +1,72 @@
// Catch-all for any /games/<slug> the admin defines in the eyoom menu.
import { notFound } from 'next/navigation';
const GAMES: Record<string, { title: string; emoji: string; subtitle: string }> = {
bacara: { title: '포인트 바카라', emoji: '🃏', subtitle: '포인트로 즐기는 무료 바카라' },
fortunes: { title: '88 포춘', emoji: '🎰', subtitle: 'Lightning 잭팟의 클래식' },
fivetreasures: { title: '5 트레저', emoji: '💎', subtitle: '5종 보물 슬롯 시뮬레이터' },
seastory: { title: '바다이야기', emoji: '🐚', subtitle: '추억의 슬롯' },
davinci: { title: '다빈치', emoji: '🎨', subtitle: '르네상스 보물 사냥' },
oceanparadise: { title: '오션 파라다이스', emoji: '🐠', subtitle: '바다의 잭팟' },
cherrymaster: { title: '체리마스터', emoji: '🍒', subtitle: '클래식 체리 릴' },
yamato: { title: '야마토', emoji: '🚀', subtitle: '우주전함 야마토' },
kyoushi: { title: '강시', emoji: '🧟', subtitle: '강시 슬롯' },
lupin: { title: '루팡', emoji: '🕵️', subtitle: '괴도 루팡' },
taiku: { title: '대공', emoji: '🛡️', subtitle: '대공 슬롯' },
matsuri: { title: '축제', emoji: '🎏', subtitle: '일본 마츠리' },
marilyn: { title: '마릴린먼로', emoji: '💋', subtitle: '마릴린 모티프 슬롯' },
giatrus: { title: '고인돌', emoji: '🦴', subtitle: '원시인 가족' },
rings: { title: '반지의 제왕', emoji: '💍', subtitle: '판타지 어드벤처' },
bakabon: { title: '바카본', emoji: '👶', subtitle: '레트로 슬롯' },
slot: { title: '무료 슬롯 체험', emoji: '🎲', subtitle: '여러 슬롯을 무료 체험' },
roulette: { title: '일일미션 룰렛', emoji: '🎡', subtitle: '하루 1회 무료 회전' },
ranking: { title: '포인트게임 랭킹', emoji: '🏆', subtitle: '게임별 상위 랭커' },
activityrank: { title: '활동 랭킹', emoji: '⚡', subtitle: '글·댓글·추천 활동 랭킹' },
muktirank: { title: '먹튀랭크', emoji: '⚠️', subtitle: '먹튀 신고 누적 랭킹' },
};
export default async function GamePage({ params }: { params: Promise<{ game: string }> }) {
const { game } = await params;
const meta = GAMES[game];
if (!meta) return notFound();
return (
<article className="space-y-6">
<header className="overflow-hidden rounded-3xl bg-gradient-to-br from-brand-700 via-brand-600 to-fuchsia-600 p-8 text-white shadow-[0_18px_38px_rgba(60,30,120,0.25)]">
<div className="flex flex-col items-start gap-3 md:flex-row md:items-center md:justify-between">
<div>
<span className="inline-flex items-center gap-1 rounded-full bg-white/15 px-3 py-1 text-[11px] font-bold tracking-widest backdrop-blur"></span>
<h1 className="mt-3 text-3xl font-extrabold tracking-tight md:text-4xl">{meta.emoji} {meta.title}</h1>
<p className="mt-2 text-[15px] text-white/85">{meta.subtitle}</p>
</div>
<a href={`/games/${game}/play`} className="rounded-full bg-white px-6 py-3 text-[14px] font-extrabold text-brand-700 shadow-[0_8px_22px_rgba(255,255,255,0.25)] hover:bg-brand-50">
</a>
</div>
</header>
<section className="grid gap-4 md:grid-cols-3">
<Card title="내 포인트" value="—" sub="로그인 후 확인" />
<Card title="오늘 베팅" value="0회" sub="실시간 누적" />
<Card title="당일 수익" value="0p" sub="누적 합계" />
</section>
<section className="rounded-2xl bg-white p-6 ring-1 ring-neutral-100">
<h2 className="mb-3 text-[16px] font-bold"> </h2>
<p className="text-[14px] leading-relaxed text-neutral-text-soft">
. .
<a href="/guide" className="text-brand-700 underline"> </a> .
</p>
</section>
</article>
);
}
function Card({ title, value, sub }: { title: string; value: string; sub: string }) {
return (
<div className="rounded-2xl bg-white p-5 ring-1 ring-neutral-100">
<p className="m-0 text-[12px] text-neutral-text-soft">{title}</p>
<p className="m-0 mt-1 text-2xl font-extrabold tracking-tight">{value}</p>
<p className="m-0 mt-0.5 text-[11px] text-neutral-text-soft">{sub}</p>
</div>
);
}
@@ -1,16 +0,0 @@
import { StubPage, FeatureGrid } from '@/lib/page-shells';
export default function Page() {
return (
<StubPage title="포인트 바카라" badge="베타" lead="회원 보유 포인트로 즐기는 무료 게임입니다. 실제 현금 베팅이 아닙니다.">
<div style={{ background: 'var(--color-bgSurface)', border: '1px solid var(--color-border)', borderRadius: 8, padding: 40, textAlign: 'center' }}>
<p style={{ fontSize: 60, margin: 0 }}>🃏</p>
<p style={{ color: 'var(--color-textMuted)', margin: '12px 0' }}> M3 React + WebSocket .</p>
<FeatureGrid items={[
{ emoji: '🏆', label: '랭킹 보기', href: '/games/bacara/rank' },
{ emoji: '📜', label: '베팅 내역', href: '/games/bacara/history' },
{ emoji: '📖', label: '게임 가이드', href: '/guide/pointgame' },
]} />
</div>
</StubPage>
);
}
@@ -1,16 +0,0 @@
import { StubPage, FeatureGrid } from '@/lib/page-shells';
export default function Page() {
return (
<StubPage title="5 트레저" badge="베타" lead="회원 보유 포인트로 즐기는 무료 게임입니다. 실제 현금 베팅이 아닙니다.">
<div style={{ background: 'var(--color-bgSurface)', border: '1px solid var(--color-border)', borderRadius: 8, padding: 40, textAlign: 'center' }}>
<p style={{ fontSize: 60, margin: 0 }}>💎</p>
<p style={{ color: 'var(--color-textMuted)', margin: '12px 0' }}> M3 React + WebSocket .</p>
<FeatureGrid items={[
{ emoji: '🏆', label: '랭킹 보기', href: '/games/fivetreasures/rank' },
{ emoji: '📜', label: '베팅 내역', href: '/games/fivetreasures/history' },
{ emoji: '📖', label: '게임 가이드', href: '/guide/pointgame' },
]} />
</div>
</StubPage>
);
}
@@ -1,16 +0,0 @@
import { StubPage, FeatureGrid } from '@/lib/page-shells';
export default function Page() {
return (
<StubPage title="88 포춘" badge="베타" lead="회원 보유 포인트로 즐기는 무료 게임입니다. 실제 현금 베팅이 아닙니다.">
<div style={{ background: 'var(--color-bgSurface)', border: '1px solid var(--color-border)', borderRadius: 8, padding: 40, textAlign: 'center' }}>
<p style={{ fontSize: 60, margin: 0 }}>🎰</p>
<p style={{ color: 'var(--color-textMuted)', margin: '12px 0' }}> M3 React + WebSocket .</p>
<FeatureGrid items={[
{ emoji: '🏆', label: '랭킹 보기', href: '/games/fortunes/rank' },
{ emoji: '📜', label: '베팅 내역', href: '/games/fortunes/history' },
{ emoji: '📖', label: '게임 가이드', href: '/guide/pointgame' },
]} />
</div>
</StubPage>
);
}
@@ -0,0 +1,15 @@
import { Sparkles } from 'lucide-react';
export default function Page() {
return (
<article className="space-y-6">
<header className="overflow-hidden rounded-3xl bg-gradient-to-br from-brand-700 to-fuchsia-600 p-8 text-white shadow-[0_18px_38px_rgba(60,30,120,0.25)]">
<span className="inline-flex items-center gap-1 rounded-full bg-white/15 px-3 py-1 text-[11px] font-bold tracking-widest backdrop-blur"><Sparkles size={12} /> </span>
<h1 className="mt-3 text-3xl font-extrabold tracking-tight md:text-4xl"> [1]</h1>
<p className="mt-2 text-[15px] text-white/85">1 </p>
</header>
<section className="rounded-2xl bg-white p-6 ring-1 ring-neutral-100">
<p className="text-[14px] leading-relaxed text-neutral-text-soft"> . (M3) (WebSocket) .</p>
</section>
</article>
);
}
@@ -0,0 +1,15 @@
import { Sparkles } from 'lucide-react';
export default function Page() {
return (
<article className="space-y-6">
<header className="overflow-hidden rounded-3xl bg-gradient-to-br from-brand-700 to-fuchsia-600 p-8 text-white shadow-[0_18px_38px_rgba(60,30,120,0.25)]">
<span className="inline-flex items-center gap-1 rounded-full bg-white/15 px-3 py-1 text-[11px] font-bold tracking-widest backdrop-blur"><Sparkles size={12} /> </span>
<h1 className="mt-3 text-3xl font-extrabold tracking-tight md:text-4xl"> [1]</h1>
<p className="mt-2 text-[15px] text-white/85">1 </p>
</header>
<section className="rounded-2xl bg-white p-6 ring-1 ring-neutral-100">
<p className="text-[14px] leading-relaxed text-neutral-text-soft"> . (M3) (WebSocket) .</p>
</section>
</article>
);
}
@@ -1,25 +0,0 @@
import { StubPage } from '@/lib/page-shells';
import { getMemberRankings } from '@/lib/page-data';
export const dynamic = 'force-dynamic';
export default async function Page() {
const ranks = await getMemberRankings();
return (
<StubPage title="회원 랭킹" lead="포인트 보유량 기준 상위 회원입니다.">
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead><tr style={{ background: 'var(--color-bgSurface)', borderTop: '2px solid var(--color-primary)' }}>
<th style={{ padding: 12 }}></th><th style={{ padding: 12, textAlign: 'left' }}></th><th></th><th></th>
</tr></thead>
<tbody>
{ranks.map((r) => (
<tr key={r.rank} style={{ borderBottom: '1px solid var(--color-border)' }}>
<td style={{ padding: 12, textAlign: 'center', fontWeight: 700, color: r.rank <= 3 ? 'var(--color-primary)' : 'inherit' }}>{r.rank <= 3 ? ['🥇','🥈','🥉'][r.rank-1] : r.rank}</td>
<td style={{ padding: 12 }}>{r.nick}</td>
<td style={{ padding: 12, textAlign: 'center' }}>Lv.{r.level}</td>
<td style={{ padding: 12, textAlign: 'right' }}>{r.point.toLocaleString()}p</td>
</tr>
))}
</tbody>
</table>
</StubPage>
);
}
@@ -1,16 +0,0 @@
import { StubPage, FeatureGrid } from '@/lib/page-shells';
export default function Page() {
return (
<StubPage title="룰렛 게임" badge="베타" lead="회원 보유 포인트로 즐기는 무료 게임입니다. 실제 현금 베팅이 아닙니다.">
<div style={{ background: 'var(--color-bgSurface)', border: '1px solid var(--color-border)', borderRadius: 8, padding: 40, textAlign: 'center' }}>
<p style={{ fontSize: 60, margin: 0 }}>🎡</p>
<p style={{ color: 'var(--color-textMuted)', margin: '12px 0' }}> M3 React + WebSocket .</p>
<FeatureGrid items={[
{ emoji: '🏆', label: '랭킹 보기', href: '/games/roulette/rank' },
{ emoji: '📜', label: '베팅 내역', href: '/games/roulette/history' },
{ emoji: '📖', label: '게임 가이드', href: '/guide/pointgame' },
]} />
</div>
</StubPage>
);
}
@@ -1,16 +0,0 @@
import { StubPage, FeatureGrid } from '@/lib/page-shells';
export default function Page() {
return (
<StubPage title="무료 슬롯 체험" badge="베타" lead="회원 보유 포인트로 즐기는 무료 게임입니다. 실제 현금 베팅이 아닙니다.">
<div style={{ background: 'var(--color-bgSurface)', border: '1px solid var(--color-border)', borderRadius: 8, padding: 40, textAlign: 'center' }}>
<p style={{ fontSize: 60, margin: 0 }}>🎲</p>
<p style={{ color: 'var(--color-textMuted)', margin: '12px 0' }}> M3 React + WebSocket .</p>
<FeatureGrid items={[
{ emoji: '🏆', label: '랭킹 보기', href: '/games/slot/rank' },
{ emoji: '📜', label: '베팅 내역', href: '/games/slot/history' },
{ emoji: '📖', label: '게임 가이드', href: '/guide/pointgame' },
]} />
</div>
</StubPage>
);
}
@@ -0,0 +1,15 @@
import { Sparkles } from 'lucide-react';
export default function Page() {
return (
<article className="space-y-6">
<header className="overflow-hidden rounded-3xl bg-gradient-to-br from-brand-700 to-fuchsia-600 p-8 text-white shadow-[0_18px_38px_rgba(60,30,120,0.25)]">
<span className="inline-flex items-center gap-1 rounded-full bg-white/15 px-3 py-1 text-[11px] font-bold tracking-widest backdrop-blur"><Sparkles size={12} /> </span>
<h1 className="mt-3 text-3xl font-extrabold tracking-tight md:text-4xl"></h1>
<p className="mt-2 text-[15px] text-white/85"> </p>
</header>
<section className="rounded-2xl bg-white p-6 ring-1 ring-neutral-100">
<p className="text-[14px] leading-relaxed text-neutral-text-soft"> . (M3) (WebSocket) .</p>
</section>
</article>
);
}
@@ -0,0 +1,15 @@
import { Sparkles } from 'lucide-react';
export default function Page() {
return (
<article className="space-y-6">
<header className="overflow-hidden rounded-3xl bg-gradient-to-br from-brand-700 to-fuchsia-600 p-8 text-white shadow-[0_18px_38px_rgba(60,30,120,0.25)]">
<span className="inline-flex items-center gap-1 rounded-full bg-white/15 px-3 py-1 text-[11px] font-bold tracking-widest backdrop-blur"><Sparkles size={12} /> </span>
<h1 className="mt-3 text-3xl font-extrabold tracking-tight md:text-4xl"></h1>
<p className="mt-2 text-[15px] text-white/85"> </p>
</header>
<section className="rounded-2xl bg-white p-6 ring-1 ring-neutral-100">
<p className="text-[14px] leading-relaxed text-neutral-text-soft"> . (M3) (WebSocket) .</p>
</section>
</article>
);
}
+88
View File
@@ -0,0 +1,88 @@
@import "tailwindcss";
@theme {
--font-sans: "Pretendard", "Noto Sans KR", system-ui, -apple-system, sans-serif;
/* Brand colors */
--color-brand-50: #f5f3ff;
--color-brand-100: #ede9fe;
--color-brand-200: #ddd6fe;
--color-brand-300: #c4b5fd;
--color-brand-400: #a78bfa;
--color-brand-500: #8b5cf6;
--color-brand-600: #7c3aed;
--color-brand-700: #6d28d9;
--color-brand-800: #5b21b6;
--color-brand-900: #4c1d95;
--color-neutral-bg: #f7f5fb;
--color-neutral-surface: #ffffff;
--color-neutral-text: #14111f;
--color-neutral-text-soft: #5d5677;
--color-neutral-border: #e8e3f4;
--shadow-card: 0 1px 2px rgba(124, 82, 224, 0.04), 0 8px 24px rgba(124, 82, 224, 0.08);
--shadow-pop: 0 12px 32px rgba(124, 82, 224, 0.18);
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
font-family: var(--font-sans);
color: var(--color-neutral-text);
background: var(--color-neutral-bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* { box-sizing: border-box; }
a { color: inherit; }
details > summary { list-style: none; cursor: pointer; }
details > summary::-webkit-details-marker { display: none; }
/* Subtle marquee animation for the headline ticker */
@keyframes ticker-scroll {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
.ticker-track {
display: inline-flex;
gap: 3rem;
white-space: nowrap;
animation: ticker-scroll 45s linear infinite;
}
.ticker-track:hover { animation-play-state: paused; }
/* Floating card hover */
.lift {
transition: transform 200ms ease, box-shadow 200ms ease;
}
.lift:hover { transform: translateY(-2px); box-shadow: var(--shadow-pop); }
/* Pretty scrollbar */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-thumb { background: #c4b5fd66; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #a78bfa; }
/* Gradient utilities used by hero */
.bg-brand-radial {
background:
radial-gradient(circle at 20% 0%, #b794f4 0%, transparent 40%),
radial-gradient(circle at 80% 100%, #6c4cd1 0%, transparent 50%),
linear-gradient(180deg, #2c1d57 0%, #1c133a 100%);
}
.bg-mega {
background:
linear-gradient(90deg, #6c4cd1 0%, #8a5cd6 50%, #a47adf 100%);
}
/* Card link */
.card-link { text-decoration: none; color: inherit; }
/* Marquee row container */
.marquee {
overflow: hidden;
position: relative;
mask-image: linear-gradient(90deg, transparent 0, #000 5%, #000 95%, transparent 100%);
}
@@ -0,0 +1,5 @@
import BoardPage from '@/app/[boardSlug]/page';
export const dynamic = 'force-dynamic';
export default async function Page({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
return <BoardPage params={Promise.resolve({ boardSlug: 'interrogation' })} searchParams={searchParams} />;
}
+25 -24
View File
@@ -1,8 +1,11 @@
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import { getThemeForPath } from '@/lib/theme'; import { getCurrentSiteUser, getCurrentPathname, getPopularTags, getMemberRankings } from '@/lib/page-data';
import { tokensToCssVars, type ThemeId } from '@slot/themes'; import { fetchMegaMenusFromDb } from '@/lib/menu-from-db';
import { getCurrentSiteUser, getCurrentPathname, getPopularTags, getMemberRankings, MEGA_MENUS } from '@/lib/page-data'; import Header from '@/components/Chrome/Header';
import Sidebar from '@/components/Chrome/Sidebar';
import Footer from '@/components/Chrome/Footer';
import './globals.css';
export const metadata: Metadata = { export const metadata: Metadata = {
title: '슬생닷컴 | 안전 슬롯사이트 추천 및 슬롯커뮤니티', title: '슬생닷컴 | 안전 슬롯사이트 추천 및 슬롯커뮤니티',
@@ -12,37 +15,35 @@ export const metadata: Metadata = {
export default async function RootLayout({ children }: { children: React.ReactNode }) { export default async function RootLayout({ children }: { children: React.ReactNode }) {
const pathname = await getCurrentPathname(); const pathname = await getCurrentPathname();
const c = await cookies(); const c = await cookies();
const cookieTheme = c.get('slot_theme_pref')?.value as ThemeId | undefined;
const user = await getCurrentSiteUser();
const theme = await getThemeForPath(pathname, cookieTheme ?? (user?.themePref as ThemeId | undefined));
const [popularTags, rankings] = await Promise.all([getPopularTags(), getMemberRankings()]);
const Root = theme.layouts.root;
const cssVars = tokensToCssVars(theme);
const isDark = c.get('slot_dark')?.value === '1'; const isDark = c.get('slot_dark')?.value === '1';
const [user, popularTags, rankings, menus] = await Promise.all([
getCurrentSiteUser(),
getPopularTags(),
getMemberRankings(),
fetchMegaMenusFromDb(),
]);
const hideEverything = pathname === '/login'; const hideEverything = pathname === '/login';
const hideSidebar = pathname.startsWith('/admin') || pathname.startsWith('/mypage') || pathname === '/login' || pathname === '/register'; const hideSidebar = pathname.startsWith('/admin') || pathname.startsWith('/mypage') || pathname === '/login' || pathname === '/register';
const showHeader = !hideEverything;
const showFooter = !hideEverything;
return ( return (
<html lang="ko" data-theme={theme.id}> <html lang="ko" className={isDark ? 'dark' : ''}>
<head>
<style dangerouslySetInnerHTML={{ __html: `:root{${cssVars}${isDark ? ';--color-bg:#0e0f12;--color-bgSurface:#16181d;--color-text:#e6e6e6;--color-textMuted:#9aa0a6;--color-border:#2a2d34' : ''}}body{margin:0;padding:0}*{box-sizing:border-box}a{color:inherit}details>summary::-webkit-details-marker{display:none}` }} />
</head>
<body> <body>
<Root> {!hideEverything && <Header user={user} menus={menus} isDark={isDark} />}
{showHeader && ( <div className="mx-auto max-w-[1280px] px-6 py-6">
<theme.slots.Header activeTheme={theme.id} siteName="슬생닷컴" user={user} bookmarked={false} menus={MEGA_MENUS} /> <div className={hideSidebar ? '' : 'grid items-start gap-6 lg:grid-cols-[minmax(0,1fr)_300px]'}>
)} <main className="min-h-[60vh] min-w-0">{children}</main>
<div style={hideSidebar ? { padding: '20px 24px' } : { display: 'grid', gridTemplateColumns: '1fr 320px', gap: 20, padding: '20px 24px', alignItems: 'start' }}>
<main style={{ minHeight: '60vh', minWidth: 0 }}>{children}</main>
{!hideSidebar && ( {!hideSidebar && (
<theme.slots.Sidebar activeTheme={theme.id} user={user} popularTags={popularTags} rankings={rankings} /> <Sidebar
user={user}
popularTags={popularTags}
rankings={rankings}
visitors={{ today: 1234, yesterday: 5678, max: 12345, total: 4_566_650 }}
/>
)} )}
</div> </div>
{showFooter && <theme.slots.Footer activeTheme={theme.id} siteName="슬생닷컴" />} </div>
</Root> {!hideEverything && <Footer />}
</body> </body>
</html> </html>
); );
@@ -0,0 +1,5 @@
import BoardPage from '@/app/[boardSlug]/page';
export const dynamic = 'force-dynamic';
export default async function Page({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
return <BoardPage params={Promise.resolve({ boardSlug: 'lottery' })} searchParams={searchParams} />;
}
@@ -0,0 +1,5 @@
import BoardPage from '@/app/[boardSlug]/page';
export const dynamic = 'force-dynamic';
export default async function Page({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
return <BoardPage params={Promise.resolve({ boardSlug: 'newsite' })} searchParams={searchParams} />;
}
+12 -8
View File
@@ -1,13 +1,17 @@
import { getThemeForPath } from '@/lib/theme'; import { getIndexProps } from '@/lib/page-data';
import { getCurrentPathname, getIndexProps } from '@/lib/page-data'; import Hero from '@/components/home/Hero';
import QuickAccess from '@/components/home/QuickAccess';
import BoardSlots from '@/components/home/BoardSlots';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
export default async function HomePage() { export default async function HomePage() {
const [theme, props] = await Promise.all([ const props = await getIndexProps();
getThemeForPath(await getCurrentPathname()), return (
getIndexProps(), <div className="flex flex-col gap-8">
]); <Hero headlines={props.headlines} kickStatus={props.kickStatus} />
const IndexHome = theme.slots.IndexHome; <QuickAccess />
return <IndexHome {...props} />; <BoardSlots boards={props.featuredBoards} />
</div>
);
} }
@@ -0,0 +1,5 @@
import BoardPage from '@/app/[boardSlug]/page';
export const dynamic = 'force-dynamic';
export default async function Page({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
return <BoardPage params={Promise.resolve({ boardSlug: 'plugin' })} searchParams={searchParams} />;
}
@@ -0,0 +1,5 @@
import BoardPage from '@/app/[boardSlug]/page';
export const dynamic = 'force-dynamic';
export default async function Page({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
return <BoardPage params={Promise.resolve({ boardSlug: 'report' })} searchParams={searchParams} />;
}
@@ -0,0 +1,15 @@
import { Sparkles } from 'lucide-react';
export default function Page() {
return (
<article className="space-y-6">
<header className="overflow-hidden rounded-3xl bg-gradient-to-br from-brand-700 to-fuchsia-600 p-8 text-white shadow-[0_18px_38px_rgba(60,30,120,0.25)]">
<span className="inline-flex items-center gap-1 rounded-full bg-white/15 px-3 py-1 text-[11px] font-bold tracking-widest backdrop-blur"><Sparkles size={12} /> </span>
<h1 className="mt-3 text-3xl font-extrabold tracking-tight md:text-4xl"></h1>
<p className="mt-2 text-[15px] text-white/85"> </p>
</header>
<section className="rounded-2xl bg-white p-6 ring-1 ring-neutral-100">
<p className="text-[14px] leading-relaxed text-neutral-text-soft"> . (M3) (WebSocket) .</p>
</section>
</article>
);
}
@@ -0,0 +1,15 @@
import { Sparkles } from 'lucide-react';
export default function Page() {
return (
<article className="space-y-6">
<header className="overflow-hidden rounded-3xl bg-gradient-to-br from-brand-700 to-fuchsia-600 p-8 text-white shadow-[0_18px_38px_rgba(60,30,120,0.25)]">
<span className="inline-flex items-center gap-1 rounded-full bg-white/15 px-3 py-1 text-[11px] font-bold tracking-widest backdrop-blur"><Sparkles size={12} /> </span>
<h1 className="mt-3 text-3xl font-extrabold tracking-tight md:text-4xl"> </h1>
<p className="mt-2 text-[15px] text-white/85"> .</p>
</header>
<section className="rounded-2xl bg-white p-6 ring-1 ring-neutral-100">
<p className="text-[14px] leading-relaxed text-neutral-text-soft"> . (M3) (WebSocket) .</p>
</section>
</article>
);
}
@@ -0,0 +1,15 @@
import { Sparkles } from 'lucide-react';
export default function Page() {
return (
<article className="space-y-6">
<header className="overflow-hidden rounded-3xl bg-gradient-to-br from-brand-700 to-fuchsia-600 p-8 text-white shadow-[0_18px_38px_rgba(60,30,120,0.25)]">
<span className="inline-flex items-center gap-1 rounded-full bg-white/15 px-3 py-1 text-[11px] font-bold tracking-widest backdrop-blur"><Sparkles size={12} /> </span>
<h1 className="mt-3 text-3xl font-extrabold tracking-tight md:text-4xl"> </h1>
<p className="mt-2 text-[15px] text-white/85"> </p>
</header>
<section className="rounded-2xl bg-white p-6 ring-1 ring-neutral-100">
<p className="text-[14px] leading-relaxed text-neutral-text-soft"> . (M3) (WebSocket) .</p>
</section>
</article>
);
}
@@ -0,0 +1,15 @@
import { Sparkles } from 'lucide-react';
export default function Page() {
return (
<article className="space-y-6">
<header className="overflow-hidden rounded-3xl bg-gradient-to-br from-brand-700 to-fuchsia-600 p-8 text-white shadow-[0_18px_38px_rgba(60,30,120,0.25)]">
<span className="inline-flex items-center gap-1 rounded-full bg-white/15 px-3 py-1 text-[11px] font-bold tracking-widest backdrop-blur"><Sparkles size={12} /> </span>
<h1 className="mt-3 text-3xl font-extrabold tracking-tight md:text-4xl"> </h1>
<p className="mt-2 text-[15px] text-white/85"> </p>
</header>
<section className="rounded-2xl bg-white p-6 ring-1 ring-neutral-100">
<p className="text-[14px] leading-relaxed text-neutral-text-soft"> . (M3) (WebSocket) .</p>
</section>
</article>
);
}
@@ -0,0 +1,15 @@
import { Sparkles } from 'lucide-react';
export default function Page() {
return (
<article className="space-y-6">
<header className="overflow-hidden rounded-3xl bg-gradient-to-br from-brand-700 to-fuchsia-600 p-8 text-white shadow-[0_18px_38px_rgba(60,30,120,0.25)]">
<span className="inline-flex items-center gap-1 rounded-full bg-white/15 px-3 py-1 text-[11px] font-bold tracking-widest backdrop-blur"><Sparkles size={12} /> </span>
<h1 className="mt-3 text-3xl font-extrabold tracking-tight md:text-4xl"> </h1>
<p className="mt-2 text-[15px] text-white/85"> / </p>
</header>
<section className="rounded-2xl bg-white p-6 ring-1 ring-neutral-100">
<p className="text-[14px] leading-relaxed text-neutral-text-soft"> . (M3) (WebSocket) .</p>
</section>
</article>
);
}
@@ -0,0 +1,15 @@
import { Sparkles } from 'lucide-react';
export default function Page() {
return (
<article className="space-y-6">
<header className="overflow-hidden rounded-3xl bg-gradient-to-br from-brand-700 to-fuchsia-600 p-8 text-white shadow-[0_18px_38px_rgba(60,30,120,0.25)]">
<span className="inline-flex items-center gap-1 rounded-full bg-white/15 px-3 py-1 text-[11px] font-bold tracking-widest backdrop-blur"><Sparkles size={12} /> </span>
<h1 className="mt-3 text-3xl font-extrabold tracking-tight md:text-4xl"> </h1>
<p className="mt-2 text-[15px] text-white/85"> / </p>
</header>
<section className="rounded-2xl bg-white p-6 ring-1 ring-neutral-100">
<p className="text-[14px] leading-relaxed text-neutral-text-soft"> . (M3) (WebSocket) .</p>
</section>
</article>
);
}
@@ -0,0 +1,15 @@
import { Sparkles } from 'lucide-react';
export default function Page() {
return (
<article className="space-y-6">
<header className="overflow-hidden rounded-3xl bg-gradient-to-br from-brand-700 to-fuchsia-600 p-8 text-white shadow-[0_18px_38px_rgba(60,30,120,0.25)]">
<span className="inline-flex items-center gap-1 rounded-full bg-white/15 px-3 py-1 text-[11px] font-bold tracking-widest backdrop-blur"><Sparkles size={12} /> </span>
<h1 className="mt-3 text-3xl font-extrabold tracking-tight md:text-4xl"> </h1>
<p className="mt-2 text-[15px] text-white/85"> </p>
</header>
<section className="rounded-2xl bg-white p-6 ring-1 ring-neutral-100">
<p className="text-[14px] leading-relaxed text-neutral-text-soft"> . (M3) (WebSocket) .</p>
</section>
</article>
);
}
@@ -0,0 +1,47 @@
import Link from 'next/link';
import { Send } from 'lucide-react';
export default function Footer() {
return (
<footer className="mt-12 bg-gradient-to-b from-[#1f1535] to-[#13092b] text-neutral-300">
<div className="mx-auto grid max-w-[1280px] gap-8 px-6 py-12 md:grid-cols-[2fr_1fr_1fr_1fr]">
<div>
<div className="flex items-center gap-2">
<span className="rounded-lg bg-gradient-to-br from-[#7c52e0] to-[#b794f4] px-3 py-1.5 text-lg font-extrabold text-white"> </span>
<span className="rounded-md bg-amber-300 px-2 py-0.5 text-[10px] font-extrabold text-amber-900"></span>
</div>
<p className="mt-3 text-[13px] leading-relaxed text-neutral-400"> .</p>
<a href="https://t.me/slotlifeCS" target="_blank" rel="noreferrer" className="mt-3 inline-flex items-center gap-1.5 rounded-full bg-[#229ED9]/15 px-3 py-1.5 text-[12px] text-[#9bd6f4] hover:bg-[#229ED9]/30">
<Send size={13} /> @slotlifeCS
</a>
</div>
<FooterCol title="커뮤니티" links={[['/free', '자유게시판'], ['/review', '후기게시판'], ['/humor', '유머/이슈'], ['/notice', '공지사항']]} />
<FooterCol title="검증/이벤트" links={[['/mukti', '먹튀사이트'], ['/fakesite', '가품사이트'], ['/event', '이벤트'], ['/lottery_ticket', '슬생복권']]} />
<FooterCol title="고객지원" links={[['/help/qa', '1:1문의'], ['/help/faq', 'FAQ'], ['/page/aboutus', '사이트소개'], ['/page/manual', '이용안내']]} />
</div>
<div className="border-t border-white/5">
<div className="mx-auto flex max-w-[1280px] flex-wrap items-center justify-between gap-3 px-6 py-4 text-[12px] text-neutral-500">
<p className="m-0">© {new Date().getFullYear()} All rights reserved.</p>
<div className="flex flex-wrap gap-4">
<Link href="/page/provision" className="hover:text-white"></Link>
<Link href="/page/privacy" className="font-bold text-white hover:text-brand-300"></Link>
<Link href="/page/noemail" className="hover:text-white"></Link>
</div>
</div>
</div>
</footer>
);
}
function FooterCol({ title, links }: { title: string; links: [string, string][] }) {
return (
<div>
<h4 className="mb-2 text-[14px] font-semibold text-white">{title}</h4>
<ul className="m-0 grid gap-1 p-0 list-none">
{links.map(([href, label]) => (
<li key={href}><Link href={href} className="text-[13px] text-neutral-400 hover:text-white">{label}</Link></li>
))}
</ul>
</div>
);
}
@@ -0,0 +1,192 @@
'use client';
import Link from 'next/link';
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Bookmark, UserPlus, ShoppingBag, Plus, LogIn, MessageCircle, Mail, Moon, Menu as MenuIcon, ChevronDown, Search,
} from 'lucide-react';
import type { MenuItem, SiteUser } from '@slot/themes';
export interface HeaderProps {
user: SiteUser | null;
menus: MenuItem[];
isDark: boolean;
}
export default function Header({ user, menus, isDark }: HeaderProps) {
return (
<header className="sticky top-0 z-30 bg-white/85 backdrop-blur-md shadow-[0_1px_0_rgba(0,0,0,0.04)]">
<UtilityBar user={user} />
<BrandRow user={user} />
<MegaNav menus={menus} isDark={isDark} />
</header>
);
}
function UtilityBar({ user }: { user: SiteUser | null }) {
const [open, setOpen] = useState(false);
return (
<div className="border-b border-neutral-100 text-[12px]">
<div className="mx-auto max-w-[1280px] flex items-center justify-between px-6 py-2">
<Link href="/bookmarks" className="inline-flex items-center gap-1 text-neutral-text-soft hover:text-brand-600">
<Bookmark size={13} />
</Link>
<div className="flex items-center gap-5 text-neutral-text-soft">
{user ? (
<Link href="/mypage" className="hover:text-brand-600">👤 {user.nick}</Link>
) : (
<Link href="/register" className="inline-flex items-center gap-1 hover:text-brand-600"><UserPlus size={13} /> </Link>
)}
{user ? null : (
<Link href="/login" className="inline-flex items-center gap-1 hover:text-brand-600"><LogIn size={13} /> </Link>
)}
<Link href="/shop/orderinquiry" className="inline-flex items-center gap-1 hover:text-brand-600"><ShoppingBag size={13} /> </Link>
<div className="relative" onMouseEnter={() => setOpen(true)} onMouseLeave={() => setOpen(false)}>
<button className="inline-flex items-center gap-1 hover:text-brand-600">
<Plus size={13} /> <ChevronDown size={12} />
</button>
<AnimatePresence>
{open && (
<motion.div
initial={{ opacity: 0, y: -6 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -6 }}
className="absolute right-0 top-full mt-2 w-44 rounded-xl border border-neutral-100 bg-white p-1.5 shadow-[0_12px_32px_rgba(124,82,224,0.18)]"
>
<DropLink href="/new"></DropLink>
<DropLink href="/help/faq"> </DropLink>
<DropLink href="/help/qa">1:1문의</DropLink>
{user && <DropLink href="/mypage/profile"></DropLink>}
{user && (
<form action="/api/auth/logout" method="POST" className="m-0">
<button type="submit" className="block w-full rounded-lg px-3 py-2 text-left text-[13px] text-rose-600 hover:bg-rose-50"></button>
</form>
)}
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
</div>
);
}
function DropLink({ href, children }: { href: string; children: React.ReactNode }) {
return (
<Link href={href} className="block rounded-lg px-3 py-2 text-[13px] text-neutral-700 hover:bg-brand-50 hover:text-brand-700">
{children}
</Link>
);
}
function BrandRow({ user }: { user: SiteUser | null }) {
return (
<div className="bg-gradient-to-b from-white to-[#fbfaff]">
<div className="mx-auto flex max-w-[1280px] items-center justify-between gap-4 px-6 py-4">
<Link href="/" className="flex items-center gap-2 no-underline">
<span className="rounded-xl bg-gradient-to-br from-[#7c52e0] to-[#b794f4] px-4 py-2.5 text-2xl font-extrabold leading-none text-white shadow-[0_4px_14px_rgba(124,82,224,0.35)]"> </span>
<span className="rounded-md bg-amber-300 px-2 py-1 text-[11px] font-extrabold tracking-wide text-amber-900"></span>
</Link>
<form className="hidden flex-1 max-w-[420px] md:flex" action="/free/search" method="GET">
<div className="flex w-full items-center gap-2 rounded-full border border-brand-100 bg-white pl-4 pr-1.5 py-1.5 shadow-sm focus-within:border-brand-400 focus-within:shadow-[0_0_0_4px_rgba(167,139,250,0.18)]">
<Search size={16} className="text-neutral-400" />
<input name="stx" placeholder="게시판 검색…" className="flex-1 bg-transparent text-[13px] outline-none placeholder:text-neutral-400" />
<button className="rounded-full bg-brand-600 px-4 py-1.5 text-[12px] font-semibold text-white hover:bg-brand-700"></button>
</div>
</form>
<div className="flex items-center gap-5">
{user ? (
<>
<IconLink href="/mypage" emoji="👤" label={user.nick} />
<IconLink href="/mypage/respond" Icon={MessageCircle} label="내글반응" badge={user.respondCount} />
<IconLink href="/memo" Icon={Mail} label="쪽지" badge={user.memoCount} />
</>
) : (
<>
<IconLink href="/login" Icon={LogIn} label="로그인" />
<IconLink href="/login" Icon={MessageCircle} label="내글반응" badge={0} />
<IconLink href="/login" Icon={Mail} label="쪽지" badge={0} />
</>
)}
</div>
</div>
</div>
);
}
function IconLink({ href, Icon, emoji, label, badge }: { href: string; Icon?: React.ComponentType<{ size?: number; className?: string }>; emoji?: string; label: string; badge?: number }) {
return (
<Link href={href} className="group relative flex min-w-[56px] flex-col items-center gap-1 text-[11px] text-neutral-text-soft transition hover:text-brand-700">
<span className="relative grid h-9 w-9 place-items-center rounded-full bg-brand-50 group-hover:bg-brand-100">
{emoji ? <span className="text-base">{emoji}</span> : Icon ? <Icon size={18} /> : null}
{typeof badge === 'number' && (
<span className="absolute -top-1 -right-1 grid h-4 min-w-4 place-items-center rounded-full bg-rose-500 px-1 text-[10px] font-bold text-white">{badge}</span>
)}
</span>
<span>{label}</span>
</Link>
);
}
function MegaNav({ menus, isDark }: { menus: MenuItem[]; isDark: boolean }) {
const [open, setOpen] = useState<number | null>(null);
return (
<nav className="bg-mega text-white shadow-[0_4px_14px_rgba(108,76,209,0.35)]">
<div className="relative mx-auto flex max-w-[1280px] items-center gap-1 px-6">
{menus.map((m, i) => (
<div
key={m.label + i}
className="relative"
onMouseEnter={() => setOpen(i)}
onMouseLeave={() => setOpen((v) => (v === i ? null : v))}
>
<Link
href={m.href}
className="flex items-center gap-1.5 px-4 py-3.5 text-[14px] font-semibold tracking-tight hover:bg-white/10"
>
{m.icon && <span aria-hidden>{m.icon}</span>}
<span>{m.label}</span>
{m.children && m.children.length > 0 && <ChevronDown size={14} className="opacity-80" />}
</Link>
<AnimatePresence>
{open === i && m.children && m.children.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 6 }} transition={{ duration: 0.16 }}
className="absolute left-0 top-full z-40 mt-0 min-w-[220px] rounded-2xl border border-brand-100 bg-white p-2 shadow-[0_18px_38px_rgba(60,30,120,0.22)]"
>
{m.children.map((c, ci) => {
const isHeader = c.href === '#' && c.label.startsWith('―');
if (isHeader) {
return (
<div key={c.label + ci} className="mt-1 px-3 py-1 text-[10px] font-bold uppercase tracking-widest text-brand-400">
{c.label.replace(/[―\s]/g, '')}
</div>
);
}
return (
<Link
key={c.label + ci}
href={c.href}
className="block rounded-lg px-3 py-2 text-[13px] text-neutral-700 hover:bg-brand-50 hover:text-brand-700"
>
{c.label}
</Link>
);
})}
</motion.div>
)}
</AnimatePresence>
</div>
))}
<span className="ml-auto" />
<button title="모든 메뉴" className="grid h-10 w-10 place-items-center rounded-full hover:bg-white/10"><MenuIcon size={18} /></button>
<form action="/api/ui/dark-mode" method="POST" className="m-0">
<button title="다크모드" className="grid h-9 w-9 place-items-center rounded-full border border-white/30 hover:bg-white/10">
<Moon size={15} />
</button>
</form>
</div>
</nav>
);
}
@@ -0,0 +1,135 @@
import Link from 'next/link';
import type { SiteUser, RankingEntry } from '@slot/themes';
import { Send, Sparkles, Trophy, Users } from 'lucide-react';
export interface SidebarProps {
user: SiteUser | null;
popularTags: { label: string; count: number }[];
rankings: RankingEntry[];
visitors: { today: number; yesterday: number; max: number; total: number };
}
export default function Sidebar({ user, popularTags, rankings, visitors }: SidebarProps) {
return (
<aside className="flex w-[300px] flex-col gap-4">
<LoginCard user={user} />
<TelegramBanner />
<TagCloudCard tags={popularTags} />
<RankingCard rankings={rankings} />
<VisitorsCard {...visitors} />
</aside>
);
}
function Card({ children, title, accent }: { children: React.ReactNode; title?: React.ReactNode; accent?: React.ReactNode }) {
return (
<section className="rounded-2xl border border-neutral-100 bg-white p-4 shadow-[0_1px_2px_rgba(0,0,0,0.02),0_8px_24px_rgba(124,82,224,0.04)]">
{title && (
<header className="mb-3 flex items-center justify-between border-b border-neutral-100 pb-2">
<h3 className="m-0 inline-flex items-center gap-1.5 text-[14px] font-bold">{title}</h3>
{accent}
</header>
)}
{children}
</section>
);
}
function LoginCard({ user }: { user: SiteUser | null }) {
return (
<Card title={<>LOGIN <span className="text-brand-500"></span></>}>
{user ? (
<div>
<p className="m-0 text-[13px]">
<strong className="text-brand-700">{user.nick}</strong>
</p>
<p className="mt-1 text-[12px] text-neutral-text-soft">
<strong className="text-neutral-800">{user.point.toLocaleString()}</strong>p ·
<strong className="text-neutral-800">{user.level}</strong>
</p>
<div className="mt-3 flex gap-2">
<Link href="/mypage" className="flex-1 rounded-lg border border-neutral-200 px-3 py-2 text-center text-[13px] hover:bg-neutral-50"></Link>
<form action="/api/auth/logout" method="POST" className="m-0 flex-1">
<button type="submit" className="w-full rounded-lg bg-brand-600 px-3 py-2 text-[13px] font-semibold text-white hover:bg-brand-700"></button>
</form>
</div>
</div>
) : (
<form action="/api/auth/login" method="POST">
<div className="mb-2 flex justify-between text-[12px]">
<Link href="/register" className="text-neutral-text-soft hover:text-brand-700"></Link>
<Link href="/auth/recover" className="text-neutral-text-soft hover:text-brand-700">/</Link>
</div>
<input name="loginId" placeholder="아이디" required className="mb-1.5 block w-full rounded-lg border border-neutral-200 px-3 py-2 text-[13px] outline-none focus:border-brand-500 focus:shadow-[0_0_0_4px_rgba(167,139,250,0.18)]" />
<input name="password" type="password" placeholder="비밀번호" required className="mb-1.5 block w-full rounded-lg border border-neutral-200 px-3 py-2 text-[13px] outline-none focus:border-brand-500 focus:shadow-[0_0_0_4px_rgba(167,139,250,0.18)]" />
<label className="my-1.5 flex items-center text-[12px] text-neutral-text-soft">
<input type="checkbox" name="auto_login" className="mr-1.5" />
</label>
<button type="submit" className="w-full rounded-lg bg-neutral-900 py-2 text-[14px] font-semibold text-white hover:bg-neutral-800"></button>
</form>
)}
</Card>
);
}
function TelegramBanner() {
return (
<a href="https://t.me/slotlifeCS" target="_blank" rel="noreferrer" className="block rounded-2xl bg-gradient-to-r from-[#229ED9] to-[#57b6e1] p-4 text-center text-white shadow-[0_4px_12px_rgba(34,158,217,0.35)] hover:shadow-[0_8px_22px_rgba(34,158,217,0.45)]">
<div className="inline-flex items-center gap-1.5 font-bold"><Send size={15} /> </div>
<div className="mt-2 inline-block rounded-full bg-white px-3 py-1 text-[13px] font-semibold text-[#229ED9]">@slotlifeCS</div>
</a>
);
}
function TagCloudCard({ tags }: { tags: { label: string; count: number }[] }) {
return (
<Card title={<><Sparkles size={14} className="text-brand-500" /> </>} accent={<Link href="/tags" className="text-[11px] text-neutral-text-soft hover:text-brand-700"> +</Link>}>
<div className="flex flex-wrap gap-1.5">
{tags.map((t, i) => (
<Link
key={t.label + i}
href={`/tag/${encodeURIComponent(t.label)}`}
className="inline-flex items-center gap-1 rounded-full bg-brand-50 px-2.5 py-1 text-[12px] text-brand-700 hover:bg-brand-100"
>
#{t.label}
<span className="text-[10px] text-neutral-text-soft">{t.count}</span>
</Link>
))}
</div>
</Card>
);
}
function RankingCard({ rankings }: { rankings: RankingEntry[] }) {
return (
<Card title={<><Trophy size={14} className="text-amber-500" /> </>} accent={<Link href="/games/activityrank" className="text-[11px] text-neutral-text-soft hover:text-brand-700"> +</Link>}>
<ol className="m-0 grid gap-1.5 p-0 list-none">
{rankings.slice(0, 8).map((r) => (
<li key={r.rank} className="flex items-center justify-between gap-2 text-[13px]">
<span className="flex items-center gap-2 truncate">
<span className={`grid h-5 w-5 place-items-center rounded-full text-[11px] font-bold ${r.rank === 1 ? 'bg-amber-400 text-white' : r.rank === 2 ? 'bg-zinc-300 text-zinc-800' : r.rank === 3 ? 'bg-orange-400 text-white' : 'bg-brand-50 text-brand-700'}`}>{r.rank}</span>
<Link href={`/profile/${encodeURIComponent(r.nick)}`} className="truncate text-neutral-800 hover:text-brand-700">{r.nick}</Link>
</span>
<span className="text-[11px] text-neutral-text-soft">{r.point.toLocaleString()}p</span>
</li>
))}
</ol>
</Card>
);
}
function VisitorsCard(v: { today: number; yesterday: number; max: number; total: number }) {
return (
<Card title={<><Users size={14} className="text-brand-500" /> </>}>
<ul className="m-0 grid gap-1 p-0 text-[12px] text-neutral-text-soft list-none">
<Row label="오늘" value={v.today} />
<Row label="어제" value={v.yesterday} />
<Row label="최대" value={v.max} />
<Row label="전체" value={v.total} />
</ul>
</Card>
);
}
function Row({ label, value }: { label: string; value: number }) {
return <li className="flex items-center justify-between"><span>{label}</span><strong className="text-neutral-800">{value.toLocaleString()}</strong></li>;
}
@@ -0,0 +1,45 @@
import Link from 'next/link';
import type { BoardSummary } from '@slot/themes';
import { ChevronRight, MessageSquare } from 'lucide-react';
const ACCENTS: Record<string, string> = {
free: 'from-violet-500 to-purple-700',
review: 'from-amber-400 to-orange-600',
mukti: 'from-rose-500 to-red-700',
humor: 'from-sky-400 to-blue-600',
pick: 'from-emerald-400 to-emerald-700',
lottery_ticket: 'from-pink-400 to-fuchsia-600',
};
export default function BoardSlots({ boards }: { boards: BoardSummary[] }) {
return (
<section className="grid gap-4 md:grid-cols-2">
{boards.map((b) => (
<article key={b.slug} className="lift overflow-hidden rounded-2xl bg-white ring-1 ring-neutral-100">
<header className={`flex items-center justify-between bg-gradient-to-r ${ACCENTS[b.slug] ?? 'from-brand-500 to-brand-700'} px-4 py-3 text-white`}>
<Link href={`/${b.slug}`} className="inline-flex items-center gap-1.5 text-[15px] font-bold tracking-tight">📰 {b.title}</Link>
<Link href={`/${b.slug}`} className="inline-flex items-center gap-0.5 rounded-full bg-white/20 px-2 py-0.5 text-[11px] font-semibold hover:bg-white/30"> <ChevronRight size={11} /></Link>
</header>
<ul className="m-0 grid divide-y divide-neutral-100 p-0 list-none">
{b.latest.length === 0 && (
<li className="px-4 py-6 text-center text-[13px] text-neutral-text-soft"> .</li>
)}
{b.latest.map((p) => (
<li key={p.id} className="flex items-center justify-between gap-2 px-4 py-2.5 transition hover:bg-brand-50/50">
<Link href={`/${b.slug}/${p.id}`} className="flex-1 truncate text-[14px] text-neutral-800 hover:text-brand-700">
{p.subject}
{p.commentCount > 0 && (
<span className="ml-1.5 inline-flex items-center gap-0.5 align-middle text-[11px] font-bold text-rose-500">
<MessageSquare size={11} /> {p.commentCount}
</span>
)}
</Link>
<span className="shrink-0 text-[11px] text-neutral-text-soft">{p.authorName}</span>
</li>
))}
</ul>
</article>
))}
</section>
);
}
@@ -0,0 +1,86 @@
'use client';
import { motion } from 'framer-motion';
import Link from 'next/link';
import { Megaphone, ChevronRight } from 'lucide-react';
export default function Hero({ headlines, kickStatus }: {
headlines: { id: string; subject: string; href: string }[];
kickStatus: 'live' | 'break' | 'offline';
}) {
const status =
kickStatus === 'live' ? { dot: 'bg-rose-500 animate-pulse', label: 'LIVE 방송 중', sub: '큰손형 라이브가 진행 중입니다' } :
kickStatus === 'break' ? { dot: 'bg-amber-400', label: '잠시 쉬는시간입니다', sub: '큰손형 방송이 곧 돌아옵니다' } :
{ dot: 'bg-zinc-400', label: '오늘은 휴방입니다', sub: '평일 오후 2시 ~ 10시 방송' };
return (
<section className="relative overflow-hidden rounded-3xl bg-brand-radial p-8 text-white shadow-[0_18px_38px_rgba(60,30,120,0.25)]">
{/* Decorative blur orbs */}
<div className="pointer-events-none absolute -top-20 -right-20 h-72 w-72 rounded-full bg-brand-400/40 blur-3xl" />
<div className="pointer-events-none absolute -bottom-32 -left-10 h-72 w-72 rounded-full bg-fuchsia-500/30 blur-3xl" />
<div className="grid gap-8 md:grid-cols-[1.4fr_1fr]">
<div className="relative">
<span className="inline-flex items-center gap-1 rounded-full bg-white/10 px-3 py-1 text-[11px] font-bold tracking-wider backdrop-blur">
<Megaphone size={12} />
</span>
<motion.h1
initial={{ opacity: 0, y: 18 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }}
className="mt-4 text-4xl font-extrabold leading-tight tracking-tight md:text-5xl"
>
<br />
<span className="bg-gradient-to-r from-amber-300 via-pink-300 to-violet-300 bg-clip-text text-transparent"> </span> .
</motion.h1>
<p className="mt-4 max-w-xl text-[15px] leading-relaxed text-white/85">
, , , , TV / .
</p>
{/* Headline ticker */}
<div className="mt-5 overflow-hidden rounded-2xl border border-white/10 bg-black/20 backdrop-blur-sm">
<div className="flex items-center gap-2 px-3 py-2 text-[12px]">
<span className="rounded-md bg-amber-300 px-2 py-0.5 font-bold text-amber-900">📢 </span>
<div className="marquee flex-1">
<div className="ticker-track">
{[...headlines, ...headlines].map((h, i) => (
<Link key={i} href={h.href} className="text-white/90 hover:text-white"> {h.subject}</Link>
))}
</div>
</div>
</div>
</div>
<div className="mt-5 flex flex-wrap gap-2">
<Link href="/guarantee" className="inline-flex items-center gap-1 rounded-full bg-white px-4 py-2 text-[13px] font-bold text-brand-700 hover:bg-brand-50">
<ChevronRight size={14} />
</Link>
<Link href="/mukti" className="inline-flex items-center gap-1 rounded-full bg-white/10 px-4 py-2 text-[13px] font-bold text-white hover:bg-white/20">
</Link>
<Link href="/lottery_ticket" className="inline-flex items-center gap-1 rounded-full bg-white/10 px-4 py-2 text-[13px] font-bold text-white hover:bg-white/20">
🎟
</Link>
</div>
</div>
{/* KICK 방송 상태 박스 */}
<div className="relative grid place-items-center">
<motion.div
initial={{ opacity: 0, scale: 0.96 }} animate={{ opacity: 1, scale: 1 }} transition={{ delay: 0.1 }}
className="w-full max-w-sm rounded-3xl bg-white/10 p-6 text-center backdrop-blur-md ring-1 ring-white/15"
>
<span className={`inline-flex items-center gap-2 rounded-full bg-black/30 px-3 py-1 text-[12px] font-bold ${kickStatus === 'live' ? 'text-rose-300' : 'text-amber-200'}`}>
<span className={`h-2 w-2 rounded-full ${status.dot}`} />
</span>
<h3 className="mt-3 text-2xl font-extrabold tracking-tight">{status.label}</h3>
<p className="mt-1 text-[13px] text-white/80">{status.sub}</p>
<a
href="https://kick.com/bighandbro" target="_blank" rel="noreferrer"
className="mt-5 inline-flex items-center gap-1.5 rounded-full bg-[#53fc18] px-5 py-2.5 text-[13px] font-extrabold text-black shadow-[0_8px_22px_rgba(83,252,24,0.45)] hover:bg-[#5cff20]"
>
KICK
</a>
<p className="mt-3 text-[11px] text-white/60"> 14:00 ~ 22:00 (KST)</p>
</motion.div>
</div>
</div>
</section>
);
}
@@ -0,0 +1,46 @@
'use client';
import { motion } from 'framer-motion';
import Link from 'next/link';
const TILES = [
{ label: '출석체크', emoji: '✅', href: '/page/attendance', from: '#a78bfa', to: '#7c3aed' },
{ label: '슬생복권', emoji: '🎟️', href: '/lottery_ticket', from: '#fb7185', to: '#e11d48' },
{ label: '회원랭킹', emoji: '🏆', href: '/games/activityrank', from: '#fbbf24', to: '#d97706' },
{ label: '현금교환', emoji: '💵', href: '/wallet/exchange', from: '#34d399', to: '#059669' },
{ label: '포인트교환', emoji: '🪙', href: '/wallet/point-exchange', from: '#60a5fa', to: '#1d4ed8' },
{ label: '기프티콘 교환', emoji: '🎁', href: '/gift_coupons', from: '#f472b6', to: '#be185d' },
{ label: '88포춘', emoji: '🎰', href: '/games/fortunes', from: '#facc15', to: '#a16207' },
{ label: '포인트바카라', emoji: '🃏', href: '/games/bacara', from: '#22d3ee', to: '#0e7490' },
{ label: '무료슬롯체험', emoji: '🎲', href: '/games/slot', from: '#c084fc', to: '#7e22ce' },
];
export default function QuickAccess() {
return (
<section>
<h2 className="mb-3 px-1 text-[15px] font-bold text-neutral-700"> </h2>
<div className="grid grid-cols-3 gap-3 md:grid-cols-3">
{TILES.map((t, i) => (
<motion.div key={t.label} initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: i * 0.03 }}>
<Link
href={t.href}
className="lift relative block overflow-hidden rounded-2xl bg-white p-4 ring-1 ring-neutral-100 hover:ring-brand-200"
style={{ '--g-from': t.from, '--g-to': t.to } as React.CSSProperties}
>
<span
className="absolute inset-0 -z-10 opacity-0 transition group-hover:opacity-100"
style={{ background: `linear-gradient(135deg, ${t.from}22, ${t.to}11)` }}
/>
<div className="flex items-center gap-3">
<span className="grid h-12 w-12 place-items-center rounded-xl text-2xl shadow-[0_6px_16px_rgba(0,0,0,0.08)]" style={{ background: `linear-gradient(135deg, ${t.from}, ${t.to})` }}>{t.emoji}</span>
<div>
<p className="m-0 text-[14px] font-bold text-neutral-800">{t.label}</p>
<p className="m-0 text-[11px] text-neutral-text-soft"> </p>
</div>
</div>
</Link>
</motion.div>
))}
</div>
</section>
);
}
+114
View File
@@ -0,0 +1,114 @@
// Build the full mega-menu tree from inspection2.g5_eyoom_menu — the actual
// data the production admin maintains. me_code is hierarchical:
// 001 = top-level
// 001001 = 1st child of 001
// 001001001 = grandchild
// We rewrite legacy /bbs/board.php?bo_table=foo URLs to /foo so the new
// site routes them through [boardSlug]/page.tsx.
import { legacySql } from '@slot/db/legacy';
import type { MenuItem } from '@slot/themes';
const ICON_FOR_TOPLEVEL: Record<string, string> = {
'보증사이트': '🛡',
'먹튀사이트': '⚠',
'커뮤니티': '💬',
'이벤트': '🎉',
'슬생정보': '',
'가품슬롯': '⊠',
'고객센터': '🎧',
'포인트게임': '🎮',
'슬생TV': '📺',
'포인트존': '🎁',
};
function rewriteLink(link: string): string {
if (!link) return '#';
// /bbs/board.php?bo_table=foo[&...] → /foo
const m = link.match(/^\/bbs\/board\.php\?bo_table=([a-z0-9_]+)/i);
if (m) return '/' + m[1];
// /bbs/qalist.php → /help/qa
if (/qalist\.php/.test(link)) return '/help/qa';
// /bbs/faq.php → /help/faq
if (/faq\.php/.test(link)) return '/help/faq';
// /bbs/<x>.php → /games/<x> for game pages we built
const g = link.match(/^\/bbs\/(bacara|fortunes|fivetreasures|seastory|davinci|oceanparadise|cherrymaster|yamato|kyoushi|lupin|taiku|matsuri|marilyn|giatrus|rings|bakabon|slot)(?:rank)?\.php/i);
if (g) return '/games/' + g[1].toLowerCase();
// /bbs/activityrank.php → /games/activityrank
const r = link.match(/^\/bbs\/(activityrank|muktirank|pointrank|levelrank|commentrank|specialrank|powerballrank)\.php/i);
if (r) return '/games/' + r[1].toLowerCase();
// /bbs/lottery_*.php → /lottery_ticket
if (/lottery/.test(link)) return '/lottery_ticket';
// /bbs/exchange-*.php and /bbs/pexchange.php → /wallet/...
if (/exchange-amount|pexchange\.php/.test(link)) return '/wallet/exchange';
if (/exchange-point/.test(link)) return '/wallet/point-exchange';
if (/exchange-event/.test(link)) return '/wallet/event-exchange';
if (/giftcon|giftcoupons/.test(link)) return '/gift_coupons';
if (/attendance/.test(link)) return '/page/attendance';
if (/notice\.php/.test(link)) return '/notice';
if (/^https?:\/\//.test(link)) return link;
return link;
}
interface MenuRow {
me_code: string;
me_name: string;
me_link: string;
me_target: string;
me_order: number;
me_use: number;
me_icon: string | null;
me_pid: string;
}
export async function fetchMegaMenusFromDb(theme = 'eb4_maga_005'): Promise<MenuItem[]> {
let rows: MenuRow[] = [];
try {
rows = await legacySql<MenuRow[]>`
SELECT me_code, me_name, me_link, me_target, me_order, me_use, me_icon, me_pid
FROM inspection2.g5_eyoom_menu
WHERE me_theme = ${theme}
AND me_use::text IN ('y','1','Y','t','true')
AND me_use_nav::text IN ('y','1','Y','t','true')
ORDER BY me_code, me_order
`;
} catch (e) {
console.warn('fetchMegaMenusFromDb fallback (no DB)', e);
return [];
}
// Build tree by me_code length (3 = top, 6 = sub, 9 = sub-sub).
type N = MenuItem & { _code: string };
const byCode = new Map<string, N>();
for (const r of rows) {
byCode.set(r.me_code, {
_code: r.me_code,
label: r.me_name,
href: rewriteLink(r.me_link),
icon: ICON_FOR_TOPLEVEL[r.me_name] ?? undefined,
children: [],
});
}
const tops: N[] = [];
for (const r of rows) {
const node = byCode.get(r.me_code)!;
if (r.me_code.length === 3) {
tops.push(node);
} else if (r.me_code.length >= 6) {
const parent = byCode.get(r.me_code.slice(0, r.me_code.length - 3));
if (parent) parent.children!.push(node);
}
}
// Sort siblings by their original me_order (preserve admin-defined order).
const orderMap = new Map<string, number>(rows.map((r) => [r.me_code, r.me_order]));
const sortNodes = (arr: N[]) => arr.sort((a, b) => (orderMap.get(a._code) ?? 0) - (orderMap.get(b._code) ?? 0));
sortNodes(tops);
for (const t of tops) sortNodes(t.children as N[]);
// Strip _code before returning
const strip = (n: N): MenuItem => ({
label: n.label,
href: n.href,
icon: n.icon,
children: n.children && n.children.length > 0 ? (n.children as N[]).map(strip) : undefined,
});
return tops.map(strip);
}
+68 -52
View File
@@ -1,5 +1,6 @@
// One-stop data fetcher for the chrome (header/footer/sidebar) + index page // One-stop data fetcher for the chrome (header/footer/sidebar) + index page
// content. Pulls from the migrated `inspection2` schema. // content. Pulls from the migrated `inspection2` schema. Menu structure
// mirrors slot-ss.com production exactly.
import { legacySql } from '@slot/db/legacy'; import { legacySql } from '@slot/db/legacy';
import { db, members, sessions } from '@slot/db'; import { db, members, sessions } from '@slot/db';
import { eq, and, gt } from 'drizzle-orm'; import { eq, and, gt } from 'drizzle-orm';
@@ -7,63 +8,89 @@ import type { MenuItem, SiteUser, RankingEntry, BoardSummary, IndexHomeProps } f
import { cookies, headers } from 'next/headers'; import { cookies, headers } from 'next/headers';
import { SESSION_COOKIE } from '@slot/auth'; import { SESSION_COOKIE } from '@slot/auth';
/** Static menu structure (mirrors the production mega-menu) */ /** Mega-menu structure copied verbatim from production https://slot-ss.com/. */
export const MEGA_MENUS: MenuItem[] = [ export const MEGA_MENUS: MenuItem[] = [
{ label: '보증사이트', href: '/guarantee', children: [ { label: '보증사이트', href: '/guarantee', children: [
{ label: '보증업체 목록', href: '/guarantee' }, { label: '보증사이트', href: '/guarantee' },
{ label: '입점신청', href: '/guarantee/apply' },
{ label: '보증업체 후기', href: '/review' },
]}, ]},
{ label: '먹튀사이트', href: '/mukti', children: [ { label: '먹튀사이트', href: '/mukti', children: [
{ label: '먹튀사이트 목록', href: '/mukti' },
{ label: '먹튀신고', href: '/complaint' }, { label: '먹튀신고', href: '/complaint' },
{ label: '먹튀검수 요청', href: '/inspection' }, { label: '먹튀사이트', href: '/mukti' },
{ label: '먹튀랭크', href: '/games/muktirank' },
]}, ]},
{ label: '커뮤니티', href: '/free', children: [ { label: '커뮤니티', href: '/free', children: [
{ label: '자유게시판', href: '/free' }, { label: '자유게시판', href: '/free' },
{ label: '후기게시판', href: '/review' },
{ label: '유머/이슈', href: '/humor' }, { label: '유머/이슈', href: '/humor' },
{ label: '픽게시판', href: '/pick' }, { label: '고배당 출금', href: '/dividend' },
{ label: '후방게시판', href: '/rear' }, { label: '후방게시판', href: '/rear' },
{ label: '카지노뉴스', href: '/news' }, { label: '활동랭킹', href: '/games/activityrank' },
]}, ]},
{ label: '이벤트', href: '/event', children: [ { label: '이벤트', href: '/event', children: [
{ label: '진행중인 이벤트', href: '/event' }, { label: '슬생 이벤트', href: '/event' },
{ label: '슬생복권', href: '/lottery_ticket' }, { label: '슬생복권', href: '/lottery_ticket' },
{ label: '기프티콘 교환', href: '/gift_coupons' }, { label: '일일미션룰렛', href: '/games/roulette' },
{ label: '기프티콘 현황', href: '/gift_exchanges' },
]}, ]},
{ label: '슬생정보', href: '/guide', children: [ { label: '슬생정보', href: '/news', children: [
{ label: '슬생 가이드', href: '/guide' }, { label: '카지노뉴스', href: '/news' },
{ label: '커뮤니티 가이드', href: '/guide/community' }, { label: '슬생컬럼', href: '/column' },
{ label: '포인트게임 가이드', href: '/guide/pointgame' }, { label: '카지노가이드', href: '/guide' },
{ label: '먹튀검수 가이드', href: '/guide/mukti' }, { label: '슬롯 리뷰', href: '/slotreview' },
]}, ]},
{ label: '가품슬롯', href: '/fakesite', icon: '⊠', children: [ { label: '가품슬롯', href: '/fakesite', icon: '⊠', children: [
{ label: '가품사이트 목록', href: '/fakesite' }, { label: '가품슬롯', href: '/fakes' },
{ label: '가품 신고', href: '/complaint' }, { label: '가품사이트', href: '/fakesite' },
]}, ]},
{ label: '고객센터', href: '/help', children: [ { label: '고객센터', href: '/notice', children: [
{ label: '1:1문의', href: '/help/qa' },
{ label: '자주묻는 질문 (FAQ)', href: '/help/faq' },
{ label: '공지사항', href: '/notice' }, { label: '공지사항', href: '/notice' },
{ label: '텔레그램 @slotlifeCS', href: 'https://t.me/slotlifeCS' }, { label: '1:1문의', href: '/help/qa' },
{ label: '슬생 가이드', href: '/guide' },
]}, ]},
{ label: '포인트게임', href: '/games', icon: '🎮', children: [ { label: '포인트게임', href: '/games', icon: '🎮', children: [
{ label: '포인트바카라', href: '/games/bacara' }, { label: '포인트 바카라', href: '/games/bacara' },
{ label: '88포춘', href: '/games/fortunes' }, { label: '― 스포츠 ―', href: '#' },
{ label: '5트레저', href: '/games/fivetreasures' }, { label: '크로스배팅', href: '/games/sports/cross' },
{ label: '룰렛', href: '/games/roulette' }, { label: '스페셜배팅', href: '/games/sports/special' },
{ label: '무료슬롯체험', href: '/games/slot' }, { label: '― 미니게임 ―', href: '#' },
{ label: '슬롯홀짝[1분]', href: '/games/mini/slot-holjjak' },
{ label: '파워볼[1분]', href: '/games/mini/powerball' },
{ label: '― 슬롯/릴 ―', href: '#' },
{ label: '5트레져', href: '/games/fivetreasures' },
{ label: '88포춘', href: '/games/fortunes' },
{ label: '바다이야기', href: '/games/seastory' },
{ label: '다빈치', href: '/games/davinci' },
{ label: '오션파라다이스', href: '/games/oceanparadise' },
{ label: '체리마스터', href: '/games/cherrymaster' },
{ label: '야마토', href: '/games/yamato' },
{ label: '강시', href: '/games/kyoushi' },
{ label: '루팡', href: '/games/lupin' },
{ label: '대공', href: '/games/taiku' },
{ label: '축제', href: '/games/matsuri' },
{ label: '마릴린먼로', href: '/games/marilyn' },
{ label: '고인돌', href: '/games/giatrus' },
{ label: '반지의제왕', href: '/games/rings' },
{ label: '바카본', href: '/games/bakabon' },
{ label: '― ―', href: '#' },
{ label: '무료슬롯체험', href: '/games/slot' },
{ label: '포인트게임 랭킹', href: '/games/ranking' },
]}, ]},
{ label: '슬생TV', href: '/tv', icon: '📺', children: [ { label: '슬생TV', href: '/tv', icon: '📺', children: [
{ label: 'KICK 큰손형 채널', href: 'https://kick.com/bighandbro' }, { label: '스포츠중계', href: '/tv/sports' },
{ label: 'TV 가이드', href: '/guide/tv' }, { label: '하이라이트', href: '/tv/highlight' },
{ label: '픽게시판', href: '/pick' },
{ label: '큰손형방송', href: 'https://kick.com/bighandbro' },
]}, ]},
{ label: '포인트존', href: '/wallet', icon: '🎁', children: [ { label: '포인트존', href: '/wallet', icon: '🎁', children: [
{ label: '포인트 내역', href: '/wallet' }, { label: '출석부', href: '/page/attendance' },
{ label: '포인트 교환', href: '/wallet/exchange' }, { label: '포인트안내', href: '/wallet/guide' },
{ label: '출석체크', href: '/page/attendance' }, { label: '포인트 현금교환', href: '/wallet/exchange' },
{ label: '슬롯버프', href: '/wallet/slotbuff' }, { label: '현금교환 리스트', href: '/wallet/exchange/list' },
{ label: '보증사이트 포인트교환', href: '/wallet/point-exchange' },
{ label: '포인트 교환 리스트', href: '/wallet/point-exchange/list' },
{ label: '기프티콘 교환', href: '/gift_coupons' },
{ label: '기프티콘 교환 리스트', href: '/gift_exchanges' },
{ label: '이벤트 포인트교환', href: '/wallet/event-exchange' },
{ label: '이벤트 포인트교환 리스트', href: '/wallet/event-exchange/list' },
]}, ]},
]; ];
@@ -85,8 +112,8 @@ export async function getCurrentSiteUser(): Promise<SiteUser | null> {
nick: m.nick, nick: m.nick,
level: m.level, level: m.level,
point: m.pointBalance, point: m.pointBalance,
respondCount: 0, // TODO: real count from posts table respondCount: 0,
memoCount: 0, // TODO: real count from memo table memoCount: 0,
}; };
} }
@@ -96,7 +123,6 @@ export async function getCurrentPathname(): Promise<string> {
} }
export async function getPopularTags(): Promise<{ label: string; count: number }[]> { export async function getPopularTags(): Promise<{ label: string; count: number }[]> {
// Pull from legacy g5_eyoom_tag if present, else fallback synthetic
try { try {
const rows = await legacySql<{ label: string; cnt: string }[]>` const rows = await legacySql<{ label: string; cnt: string }[]>`
SELECT eb_tag AS label, COUNT(*)::text AS cnt SELECT eb_tag AS label, COUNT(*)::text AS cnt
@@ -124,13 +150,7 @@ export async function getMemberRankings(): Promise<RankingEntry[]> {
`; `;
return rows.map((r, i) => ({ rank: i + 1, nick: r.mb_nick, level: r.mb_level, point: r.mb_point })); return rows.map((r, i) => ({ rank: i + 1, nick: r.mb_nick, level: r.mb_level, point: r.mb_point }));
} catch { } catch {
return [ return [];
{ rank: 1, nick: '대환장파티', level: 12, point: 723564 },
{ rank: 2, nick: '즐라탄', level: 12, point: 689012 },
{ rank: 3, nick: '슬생전설', level: 11, point: 612300 },
{ rank: 4, nick: '슬롯킹', level: 11, point: 589122 },
{ rank: 5, nick: '잭팟헌터', level: 10, point: 543080 },
];
} }
} }
@@ -145,16 +165,14 @@ export async function getHeadlines(): Promise<{ id: string; subject: string; hre
if (rows.length > 0) return rows.map((r) => ({ id: String(r.wr_id), subject: r.wr_subject, href: `/notice/${r.wr_id}` })); if (rows.length > 0) return rows.map((r) => ({ id: String(r.wr_id), subject: r.wr_subject, href: `/notice/${r.wr_id}` }));
} catch {} } catch {}
return [ return [
{ id: '1', subject: '🎉 슬롯생활 보증업체 후기 이벤트 (03월 4주차)', href: '/notice' }, { id: '1', subject: '🎉 슬롯생활 보증업체 후기 이벤트', href: '/notice' },
{ id: '2', subject: '● 온카판 입점사이트들의 답변회피 및 무대응', href: '/notice' },
{ id: '3', subject: '🎉 슬롯생활 보증업체 후기 이벤트 (01월)', href: '/notice' },
]; ];
} }
export function getKickStatus(now = new Date()): 'live' | 'break' | 'offline' { export function getKickStatus(now = new Date()): 'live' | 'break' | 'offline' {
const seoul = new Date(now.toLocaleString('en-US', { timeZone: 'Asia/Seoul' })); const seoul = new Date(now.toLocaleString('en-US', { timeZone: 'Asia/Seoul' }));
const hour = seoul.getHours(); const hour = seoul.getHours();
const day = seoul.getDay(); // 0 = Sun, 6 = Sat const day = seoul.getDay();
if (day === 0 || day === 6) return 'offline'; if (day === 0 || day === 6) return 'offline';
if (hour >= 14 && hour < 22) return Math.random() < 0.5 ? 'live' : 'break'; if (hour >= 14 && hour < 22) return Math.random() < 0.5 ? 'live' : 'break';
return 'offline'; return 'offline';
@@ -163,7 +181,7 @@ export function getKickStatus(now = new Date()): 'live' | 'break' | 'offline' {
export const QUICK_ACCESS = [ export const QUICK_ACCESS = [
{ label: '출석체크', emoji: '✅', href: '/page/attendance' }, { label: '출석체크', emoji: '✅', href: '/page/attendance' },
{ label: '슬생복권', emoji: '🎟️', href: '/lottery_ticket' }, { label: '슬생복권', emoji: '🎟️', href: '/lottery_ticket' },
{ label: '회원랭킹', emoji: '🏆', href: '/games/ranking' }, { label: '회원랭킹', emoji: '🏆', href: '/games/activityrank' },
{ label: '현금교환', emoji: '💵', href: '/wallet/exchange' }, { label: '현금교환', emoji: '💵', href: '/wallet/exchange' },
{ label: '포인트교환', emoji: '🪙', href: '/wallet/point-exchange' }, { label: '포인트교환', emoji: '🪙', href: '/wallet/point-exchange' },
{ label: '기프티콘 교환', emoji: '🎁', href: '/gift_coupons' }, { label: '기프티콘 교환', emoji: '🎁', href: '/gift_coupons' },
@@ -201,9 +219,7 @@ export async function getFeaturedBoards(): Promise<BoardSummary[]> {
authorName: r.wr_name, authorName: r.wr_name,
})), })),
}); });
} catch (e) { } catch {}
// skip board on error
}
} }
return out; return out;
} }
+481 -47
View File
@@ -29,12 +29,27 @@ importers:
'@slot/themes': '@slot/themes':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/themes version: link:../../packages/themes
'@tailwindcss/postcss':
specifier: ^4.2.4
version: 4.2.4
clsx:
specifier: ^2.1.1
version: 2.1.1
drizzle-orm: drizzle-orm:
specifier: ^0.36.4 specifier: ^0.36.4
version: 0.36.4(@types/react@19.2.14)(postgres@3.4.9)(react@19.2.5) version: 0.36.4(@types/react@19.2.14)(postgres@3.4.9)(react@19.2.5)
framer-motion:
specifier: ^12.38.0
version: 12.38.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
lucide-react:
specifier: ^1.11.0
version: 1.11.0(react@19.2.5)
next: next:
specifier: ^15.1.0 specifier: ^15.1.0
version: 15.5.15(react-dom@19.2.5(react@19.2.5))(react@19.2.5) version: 15.5.15(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
postcss:
specifier: ^8.5.12
version: 8.5.12
postgres: postgres:
specifier: ^3.4.5 specifier: ^3.4.5
version: 3.4.9 version: 3.4.9
@@ -44,6 +59,9 @@ importers:
react-dom: react-dom:
specifier: ^19.0.0 specifier: ^19.0.0
version: 19.2.5(react@19.2.5) version: 19.2.5(react@19.2.5)
tailwindcss:
specifier: ^4.2.4
version: 4.2.4
devDependencies: devDependencies:
'@types/node': '@types/node':
specifier: ^22.10.0 specifier: ^22.10.0
@@ -56,10 +74,10 @@ importers:
version: 19.2.3(@types/react@19.2.14) version: 19.2.3(@types/react@19.2.14)
eslint: eslint:
specifier: ^9.17.0 specifier: ^9.17.0
version: 9.39.4 version: 9.39.4(jiti@2.6.1)
eslint-config-next: eslint-config-next:
specifier: ^15.1.0 specifier: ^15.1.0
version: 15.5.15(eslint@9.39.4)(typescript@5.9.3) version: 15.5.15(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
typescript: typescript:
specifier: ^5.7.2 specifier: ^5.7.2
version: 5.9.3 version: 5.9.3
@@ -120,6 +138,10 @@ importers:
packages: packages:
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
'@drizzle-team/brocli@0.10.2': '@drizzle-team/brocli@0.10.2':
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
@@ -761,6 +783,22 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
'@jridgewell/remapping@2.3.5':
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@napi-rs/wasm-runtime@0.2.12': '@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
@@ -843,6 +881,94 @@ packages:
'@swc/helpers@0.5.15': '@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@tailwindcss/node@4.2.4':
resolution: {integrity: sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==}
'@tailwindcss/oxide-android-arm64@4.2.4':
resolution: {integrity: sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [android]
'@tailwindcss/oxide-darwin-arm64@4.2.4':
resolution: {integrity: sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [darwin]
'@tailwindcss/oxide-darwin-x64@4.2.4':
resolution: {integrity: sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==}
engines: {node: '>= 20'}
cpu: [x64]
os: [darwin]
'@tailwindcss/oxide-freebsd-x64@4.2.4':
resolution: {integrity: sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==}
engines: {node: '>= 20'}
cpu: [x64]
os: [freebsd]
'@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4':
resolution: {integrity: sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==}
engines: {node: '>= 20'}
cpu: [arm]
os: [linux]
'@tailwindcss/oxide-linux-arm64-gnu@4.2.4':
resolution: {integrity: sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
'@tailwindcss/oxide-linux-arm64-musl@4.2.4':
resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
'@tailwindcss/oxide-linux-x64-gnu@4.2.4':
resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
'@tailwindcss/oxide-linux-x64-musl@4.2.4':
resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
'@tailwindcss/oxide-wasm32-wasi@4.2.4':
resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
bundledDependencies:
- '@napi-rs/wasm-runtime'
- '@emnapi/core'
- '@emnapi/runtime'
- '@tybys/wasm-util'
- '@emnapi/wasi-threads'
- tslib
'@tailwindcss/oxide-win32-arm64-msvc@4.2.4':
resolution: {integrity: sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [win32]
'@tailwindcss/oxide-win32-x64-msvc@4.2.4':
resolution: {integrity: sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==}
engines: {node: '>= 20'}
cpu: [x64]
os: [win32]
'@tailwindcss/oxide@4.2.4':
resolution: {integrity: sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==}
engines: {node: '>= 20'}
'@tailwindcss/postcss@4.2.4':
resolution: {integrity: sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg==}
'@tybys/wasm-util@0.10.1': '@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@@ -1150,6 +1276,10 @@ packages:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
color-convert@2.0.1: color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'} engines: {node: '>=7.0.0'}
@@ -1329,6 +1459,10 @@ packages:
emoji-regex@9.2.2: emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
enhanced-resolve@5.21.0:
resolution: {integrity: sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==}
engines: {node: '>=10.13.0'}
es-abstract@1.24.2: es-abstract@1.24.2:
resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1557,6 +1691,20 @@ packages:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
framer-motion@12.38.0:
resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
fsevents@2.3.2: fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1620,6 +1768,9 @@ packages:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
has-bigints@1.1.0: has-bigints@1.1.0:
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1784,6 +1935,10 @@ packages:
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
jiti@2.6.1:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
js-tokens@4.0.0: js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -1822,6 +1977,76 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [android]
lightningcss-darwin-arm64@1.32.0:
resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
lightningcss-darwin-x64@1.32.0:
resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
lightningcss-freebsd-x64@1.32.0:
resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
lightningcss-linux-arm-gnueabihf@1.32.0:
resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
lightningcss-linux-arm64-gnu@1.32.0:
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
lightningcss-win32-x64-msvc@1.32.0:
resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
lightningcss@1.32.0:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
locate-path@6.0.0: locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -1833,6 +2058,14 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true hasBin: true
lucide-react@1.11.0:
resolution: {integrity: sha512-UOhjdztXCgdBReRcIhsvz2siIBogfv/lhJEIViCpLt924dO+GDms9T7DNoucI23s6kEPpe988m5N0D2ajnzb2g==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
math-intrinsics@1.1.0: math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1855,6 +2088,12 @@ packages:
minimist@1.2.8: minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
motion-dom@12.38.0:
resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==}
motion-utils@12.36.0:
resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==}
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -1988,6 +2227,10 @@ packages:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
postcss@8.5.12:
resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==}
engines: {node: ^10 || ^12 || >=14}
postgres@3.4.9: postgres@3.4.9:
resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==} resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -2202,6 +2445,13 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
tailwindcss@4.2.4:
resolution: {integrity: sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==}
tapable@2.3.3:
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
engines: {node: '>=6'}
tinyglobby@0.2.16: tinyglobby@0.2.16:
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@@ -2316,6 +2566,8 @@ packages:
snapshots: snapshots:
'@alloc/quick-lru@5.2.0': {}
'@drizzle-team/brocli@0.10.2': {} '@drizzle-team/brocli@0.10.2': {}
'@emnapi/core@1.10.0': '@emnapi/core@1.10.0':
@@ -2557,9 +2809,9 @@ snapshots:
'@esbuild/win32-x64@0.27.7': '@esbuild/win32-x64@0.27.7':
optional: true optional: true
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))':
dependencies: dependencies:
eslint: 9.39.4 eslint: 9.39.4(jiti@2.6.1)
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {} '@eslint-community/regexpp@4.12.2': {}
@@ -2716,6 +2968,25 @@ snapshots:
'@img/sharp-win32-x64@0.34.5': '@img/sharp-win32-x64@0.34.5':
optional: true optional: true
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/remapping@2.3.5':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.31':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@napi-rs/wasm-runtime@0.2.12': '@napi-rs/wasm-runtime@0.2.12':
dependencies: dependencies:
'@emnapi/core': 1.10.0 '@emnapi/core': 1.10.0
@@ -2775,6 +3046,75 @@ snapshots:
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
'@tailwindcss/node@4.2.4':
dependencies:
'@jridgewell/remapping': 2.3.5
enhanced-resolve: 5.21.0
jiti: 2.6.1
lightningcss: 1.32.0
magic-string: 0.30.21
source-map-js: 1.2.1
tailwindcss: 4.2.4
'@tailwindcss/oxide-android-arm64@4.2.4':
optional: true
'@tailwindcss/oxide-darwin-arm64@4.2.4':
optional: true
'@tailwindcss/oxide-darwin-x64@4.2.4':
optional: true
'@tailwindcss/oxide-freebsd-x64@4.2.4':
optional: true
'@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4':
optional: true
'@tailwindcss/oxide-linux-arm64-gnu@4.2.4':
optional: true
'@tailwindcss/oxide-linux-arm64-musl@4.2.4':
optional: true
'@tailwindcss/oxide-linux-x64-gnu@4.2.4':
optional: true
'@tailwindcss/oxide-linux-x64-musl@4.2.4':
optional: true
'@tailwindcss/oxide-wasm32-wasi@4.2.4':
optional: true
'@tailwindcss/oxide-win32-arm64-msvc@4.2.4':
optional: true
'@tailwindcss/oxide-win32-x64-msvc@4.2.4':
optional: true
'@tailwindcss/oxide@4.2.4':
optionalDependencies:
'@tailwindcss/oxide-android-arm64': 4.2.4
'@tailwindcss/oxide-darwin-arm64': 4.2.4
'@tailwindcss/oxide-darwin-x64': 4.2.4
'@tailwindcss/oxide-freebsd-x64': 4.2.4
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.4
'@tailwindcss/oxide-linux-arm64-gnu': 4.2.4
'@tailwindcss/oxide-linux-arm64-musl': 4.2.4
'@tailwindcss/oxide-linux-x64-gnu': 4.2.4
'@tailwindcss/oxide-linux-x64-musl': 4.2.4
'@tailwindcss/oxide-wasm32-wasi': 4.2.4
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.4
'@tailwindcss/oxide-win32-x64-msvc': 4.2.4
'@tailwindcss/postcss@4.2.4':
dependencies:
'@alloc/quick-lru': 5.2.0
'@tailwindcss/node': 4.2.4
'@tailwindcss/oxide': 4.2.4
postcss: 8.5.12
tailwindcss: 4.2.4
'@tybys/wasm-util@0.10.1': '@tybys/wasm-util@0.10.1':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@@ -2798,15 +3138,15 @@ snapshots:
dependencies: dependencies:
csstype: 3.2.3 csstype: 3.2.3
'@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': '@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
'@eslint-community/regexpp': 4.12.2 '@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.59.0(eslint@9.39.4)(typescript@5.9.3) '@typescript-eslint/parser': 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.59.0 '@typescript-eslint/scope-manager': 8.59.0
'@typescript-eslint/type-utils': 8.59.0(eslint@9.39.4)(typescript@5.9.3) '@typescript-eslint/type-utils': 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/utils': 8.59.0(eslint@9.39.4)(typescript@5.9.3) '@typescript-eslint/utils': 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.59.0 '@typescript-eslint/visitor-keys': 8.59.0
eslint: 9.39.4 eslint: 9.39.4(jiti@2.6.1)
ignore: 7.0.5 ignore: 7.0.5
natural-compare: 1.4.0 natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@5.9.3) ts-api-utils: 2.5.0(typescript@5.9.3)
@@ -2814,14 +3154,14 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3)': '@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
'@typescript-eslint/scope-manager': 8.59.0 '@typescript-eslint/scope-manager': 8.59.0
'@typescript-eslint/types': 8.59.0 '@typescript-eslint/types': 8.59.0
'@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.59.0 '@typescript-eslint/visitor-keys': 8.59.0
debug: 4.4.3 debug: 4.4.3
eslint: 9.39.4 eslint: 9.39.4(jiti@2.6.1)
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -2844,13 +3184,13 @@ snapshots:
dependencies: dependencies:
typescript: 5.9.3 typescript: 5.9.3
'@typescript-eslint/type-utils@8.59.0(eslint@9.39.4)(typescript@5.9.3)': '@typescript-eslint/type-utils@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
'@typescript-eslint/types': 8.59.0 '@typescript-eslint/types': 8.59.0
'@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.59.0(eslint@9.39.4)(typescript@5.9.3) '@typescript-eslint/utils': 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
debug: 4.4.3 debug: 4.4.3
eslint: 9.39.4 eslint: 9.39.4(jiti@2.6.1)
ts-api-utils: 2.5.0(typescript@5.9.3) ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
@@ -2873,13 +3213,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/utils@8.59.0(eslint@9.39.4)(typescript@5.9.3)': '@typescript-eslint/utils@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1))
'@typescript-eslint/scope-manager': 8.59.0 '@typescript-eslint/scope-manager': 8.59.0
'@typescript-eslint/types': 8.59.0 '@typescript-eslint/types': 8.59.0
'@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3)
eslint: 9.39.4 eslint: 9.39.4(jiti@2.6.1)
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -3103,6 +3443,8 @@ snapshots:
strip-ansi: 6.0.1 strip-ansi: 6.0.1
wrap-ansi: 7.0.0 wrap-ansi: 7.0.0
clsx@2.1.1: {}
color-convert@2.0.1: color-convert@2.0.1:
dependencies: dependencies:
color-name: 1.1.4 color-name: 1.1.4
@@ -3170,8 +3512,7 @@ snapshots:
has-property-descriptors: 1.0.2 has-property-descriptors: 1.0.2
object-keys: 1.1.1 object-keys: 1.1.1
detect-libc@2.1.2: detect-libc@2.1.2: {}
optional: true
doctrine@2.1.0: doctrine@2.1.0:
dependencies: dependencies:
@@ -3202,6 +3543,11 @@ snapshots:
emoji-regex@9.2.2: {} emoji-regex@9.2.2: {}
enhanced-resolve@5.21.0:
dependencies:
graceful-fs: 4.2.11
tapable: 2.3.3
es-abstract@1.24.2: es-abstract@1.24.2:
dependencies: dependencies:
array-buffer-byte-length: 1.0.2 array-buffer-byte-length: 1.0.2
@@ -3394,19 +3740,19 @@ snapshots:
escape-string-regexp@4.0.0: {} escape-string-regexp@4.0.0: {}
eslint-config-next@15.5.15(eslint@9.39.4)(typescript@5.9.3): eslint-config-next@15.5.15(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3):
dependencies: dependencies:
'@next/eslint-plugin-next': 15.5.15 '@next/eslint-plugin-next': 15.5.15
'@rushstack/eslint-patch': 1.16.1 '@rushstack/eslint-patch': 1.16.1
'@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) '@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': 8.59.0(eslint@9.39.4)(typescript@5.9.3) '@typescript-eslint/parser': 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.4 eslint: 9.39.4(jiti@2.6.1)
eslint-import-resolver-node: 0.3.10 eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@9.39.4) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.4) eslint-plugin-react-hooks: 5.2.0(eslint@9.39.4(jiti@2.6.1))
optionalDependencies: optionalDependencies:
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
@@ -3422,33 +3768,33 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4): eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)):
dependencies: dependencies:
'@nolyfill/is-core-module': 1.0.39 '@nolyfill/is-core-module': 1.0.39
debug: 4.4.3 debug: 4.4.3
eslint: 9.39.4 eslint: 9.39.4(jiti@2.6.1)
get-tsconfig: 4.14.0 get-tsconfig: 4.14.0
is-bun-module: 2.0.0 is-bun-module: 2.0.0
stable-hash: 0.0.5 stable-hash: 0.0.5
tinyglobby: 0.2.16 tinyglobby: 0.2.16
unrs-resolver: 1.11.1 unrs-resolver: 1.11.1
optionalDependencies: optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4): eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)):
dependencies: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 8.59.0(eslint@9.39.4)(typescript@5.9.3) '@typescript-eslint/parser': 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.4 eslint: 9.39.4(jiti@2.6.1)
eslint-import-resolver-node: 0.3.10 eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4): eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)):
dependencies: dependencies:
'@rtsao/scc': 1.1.0 '@rtsao/scc': 1.1.0
array-includes: 3.1.9 array-includes: 3.1.9
@@ -3457,9 +3803,9 @@ snapshots:
array.prototype.flatmap: 1.3.3 array.prototype.flatmap: 1.3.3
debug: 3.2.7 debug: 3.2.7
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 9.39.4 eslint: 9.39.4(jiti@2.6.1)
eslint-import-resolver-node: 0.3.10 eslint-import-resolver-node: 0.3.10
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4) eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
hasown: 2.0.3 hasown: 2.0.3
is-core-module: 2.16.1 is-core-module: 2.16.1
is-glob: 4.0.3 is-glob: 4.0.3
@@ -3471,13 +3817,13 @@ snapshots:
string.prototype.trimend: 1.0.9 string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0 tsconfig-paths: 3.15.0
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 8.59.0(eslint@9.39.4)(typescript@5.9.3) '@typescript-eslint/parser': 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
transitivePeerDependencies: transitivePeerDependencies:
- eslint-import-resolver-typescript - eslint-import-resolver-typescript
- eslint-import-resolver-webpack - eslint-import-resolver-webpack
- supports-color - supports-color
eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4): eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)):
dependencies: dependencies:
aria-query: 5.3.2 aria-query: 5.3.2
array-includes: 3.1.9 array-includes: 3.1.9
@@ -3487,7 +3833,7 @@ snapshots:
axobject-query: 4.1.0 axobject-query: 4.1.0
damerau-levenshtein: 1.0.8 damerau-levenshtein: 1.0.8
emoji-regex: 9.2.2 emoji-regex: 9.2.2
eslint: 9.39.4 eslint: 9.39.4(jiti@2.6.1)
hasown: 2.0.3 hasown: 2.0.3
jsx-ast-utils: 3.3.5 jsx-ast-utils: 3.3.5
language-tags: 1.0.9 language-tags: 1.0.9
@@ -3496,11 +3842,11 @@ snapshots:
safe-regex-test: 1.1.0 safe-regex-test: 1.1.0
string.prototype.includes: 2.0.1 string.prototype.includes: 2.0.1
eslint-plugin-react-hooks@5.2.0(eslint@9.39.4): eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)):
dependencies: dependencies:
eslint: 9.39.4 eslint: 9.39.4(jiti@2.6.1)
eslint-plugin-react@7.37.5(eslint@9.39.4): eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.6.1)):
dependencies: dependencies:
array-includes: 3.1.9 array-includes: 3.1.9
array.prototype.findlast: 1.2.5 array.prototype.findlast: 1.2.5
@@ -3508,7 +3854,7 @@ snapshots:
array.prototype.tosorted: 1.1.4 array.prototype.tosorted: 1.1.4
doctrine: 2.1.0 doctrine: 2.1.0
es-iterator-helpers: 1.3.2 es-iterator-helpers: 1.3.2
eslint: 9.39.4 eslint: 9.39.4(jiti@2.6.1)
estraverse: 5.3.0 estraverse: 5.3.0
hasown: 2.0.3 hasown: 2.0.3
jsx-ast-utils: 3.3.5 jsx-ast-utils: 3.3.5
@@ -3533,9 +3879,9 @@ snapshots:
eslint-visitor-keys@5.0.1: {} eslint-visitor-keys@5.0.1: {}
eslint@9.39.4: eslint@9.39.4(jiti@2.6.1):
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1))
'@eslint-community/regexpp': 4.12.2 '@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.21.2 '@eslint/config-array': 0.21.2
'@eslint/config-helpers': 0.4.2 '@eslint/config-helpers': 0.4.2
@@ -3569,6 +3915,8 @@ snapshots:
minimatch: 3.1.5 minimatch: 3.1.5
natural-compare: 1.4.0 natural-compare: 1.4.0
optionator: 0.9.4 optionator: 0.9.4
optionalDependencies:
jiti: 2.6.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -3636,6 +3984,15 @@ snapshots:
dependencies: dependencies:
is-callable: 1.2.7 is-callable: 1.2.7
framer-motion@12.38.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
motion-dom: 12.38.0
motion-utils: 12.36.0
tslib: 2.8.1
optionalDependencies:
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
fsevents@2.3.2: fsevents@2.3.2:
optional: true optional: true
@@ -3704,6 +4061,8 @@ snapshots:
gopd@1.2.0: {} gopd@1.2.0: {}
graceful-fs@4.2.11: {}
has-bigints@1.1.0: {} has-bigints@1.1.0: {}
has-flag@4.0.0: {} has-flag@4.0.0: {}
@@ -3870,6 +4229,8 @@ snapshots:
has-symbols: 1.1.0 has-symbols: 1.1.0
set-function-name: 2.0.2 set-function-name: 2.0.2
jiti@2.6.1: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
js-yaml@4.1.1: js-yaml@4.1.1:
@@ -3908,6 +4269,55 @@ snapshots:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
type-check: 0.4.0 type-check: 0.4.0
lightningcss-android-arm64@1.32.0:
optional: true
lightningcss-darwin-arm64@1.32.0:
optional: true
lightningcss-darwin-x64@1.32.0:
optional: true
lightningcss-freebsd-x64@1.32.0:
optional: true
lightningcss-linux-arm-gnueabihf@1.32.0:
optional: true
lightningcss-linux-arm64-gnu@1.32.0:
optional: true
lightningcss-linux-arm64-musl@1.32.0:
optional: true
lightningcss-linux-x64-gnu@1.32.0:
optional: true
lightningcss-linux-x64-musl@1.32.0:
optional: true
lightningcss-win32-arm64-msvc@1.32.0:
optional: true
lightningcss-win32-x64-msvc@1.32.0:
optional: true
lightningcss@1.32.0:
dependencies:
detect-libc: 2.1.2
optionalDependencies:
lightningcss-android-arm64: 1.32.0
lightningcss-darwin-arm64: 1.32.0
lightningcss-darwin-x64: 1.32.0
lightningcss-freebsd-x64: 1.32.0
lightningcss-linux-arm-gnueabihf: 1.32.0
lightningcss-linux-arm64-gnu: 1.32.0
lightningcss-linux-arm64-musl: 1.32.0
lightningcss-linux-x64-gnu: 1.32.0
lightningcss-linux-x64-musl: 1.32.0
lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0
locate-path@6.0.0: locate-path@6.0.0:
dependencies: dependencies:
p-locate: 5.0.0 p-locate: 5.0.0
@@ -3918,6 +4328,14 @@ snapshots:
dependencies: dependencies:
js-tokens: 4.0.0 js-tokens: 4.0.0
lucide-react@1.11.0(react@19.2.5):
dependencies:
react: 19.2.5
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
math-intrinsics@1.1.0: {} math-intrinsics@1.1.0: {}
merge2@1.4.1: {} merge2@1.4.1: {}
@@ -3937,6 +4355,12 @@ snapshots:
minimist@1.2.8: {} minimist@1.2.8: {}
motion-dom@12.38.0:
dependencies:
motion-utils: 12.36.0
motion-utils@12.36.0: {}
ms@2.1.3: {} ms@2.1.3: {}
nanoid@3.3.11: {} nanoid@3.3.11: {}
@@ -4072,6 +4496,12 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
postcss@8.5.12:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
postgres@3.4.9: {} postgres@3.4.9: {}
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
@@ -4350,6 +4780,10 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {} supports-preserve-symlinks-flag@1.0.0: {}
tailwindcss@4.2.4: {}
tapable@2.3.3: {}
tinyglobby@0.2.16: tinyglobby@0.2.16:
dependencies: dependencies:
fdir: 6.5.0(picomatch@4.0.4) fdir: 6.5.0(picomatch@4.0.4)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 KiB

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 507 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 510 KiB

+19
View File
@@ -71,6 +71,25 @@ for (const t of THEMES) {
console.log(`✓ theme=${t} home captured`); console.log(`✓ theme=${t} home captured`);
} }
// Capture full-page home + each top-level mega-menu opened
await context.addCookies([{ name: 'slot_theme_pref', value: 'eyoom', url: 'http://localhost:3000' }]);
await page.goto('http://localhost:3000/', { waitUntil: 'networkidle' });
await page.screenshot({ path: `${OUT}/home-full.png`, fullPage: true });
console.log('✓ home-full captured');
for (const label of ['보증사이트', '먹튀사이트', '커뮤니티', '이벤트', '슬생정보', '가품슬롯', '고객센터', '포인트게임', '슬생TV', '포인트존']) {
try {
await page.goto('http://localhost:3000/', { waitUntil: 'networkidle' });
const link = page.getByRole('link', { name: label, exact: true }).first();
await link.hover();
await page.waitForTimeout(400);
await page.screenshot({ path: `${OUT}/menu-${label}.png`, fullPage: false });
console.log('✓ menu', label);
} catch (e) {
console.log('✗ menu', label, e.message?.slice(0, 60));
}
}
await browser.close(); await browser.close();
console.log(`\nDone. ${okCount} ok, ${failCount} fail. → ${OUT}`); console.log(`\nDone. ${okCount} ok, ${failCount} fail. → ${OUT}`);
process.exit(failCount > 0 ? 1 : 0); process.exit(failCount > 0 ? 1 : 0);