Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion loopin-web/.env
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VITE_API_URL=https://loopin-1-77vi.onrender.com/api
VITE_CONTRACT_ADDRESS=ST36BMEQDCRCKYF8HPPDMN1BCSY6TR2NG0BZSQPYG
VITE_CONTRACT_ADDRESS=ST4XJYMD9FCF3PZXAKFRTSXX9CHRSEDS6BJ4ASFQ
VITE_CONTRACT_NAME=loopin-game
VITE_NETWORK=testnet
10 changes: 10 additions & 0 deletions loopin-web/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Backend API URL
VITE_API_URL=http://localhost:3000/api

# Wallet Connect (for future use)
VITE_WALLET_CONNECT_PROJECT_ID=

# Smart Contract
VITE_CONTRACT_ADDRESS=ST36BMEQDCRCKYF8HPPDMN1BCSY6TR2NG0BZSQPYG
VITE_CONTRACT_NAME=loopin-game
VITE_NETWORK=testnet
73 changes: 0 additions & 73 deletions loopin-web/README.md

This file was deleted.

24 changes: 18 additions & 6 deletions loopin-web/src/components/dashboard/ActiveSessionsList.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import { Users, Clock, ArrowUpRight } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { SlideUp, StaggerContainer } from '@/components/animation/MotionWrapper';
import { Game } from '@/lib/api';
import { joinGame } from '@/lib/contracts';

interface ActiveSessionsListProps {
activeSessions: Game[];
}

