Skip to content

Commit cb623eb

Browse files
committed
feat: Implement AI-powered 2D game builder with new landing page, credit system, and authentication.
1 parent 1222019 commit cb623eb

15 files changed

Lines changed: 282 additions & 105 deletions

File tree

apps/web/public/file.svg

Lines changed: 0 additions & 1 deletion
This file was deleted.

apps/web/public/globe.svg

Lines changed: 0 additions & 1 deletion
This file was deleted.

apps/web/public/next.svg

Lines changed: 0 additions & 1 deletion
This file was deleted.

apps/web/public/vercel.svg

Lines changed: 0 additions & 1 deletion
This file was deleted.

apps/web/public/window.svg

Lines changed: 0 additions & 1 deletion
This file was deleted.

apps/web/src/app/api/chat/route.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from "next/server";
22
import { auth } from "@/auth";
33
import { prisma } from "@packages/model/db/client";
44
import { Controller } from "@packages/controller/index";
5+
import { consumeCredit } from "@/lib/credits";
6+
import crypto from "crypto";
57

68
export async function POST(req: NextRequest) {
79
try {
@@ -18,14 +20,23 @@ export async function POST(req: NextRequest) {
1820
prompt?: string;
1921
};
2022

23+
const isGuest = session.user.email === "[email protected]";
24+
const actualUserId = isGuest ? null : userId;
25+
let deviceId = req.cookies.get("guest_device_id")?.value;
26+
let newDeviceId = false;
27+
28+
if (isGuest && !deviceId) {
29+
deviceId = crypto.randomUUID();
30+
newDeviceId = true;
31+
}
32+
2133
// New game flow — create session and start
2234
if (!sessionId && prompt) {
23-
// Guest mode: delete previous sessions to enforce single-slot
24-
// if (session.user.email === "[email protected]") {
25-
// await prisma.session.deleteMany({
26-
// where: { userId },
27-
// });
28-
// }
35+
const creditStatus = await consumeCredit(actualUserId, deviceId);
36+
37+
if (!creditStatus.allowed) {
38+
return NextResponse.json({ error: "Insufficient credits. Please try again tomorrow." }, { status: 403 });
39+
}
2940

3041
const newSession = await prisma.session.create({
3142
data: {
@@ -38,11 +49,17 @@ export async function POST(req: NextRequest) {
3849
const controller = new Controller(newSession.id);
3950
const result = await controller.start();
4051

41-
return NextResponse.json({
52+
const response = NextResponse.json({
4253
type: result.type,
4354
data: result.data,
4455
sessionId: newSession.id,
4556
});
57+
58+
if (newDeviceId && deviceId) {
59+
response.cookies.set("guest_device_id", deviceId, { maxAge: 60 * 60 * 24 * 365, httpOnly: true });
60+
}
61+
62+
return response;
4663
}
4764

4865
// Existing game flow — continue session
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { auth } from "@/auth";
3+
import { getCreditsInfo } from "@/lib/credits";
4+
5+
export async function GET(req: NextRequest) {
6+
try {
7+
const session = await auth();
8+
let userId = session?.user?.id;
9+
10+
// If guestuser@gmail.com, we treat them as guest
11+
if (session?.user?.email === "[email protected]") {
12+
userId = undefined;
13+
}
14+
15+
const deviceId = req.cookies.get("guest_device_id")?.value;
16+
17+
const info = await getCreditsInfo(userId, deviceId);
18+
19+
if (!info) {
20+
return NextResponse.json({ credits: 0, maxCredits: 0, error: "Unable to find credit info" }, { status: 400 });
21+
}
22+
23+
return NextResponse.json(info);
24+
} catch (error) {
25+
console.error("[/api/credits] Error:", error);
26+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
27+
}
28+
}

apps/web/src/app/builder/page.tsx

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import { useState } from "react";
44
import { GameBuilderProvider, useGameBuilder } from "@/context/GameBuilderContext";
5-
import { MessageSquare, Code2, Play, PanelRightClose, PanelRightOpen } from "lucide-react";
5+
import { CreditsProvider, useCredits } from "@/context/CreditsContext";
6+
import { MessageSquare, Code2, Play, PanelRightClose, PanelRightOpen, Coins } from "lucide-react";
67
import SessionHistory from "@/components/SessionHistory";
78
import ChatInterface from "@/components/ChatInterface";
89
import CodeViewer from "@/components/CodeViewer";
@@ -21,6 +22,8 @@ function BuilderLayout() {
2122
const { data: session } = useSession();
2223
const [activeTab, setActiveTab] = useState<MobileTab>("chat");
2324
const [showHistoryPanel, setShowHistoryPanel] = useState(false);
25+
const [showCredits, setShowCredits] = useState(false);
26+
const { credits, maxCredits, isGuest, isLoading } = useCredits();
2427

2528
const tabs: { id: MobileTab; label: string; icon: typeof MessageSquare }[] = [
2629
{ id: "chat", label: "Chat", icon: MessageSquare },
@@ -66,8 +69,19 @@ function BuilderLayout() {
6669
</div>
6770

6871
{/* Bottom User Actions */}
69-
<div className="flex flex-col gap-4 items-center shrink-0 w-full mt-auto">
70-
<div className="w-10 h-10 rounded-full bg-slate-100 border-2 border-white shadow-sm flex items-center justify-center cursor-pointer hover:bg-slate-200 transition-colors overflow-hidden">
72+
<div className="flex flex-col gap-4 items-center shrink-0 w-full mt-auto relative">
73+
{showCredits && (
74+
<div className="absolute bottom-full left-14 mb-2 p-3 bg-white shadow-xl border border-slate-200 rounded-xl w-48 z-50 animate-fade-in text-center flex flex-col items-center">
75+
<Coins className="w-6 h-6 text-amber-500 mb-1" />
76+
<h4 className="font-bold text-slate-800 text-sm">{isGuest ? "Guest Credits" : "Daily Credits"}</h4>
77+
<p className="text-2xl font-black text-indigo-600 my-1">{isLoading ? "..." : credits} <span className="text-sm text-slate-400 font-medium">/ {maxCredits}</span></p>
78+
<p className="text-[10px] text-slate-500 font-medium uppercase tracking-wider">Refreshes at midnight</p>
79+
</div>
80+
)}
81+
82+
<div
83+
onClick={() => setShowCredits(!showCredits)}
84+
className="w-10 h-10 rounded-full bg-slate-100 border-2 border-white shadow-sm flex items-center justify-center cursor-pointer hover:bg-slate-200 transition-colors overflow-hidden">
7185
{session?.user?.image ? (
7286
<img src={session.user.image} alt="User" className="w-full h-full object-cover" />
7387
) : (
@@ -149,8 +163,10 @@ function BuilderLayout() {
149163

150164
export default function BuilderPage() {
151165
return (
152-
<GameBuilderProvider>
153-
<BuilderLayout />
154-
</GameBuilderProvider>
166+
<CreditsProvider>
167+
<GameBuilderProvider>
168+
<BuilderLayout />
169+
</GameBuilderProvider>
170+
</CreditsProvider>
155171
);
156172
}

apps/web/src/app/login/page.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,14 @@ function LoginContent() {
115115
</div>
116116

117117
{/* Footer note */}
118-
<p className="text-xs text-muted-foreground/70 text-center mt-7">
119-
Guest mode saves only your latest game. Sign in to keep history.
120-
</p>
118+
<div className="text-xs text-muted-foreground/80 text-center mt-7 space-y-2 border-t border-border/40 pt-5">
119+
<p>
120+
🎁 <span className="font-semibold text-slate-700">Daily Free Credits:</span> Guests get 2 credits, Registered users get 5!
121+
</p>
122+
<p className="opacity-80">
123+
Guest mode saves only your latest game. Sign in to keep history.
124+
</p>
125+
</div>
121126
</div>
122127
</div>
123128
</div>

apps/web/src/app/page.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,11 @@ export default function LandingPage() {
164164
</Link>
165165
</motion.div>
166166

167+
{/* Daily Credits Marketing Text */}
168+
<motion.div variants={fadeInUp} className="mt-8 text-sm font-medium text-slate-500 max-w-lg mx-auto pointer-events-none">
169+
Refreshed Daily: <span className="text-indigo-500 font-bold">5 free credits</span> for logged-in users, or <span className="text-slate-600 font-semibold">2 credits</span> as a guest! Every game generation consumes 1 credit.
170+
</motion.div>
171+
167172
{/* Social Proof Placeholder */}
168173
<motion.div
169174
variants={fadeInUp}
@@ -414,6 +419,7 @@ export default function LandingPage() {
414419
</Link>
415420
</div>
416421
<p className="mt-8 text-sm text-slate-400 font-medium">No credit card required. Free tier includes Gemini 3.1 Pro access.</p>
422+
<p className="mt-3 text-sm text-slate-400 font-medium">Guests receive 2 free credits daily, logged in users receive 5.</p>
417423
</motion.div>
418424
</section>
419425

0 commit comments

Comments
 (0)