const ActiveSessionsList: React.FC<ActiveSessionsListProps> = ({ activeSessions }) => {
const navigate = useNavigate();

const handleJoin = (session: Game) => {
// Parse ID as int because contract expects uint
// Ensure entry_fee is number
joinGame(parseInt(session.id), session.entry_fee, () => {
navigate(`/game/${session.id}`);
});
};

return (
<div>
<div className="flex items-center justify-between mb-8 md:mb-12">
Expand Down Expand Up @@ -55,11 +66,12 @@ const ActiveSessionsList: React.FC<ActiveSessionsListProps> = ({ activeSessions
<div className="font-display text-lg md:text-xl font-bold">{session.entry_fee} STX</div>
</div>

<Link to={`/game/${session.id}`} className="block">
<Button className="h-12 w-12 md:h-14 md:w-14 rounded-full bg-[#09090B] hover:bg-[#D4FF00] hover:text-black p-0 flex items-center justify-center transition-colors">
<ArrowUpRight className="w-5 h-5 md:w-6 md:h-6 text-[#D4FF00]" />
</Button>
</Link>
<Button
onClick={() => handleJoin(session)}
className="h-12 w-12 md:h-14 md:w-14 rounded-full bg-[#09090B] hover:bg-[#D4FF00] hover:text-black p-0 flex items-center justify-center transition-colors"
>
<ArrowUpRight className="w-5 h-5 md:w-6 md:h-6 text-[#D4FF00]" />
</Button>
</div>
</div>
</div>
Expand Down
73 changes: 70 additions & 3 deletions loopin-web/src/components/dashboard/DashboardActionGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@

import React from 'react';
import { Gift, Zap, X } from 'lucide-react';
import React, { useState } from 'react';
import { Gift, Zap, X, MapPin } from 'lucide-react';
import { Dialog, DialogContent, DialogTrigger, DialogClose } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { createGame } from '@/lib/contracts';
import { SlideUp } from '@/components/animation/MotionWrapper';
import PowerupShop from './PowerupShop';
import DailyRewardCard from './DailyRewardCard';
Expand All @@ -19,8 +22,72 @@ const DashboardActionGrid: React.FC<DashboardActionGridProps> = ({
onBalanceUpdate,
onRewardClaimed
}) => {
const [gameType, setGameType] = useState("Standard");
const [maxPlayers, setMaxPlayers] = useState(10);

const handleCreateGame = () => {
createGame(gameType, maxPlayers);
};

return (
<div className="grid grid-cols-2 gap-3 md:gap-6 mb-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 md:gap-6 mb-8">
{/* Create Game Trigger */}
<Dialog>
<DialogTrigger asChild>
<div className="cursor-pointer group h-full">
<SlideUp delay={0.2} className="h-full bg-black border border-white/20 hover:border-[#D4FF00] rounded-[32px] p-6 md:p-8 flex flex-col justify-between transition-all duration-300 hover:-translate-y-1 relative overflow-hidden shadow-xl">
<div className="flex items-start justify-between mb-6">
<div className="w-12 h-12 rounded-2xl bg-[#D4FF00] flex items-center justify-center text-black group-hover:scale-110 transition-transform">
<MapPin className="w-6 h-6" />
</div>
<div className="text-[10px] font-bold bg-[#D4FF00] text-black px-3 py-1.5 rounded-full uppercase tracking-widest">
New
</div>
</div>
<div>
<h3 className="font-display text-xl font-bold uppercase leading-none mb-2 text-white">Start Grid</h3>
<p className="text-gray-400 text-sm font-medium line-clamp-1">Create a new game.</p>
</div>
</SlideUp>
</div>
</DialogTrigger>
<DialogContent className="w-[95vw] max-w-[425px] p-0 bg-transparent border-none shadow-none focus:outline-none [&>button]:hidden">
<div className="bg-white rounded-[32px] overflow-hidden p-6 shadow-2xl relative">
<DialogClose className="absolute right-4 top-4 z-50 p-2 bg-gray-100 hover:bg-[#D4FF00] rounded-full transition-colors">
<X className="w-4 h-4 text-black" />
</DialogClose>

<h2 className="font-display text-2xl font-black uppercase mb-4">Launch Grid</h2>

<div className="space-y-4">
<div>
<label className="text-xs font-bold uppercase text-gray-500 mb-1 block">Game Name</label>
<Input
value={gameType}
onChange={(e) => setGameType(e.target.value)}
className="font-bold border-2 border-black h-12 rounded-xl"
/>
</div>
<div>
<label className="text-xs font-bold uppercase text-gray-500 mb-1 block">Max Runners</label>
<Input
type="number"
value={maxPlayers}
onChange={(e) => setMaxPlayers(parseInt(e.target.value))}
className="font-bold border-2 border-black h-12 rounded-xl"
/>
</div>

<Button
onClick={handleCreateGame}
className="w-full h-14 bg-[#D4FF00] hover:bg-[#bce600] text-black font-black uppercase text-lg rounded-xl mt-4"
>
Deploy Contract
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Daily Reward Trigger */}
<Dialog>
<DialogTrigger asChild>
Expand Down
59 changes: 59 additions & 0 deletions loopin-web/src/lib/contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { openContractCall } from '@stacks/connect';
import { STACKS_TESTNET } from '@stacks/network';
import { Cl, PostConditionMode } from '@stacks/transactions';

const network = STACKS_TESTNET;
const contractAddress = import.meta.env.VITE_CONTRACT_ADDRESS;
const contractName = import.meta.env.VITE_CONTRACT_NAME || 'loopin-game';

export const createGame = (initialGameType: string, initialMaxPlayers: number) => {
if (!contractAddress) {
alert("Contract address not set in .env!");
return;
}

const options = {
contractAddress,
contractName,
functionName: 'create-game',
functionArgs: [
Cl.stringAscii(initialGameType),
Cl.uint(initialMaxPlayers)
],
network,
postConditionMode: PostConditionMode.Allow, // Simplest for now
onFinish: (data: any) => {
console.log('Transaction broadcasted:', data);
alert(`Game Creation Transaction Broadcasted! TxId: ${data.txId}`);
// Ideally we reload window or poll
setTimeout(() => window.location.reload(), 2000);
},
};

openContractCall(options);
};

export const joinGame = (gameId: number, entryFee: number, onSuccess?: () => void) => {
if (!contractAddress) {
alert("Contract address not set in .env!");
return;
}

const options = {
contractAddress,
contractName,
functionName: 'join-game',
functionArgs: [
Cl.uint(gameId)
],
network,
postConditionMode: PostConditionMode.Allow, // Allow STX transfer
onFinish: (data: any) => {
console.log('Transaction broadcasted:', data);
// alert(`Joined Game! TxId: ${data.txId}`);
if (onSuccess) onSuccess();
},
};

openContractCall(options);
};
45 changes: 30 additions & 15 deletions loopin-web/src/lib/wallet-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,39 +43,54 @@ export const connectWalletDesktop = (
userSession: UserSession,
onFinish?: () => void
) => {
console.log('[Wallet] Starting authentication...');
console.log('[Wallet] Is mobile?', isMobileDevice());
console.log('[Wallet] User agent:', navigator.userAgent);
console.log('[Wallet] 🚀 Starting authentication...');

authenticate({
appDetails: {
name: "Loopin",
icon: window.location.origin + "/logo.svg",
},
onFinish: (data: any) => {
console.log('[Wallet] onFinish called with data:', data);
console.log('[Wallet] ✅ Authentication successful!');
console.log('[Wallet] Data received:', data);

// Save wallet address to localStorage
// CRITICAL: Save wallet address IMMEDIATELY
try {
// Method 1: Try to get from userSession
if (userSession.isUserSignedIn()) {
const userData = userSession.loadUserData();
const walletAddress = userData.profile.stxAddress.mainnet;
console.log('[Wallet] Saving wallet address:', walletAddress);
console.log('[Wallet] ✅ Got wallet from session:', walletAddress);
localStorage.setItem('loopin_wallet', walletAddress);

// Trigger storage event for Header to detect
window.dispatchEvent(new StorageEvent('storage', {
key: 'loopin_wallet',
newValue: walletAddress,
url: window.location.href
}));

console.log('[Wallet] ✅ Wallet saved to localStorage');

// Call callback if provided
if (onFinish) {
onFinish();
}

// Force page reload to update all components
setTimeout(() => {
console.log('[Wallet] 🔄 Reloading page...');
window.location.reload();
}, 500);
} else {
console.error('[Wallet] ❌ User not signed in after authentication');
}
} catch (error) {
console.error('[Wallet] Error saving wallet address:', error);
}

if (onFinish) {
onFinish();
} else {
// Reload to update UI
window.location.reload();
console.error('[Wallet] ❌ Error saving wallet:', error);
}
},
onCancel: () => {
console.log('[Wallet] User cancelled connection');
console.log('[Wallet] User cancelled connection');
},
userSession,
});
Expand Down
44 changes: 44 additions & 0 deletions loopin-web/src/types/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Player Profile Type
export interface PlayerProfile {
id: string;
wallet_address: string;
username: string;
avatar_seed: string;
level: number;
joined_at: string;
}

// Player Stats Type
export interface PlayerStats {
player_id: string;
total_area: number;
games_played: number;
games_won: number;
total_earnings: number;
current_streak: number;
}

// Game Session Type
export interface GameSession {
id: string;
game_type: string;
status: string;
max_players: number;
entry_fee: number;
prize_pool: number;
creator_wallet: string;
created_at: string;
start_time?: string;
end_time?: string;
}

// Game Participant Type
export interface GameParticipant {
id: string;
game_id: string;
player_id: string;
area_captured: number;
rank: number;
prize_won: number;
joined_at: string;
}