+ {/* Progress Steps */}
+
+
+
+ {[1, 2, 3, 4].map((stepNumber) => (
+
+
+ {getStepIcon(stepNumber)}
+
+ {stepNumber < 4 && (
+
)}
+ ))}
+
+
+
+
+
= 1 ? 'text-blue-600 dark:text-blue-400' : 'text-slate-500 dark:text-slate-400'}`}>
+ Game Info
+
+
= 2 ? 'text-blue-600 dark:text-blue-400' : 'text-slate-500 dark:text-slate-400'}`}>
+ Game Type
+
+
= 3 ? 'text-blue-600 dark:text-blue-400' : 'text-slate-500 dark:text-slate-400'}`}>
+ Upload Files
+
+
= 4 ? 'text-blue-600 dark:text-blue-400' : 'text-slate-500 dark:text-slate-400'}`}>
+ Complete
+
+
+
+
+ {/* Step Content */}
+
+ {step === 1 && (
+
+
Game Information
+
+
+
+
+
+
setGameId(e.target.value)}
+ placeholder="e.g., my-awesome-game"
+ className="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 placeholder-slate-500 dark:placeholder-slate-400 focus:border-blue-500 dark:focus:border-blue-400 focus:outline-none transition-colors duration-200"
+ />
+ {gameId && (
+
+ )}
+ {available !== null && (
+
+ {available ? 'Available' : 'Already taken'}
+
+ )}
+
+
-
-
+
+
+ setGameName(e.target.value)}
+ placeholder="Enter your game name"
+ className="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 placeholder-slate-500 dark:placeholder-slate-400 focus:border-blue-500 dark:focus:border-blue-400 focus:outline-none transition-colors duration-200"
+ />
+
+
-
)}
{step === 2 && (
- <>
-
-
-
-
-
- {/*
- {
- setHasStreamingAssets(e.target.checked);
- setStreamingAssetsFiles(null);
- }}
- style={{ marginRight: 6 }}
- />
- Has Streaming Assets?
- */}
-
- {/* {hasStreamingAssets && (
-
-
- Upload StreamingAssets Folder:
-
-
-
- Select the entire StreamingAssets folder to preserve structure
-
+
+
Select Game Type
+
+
+
setGameType('unity')}
+ className={`p-6 border-2 rounded-xl cursor-pointer transition-all duration-200 ${
+ gameType === 'unity'
+ ? 'border-blue-500 bg-blue-50 dark:bg-blue-950/50'
+ : 'border-slate-300 dark:border-slate-600 hover:border-slate-400 dark:hover:border-slate-500'
+ }`}
+ >
+
+
UNITY
+
Unity WebGL
+
+ Upload Unity WebGL builds with .data, .wasm, .js files
+
+
- )} */}
+
setGameType('html')}
+ className={`p-6 border-2 rounded-xl cursor-pointer transition-all duration-200 ${
+ gameType === 'html'
+ ? 'border-blue-500 bg-blue-50 dark:bg-blue-950/50'
+ : 'border-slate-300 dark:border-slate-600 hover:border-slate-400 dark:hover:border-slate-500'
+ }`}
+ >
+
+
HTML5
+
HTML5 Game
+
+ Upload HTML5 games with HTML, CSS, JS files
+
+
+
+
-
+
+
+
+
+ )}
+
+ {step === 3 && (
+
+
Upload Game Files
+
+ {gameType === 'unity' ? (
+
+
+
+
+
+
+
+
+
+
+ setHasStreamingAssets(e.target.checked)}
+ className="w-4 h-4 text-blue-600 bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-blue-500"
+ />
+ Has Streaming Assets
+
+
+
+ {hasStreamingAssets && (
+
+ )}
+
+ ) : gameType === 'html' ? (
+
+ {/* Upload Method Selection */}
+
+
Upload Method
+
+
+
+
+
+
+ {/* Upload Interface */}
+ {uploadType === 'folder' ? (
+
+
+ Select Game Folder
+
+
setHtmlFiles(e.target.files)}
+ className="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 focus:border-blue-500 dark:focus:border-blue-400 focus:outline-none transition-colors duration-200"
+ />
+
+ Select the entire folder containing your HTML5 game files
+
+
+ ) : (
+
+
+ Select Game Files
+
+
setHtmlFiles(e.target.files)}
+ className="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 focus:border-blue-500 dark:focus:border-blue-400 focus:outline-none transition-colors duration-200"
+ />
+
+ Select all files for your HTML5 game (HTML, CSS, JS, images, audio, etc.)
+
+
+ )}
+ {/* File List Preview */}
+ {htmlFiles && htmlFiles.length > 0 && (
+
+
+ Selected Files ({htmlFiles.length})
+
+
+ {Array.from(htmlFiles).slice(0, 10).map((file, index) => (
+
+ {uploadType === 'folder' && file.webkitRelativePath
+ ? file.webkitRelativePath
+ : file.name}
+
+ ))}
+ {htmlFiles.length > 10 && (
+
+ ... and {htmlFiles.length - 10} more files
+
+ )}
+
+
+ )}
+
+ {/* Requirements Info */}
+
+
HTML5 Game Requirements
+
+ - • Must include at least one HTML file (index.html recommended)
+ - • All assets should be relative paths (no absolute URLs)
+ - • Game should work offline without external dependencies
+ - • Maximum total size: 100MB
+
+
+
+ ) : null}
+
+
+
+ {gameType === 'html' && canSubmit() && (
+
+ )}
+
+
- >
+
)}
- >
- )}
- {uploaded &&
Unity Build Uploaded! You can now preview the game.
}
+ {step === 4 && (
+
+
+ DONE
+
+
+ Game Published Successfully!
+
+
+ Your game "{gameName}" is now live in the Arena. Players can discover and play it!
+
+
+
+ )}
+
+
+
+ {/* Preview Modal */}
+ {previewing && previewUrl && (
+
+
+
+
Game Preview
+
+
+
+
+
+
+
+ )}
);
-}
+}
\ No newline at end of file
diff --git a/app/contribute/publish/publish.module.css b/app/contribute/publish/publish.module.css
deleted file mode 100644
index 2a875ce..0000000
--- a/app/contribute/publish/publish.module.css
+++ /dev/null
@@ -1,163 +0,0 @@
-.wrapper {
- max-width: 600px;
- margin: 40px auto;
- padding: 20px;
- background-color: #1b1b1b85;
- border-radius: 12px;
- box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
- color: #f1f1f9;
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- box-shadow: 0px 0px 10px #ffffff6c;
-}
-
-.wrapper .title {
- font-size: 2rem;
- font-weight: 700;
- margin-bottom: 30px;
- text-align: center;
- color: #ffffff;
-}
-
-.wrapper label {
- display: flex;
- flex-direction: column;
- margin-bottom: 18px;
- font-weight: 600;
- font-size: 1rem;
- color: #ffffffcb;
-}
-
-.uploader {
- text-align: center;
- font-weight: 600;
- font-size: 14px;
- color: rgb(255, 227, 66);
-}
-
-/*
-.checkForAvailability {
- width: max-content;
- padding: 10px 10px;
- background-color: rgb(255, 69, 69);
- outline: none;
- border: none;
- color: rgb(255, 255, 255);
- font-size: 14px;
- font-weight: 700;
- border-radius: 10px;
- box-shadow: 0 0 5px rgb(255, 81, 81);
-} */
-
-.checkForAvailability {
- display: inline-flex;
- align-items: center;
- gap: 0.75rem;
- background: linear-gradient(135deg, #7e7473d2, #7e7473d2);
- border: none;
- padding: 10px 10px;
- font-size: 14px;
- font-weight: 700;
- color: white;
- border-radius: 12px;
- cursor: pointer;
- transition: all 0.3s ease;
-}
-
-.checkForAvailability:hover {
- background: linear-gradient(135deg, #ff4a4a, #ff4a4a);
- transform: scale(1.05);
-}
-
-.wrapper input[type='text'],
-.wrapper textarea,
-.wrapper input[type='file'] {
- margin-top: 6px;
- padding: 10px 12px;
- border-radius: 6px;
- border: 1px solid #ffffff;
- background-color: #ffffff;
- color: #000000;
- font-size: 14px;
- outline-offset: 2px;
- transition: border-color 0.3s ease;
-}
-
-.wrapper input[type='text']:focus,
-.wrapper textarea:focus,
-.wrapper input[type='file']:focus {
- border: 2px solid #0d6eff;
- outline: #0d6eff;
- font-weight: 700;
-}
-
-.wrapper textarea {
- resize: vertical;
- min-height: 80px;
- max-height: 160px;
- font-family: inherit;
-}
-
-.wrapper .buttonGroup {
- display: flex;
- justify-content: flex-end;
- gap: 12px;
- margin-top: 20px;
-}
-
-.wrapper .backBtn {
- background-color: #ef4444;
- border: none;
- border-radius: 6px;
- color: white;
- padding: 12px 24px;
- font-weight: 700;
- cursor: pointer;
- transition: background-color 0.3s ease;
-}
-
-.wrapper .backBtn:hover {
- background-color: #b91c1c;
-}
-
-/* .wrapper .uploadBtn {
- background-color: #3b82f6;
- border: none;
- border-radius: 6px;
- color: white;
- padding: 12px 24px;
- font-weight: 700;
- cursor: pointer;
- transition: background-color 0.3s ease;
-}
-
-.wrapper .uploadBtn:hover {
- background-color: #2563eb;
-} */
-
-.wrapper p {
- text-align: center;
- font-size: 1.2rem;
- font-weight: 600;
- color: #22c55e;
-}
-
-
-.uploadBtn {
- display: inline-flex;
- align-items: center;
- gap: 0.75rem;
- background: linear-gradient(135deg, #65626d, #8e869b);
- border: none;
- padding: 0.75rem 1.5rem;
- font-size: 1rem;
- font-weight: 600;
- color: white;
- border-radius: 12px;
- cursor: pointer;
- transition: all 0.3s ease;
-}
-
-.uploadBtn:hover {
- background: linear-gradient(135deg, #2f5988, #0c7dff);
- transform: scale(1.05);
-}
\ No newline at end of file
diff --git a/app/coupon/redeem/coupon.module.css b/app/coupon/redeem/coupon.module.css
deleted file mode 100644
index 25bc35a..0000000
--- a/app/coupon/redeem/coupon.module.css
+++ /dev/null
@@ -1,72 +0,0 @@
-.container {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- min-height: 100vh;
- padding: 1rem;
- user-select: none;
- text-align: center;
-}
-
-.infoText {
- margin-top: 2rem;
- font-weight: 600;
- color: #4a2503;
-}
-
-.couponWrapper {
- position: relative;
- width: 100%;
- max-width: 500px;
- border-radius: 16px;
- box-shadow: 0 0 24px rgba(255, 215, 0, 0.4);
- cursor: pointer;
- touch-action: none;
- user-select: none;
-}
-
-.couponWrapper.redeemed {
- cursor: default;
-}
-
-.couponImage {
- width: 100%;
- border-radius: 16px;
- user-select: none;
- -webkit-user-drag: none;
- filter: grayscale(100%);
- transition: filter 0.4s ease;
-}
-
-.couponImage.redeemed {
- filter: none;
-}
-
-.scratchCanvas {
- position: absolute;
- top: 0;
- left: 0;
- border-radius: 16px;
- width: 100%;
- height: 100%;
- touch-action: none;
- user-select: none;
-}
-
-.redeemingText {
- margin-top: 20px;
- font-weight: bold;
- color: #4a2503;
-}
-
-.couponOverlay {
- position: absolute;
- bottom: 8px;
- right: 8px;
- color: #4a2503;
- font-size: .7rem;
- padding: 6px 12px;
- pointer-events: none;
- z-index: 2;
-}
\ No newline at end of file
diff --git a/app/coupon/redeem/page.tsx b/app/coupon/redeem/page.tsx
index 219078b..20c8043 100644
--- a/app/coupon/redeem/page.tsx
+++ b/app/coupon/redeem/page.tsx
@@ -1,12 +1,11 @@
'use client';
-import { useSession } from 'next-auth/react';
+import { useSession, signOut, signIn } from 'next-auth/react';
import SignIn from '@/components/auth/SignIn';
import { useState, useRef, useEffect } from 'react';
-import { getUserDataFromPlayFab, updatePlayFabUserData } from '@/lib/playfab/playfab';
+import { getUserDataFromPlayFab, updatePlayFabUserData, playFabLoginWithAzureAD } from '@/lib/playfab/playfab';
import Popup from '@/components/popup/Popup';
import { useSearchParams } from 'next/navigation';
-import styles from './coupon.module.css';
export default function ScratchToRedeem() {
const { data: session, status } = useSession();
@@ -26,13 +25,23 @@ export default function ScratchToRedeem() {
}, [searchParams]);
useEffect(() => {
+ const initializeCanvas = () => {
if (!canvasRef.current || !containerRef.current) return;
+
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Set canvas size to container size
const rect = containerRef.current.getBoundingClientRect();
+
+ // Check if dimensions are valid
+ if (rect.width <= 0 || rect.height <= 0) {
+ console.log('Container not ready, retrying...');
+ setTimeout(initializeCanvas, 100);
+ return;
+ }
+
canvas.width = rect.width;
canvas.height = rect.height;
@@ -50,6 +59,22 @@ export default function ScratchToRedeem() {
noise.data[i + 3] = 255;
}
ctx.putImageData(noise, 0, 0);
+ };
+
+ // Initialize canvas after a short delay to ensure DOM is ready
+ const timer = setTimeout(initializeCanvas, 100);
+
+ // Handle window resize
+ const handleResize = () => {
+ setTimeout(initializeCanvas, 50);
+ };
+
+ window.addEventListener('resize', handleResize);
+
+ return () => {
+ clearTimeout(timer);
+ window.removeEventListener('resize', handleResize);
+ };
}, [couponCode]);
const startDrawing = (e: React.MouseEvent | React.TouchEvent) => {
@@ -70,8 +95,8 @@ export default function ScratchToRedeem() {
const rect = canvas.getBoundingClientRect();
if ('touches' in e && e.touches.length > 0) {
- return {
- x: e.touches[0].clientX - rect.left,
+ return {
+ x: e.touches[0].clientX - rect.left,
y: e.touches[0].clientY - rect.top,
};
} else if ('clientX' in e) {
@@ -95,7 +120,7 @@ export default function ScratchToRedeem() {
ctx.globalCompositeOperation = 'destination-out';
ctx.beginPath();
- ctx.arc(pos.x, pos.y, 50, 0, Math.PI * 2, false); // brush radius 20px
+ ctx.arc(pos.x, pos.y, 50, 0, Math.PI * 2, false); // brush radius 50px
ctx.fill();
};
@@ -105,125 +130,256 @@ export default function ScratchToRedeem() {
const ctx = canvas.getContext('2d');
if (!ctx) return;
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
- const pixels = imageData.data;
- let transparentPixels = 0;
+ // Check if canvas has valid dimensions
+ if (canvas.width <= 0 || canvas.height <= 0) return;
+
+ try {
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const pixels = imageData.data;
+ let transparentPixels = 0;
+
+ for (let i = 3; i < pixels.length; i += 4) {
+ if (pixels[i] === 0) transparentPixels++;
+ }
+
+ const scratchedPercent = (transparentPixels / (canvas.width * canvas.height)) * 100;
- for (let i = 3; i < pixels.length; i += 4) {
- if (pixels[i] === 0) transparentPixels++;
+ // Auto redeem if scratched more than 50%
+ if (scratchedPercent > 50 && !redeemed) {
+ redeemCoupon();
+ }
+ } catch (error) {
+ console.error('Error checking scratch percent:', error);
}
+ };
- const scratchedPercent = (transparentPixels / (canvas.width * canvas.height)) * 100;
+ const refreshPlayFabSession = async () => {
+ if (!session) throw new Error('No session available');
+
+ const titleId = process.env.NEXT_PUBLIC_PLAYFAB_TITLE_ID;
+ if (!titleId) throw new Error('Title ID not found');
+
+ console.log('Refreshing PlayFab session...');
+ const loginResult = await playFabLoginWithAzureAD(titleId, session);
+
+ if (loginResult.error) throw new Error(`Failed to refresh session: ${loginResult.error.errorMessage}`);
+
+ return loginResult.data?.SessionTicket;
+ };
- // Auto redeem if scratched more than 50%
- if (scratchedPercent > 50 && !redeemed) {
- redeemCoupon();
+ const refreshUserSession = async () => {
+ console.log('Mail sending failed, refreshing session...');
+ try {
+ await signOut({ redirect: false });
+ console.log('Signed out successfully');
+
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ const signInResult = await signIn('azure-ad', {
+ redirect: false,
+ callbackUrl: window.location.href
+ });
+
+ return signInResult?.ok;
+ } catch (error) {
+ console.error('Sign out/re-sign in error:', error);
+ return false;
}
};
- const redeemCoupon = async () => {
- setRedeemed(true);
+ const sendMailAndUpdatePlayFab = async (sessionTicket: string, coupons: string[], couponCode: string, titleId: string) => {
+ const mailRes = await fetch('/api/send-coupon-mail', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ coupon: couponCode }),
+ });
+
+ if (mailRes.ok) {
+ console.log('Mail sent successfully, updating PlayFab data...');
+ const updatedCoupons = coupons.filter((c: string) => c !== couponCode);
+ await updatePlayFabUserData(sessionTicket, titleId, { Coupons: JSON.stringify(updatedCoupons) });
+ console.log('Coupon removed from PlayFab inventory');
+ return { success: true, message: 'Coupon verified and mail sent! Please check your email!' };
+ } else {
+ const errorText = await mailRes.text();
+ console.error('Mail sending failed:', errorText);
+ return { success: false, error: errorText };
+ }
+ };
- // Clear scratch overlay fully
+ const clearScratchOverlay = () => {
const canvas = canvasRef.current;
if (canvas) {
const ctx = canvas.getContext('2d');
- if (ctx) {
- ctx.clearRect(0, 0, canvas.width, canvas.height);
+ if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);
+ }
+ };
+
+ const getSessionTicket = async () => {
+ let sessionTicket = sessionStorage.getItem('playfabSessionTicket');
+ if (!sessionTicket) {
+ console.log('No session ticket found, refreshing...');
+ const newTicket = await refreshPlayFabSession();
+ if (!newTicket) throw new Error('Failed to get new session ticket');
+ sessionTicket = newTicket;
+ }
+ return sessionTicket;
+ };
+
+ const getUserDataWithRetry = async (sessionTicket: string, titleId: string) => {
+ try {
+ return await getUserDataFromPlayFab(sessionTicket, titleId);
+ } catch (error: any) {
+ if (error.message?.includes('expired') || error.message?.includes('invalid')) {
+ console.log('Session expired, refreshing...');
+ const newTicket = await refreshPlayFabSession();
+ if (!newTicket) throw new Error('Failed to refresh session ticket');
+ return await getUserDataFromPlayFab(newTicket, titleId);
}
+ throw error;
}
+ };
+
+ const handleRetryAfterSessionRefresh = async (coupons: string[], couponCode: string, titleId: string) => {
+ await new Promise(resolve => setTimeout(resolve, 2000));
+ console.log('Retrying mail sending after session refresh...');
+
+ const freshSessionTicket = sessionStorage.getItem('playfabSessionTicket');
+ if (!freshSessionTicket) {
+ showPopup('Mail sent but failed to update inventory. Please contact support.', 'error');
+ return;
+ }
+
+ const result = await sendMailAndUpdatePlayFab(freshSessionTicket, coupons, couponCode, titleId);
+ if (result.success) {
+ showPopup('Session refreshed and coupon redeemed successfully! Please check your email!', 'success');
+ } else {
+ showPopup('Mail sending failed even after session refresh. Please try again later.', 'error');
+ }
+ };
+ const redeemCoupon = async () => {
+ setRedeemed(true);
+ clearScratchOverlay();
setLoading(true);
- const sessionTicket = sessionStorage.getItem('playfabSessionTicket');
const titleId = process.env.NEXT_PUBLIC_PLAYFAB_TITLE_ID;
-
- if (!sessionTicket || !titleId) {
- showPopup('Missing session ticket or title ID', 'error');
+ if (!titleId) {
+ showPopup('Title ID not configured', 'error');
setLoading(false);
return;
}
try {
- const userDataRes = await getUserDataFromPlayFab(sessionTicket, titleId);
+ const sessionTicket = await getSessionTicket();
+ const userDataRes = await getUserDataWithRetry(sessionTicket, titleId);
+
const coupons = userDataRes?.data?.Data?.Coupons?.Value
? JSON.parse(userDataRes.data.Data.Coupons.Value)
: [];
- if (coupons.includes(couponCode)) {
- const mailRes = await fetch('/api/send-coupon-mail', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ coupon: couponCode }),
- });
-
- if (mailRes.ok) {
- // Remove the used coupon from the array
- const updatedCoupons = coupons.filter((c: string) => c !== couponCode);
- // Update PlayFab UserData
- await updatePlayFabUserData(sessionTicket, titleId, { Coupons: JSON.stringify(updatedCoupons) });
+ if (!coupons.includes(couponCode)) {
+ console.log('Coupon not found in user inventory:', couponCode);
+ showPopup('Invalid coupon code or already used.', 'error');
+ return;
+ }
- showPopup('Coupon verified! Please check your mail!', 'success');
+ console.log('Coupon verified in PlayFab, sending mail...');
+ const result = await sendMailAndUpdatePlayFab(sessionTicket, coupons, couponCode, titleId);
+
+ if (result.success) {
+ showPopup(result.message!, 'success');
+ } else {
+ const sessionRefreshed = await refreshUserSession();
+ if (sessionRefreshed) {
+ await handleRetryAfterSessionRefresh(coupons, couponCode, titleId);
} else {
- const errorText = await mailRes.text();
- showPopup('Coupon valid, but failed to send mail. ' + errorText, 'error');
+ showPopup('Session refresh failed. Please try logging in again.', 'error');
}
- } else {
- showPopup('Invalid coupon code.', 'error');
}
- } catch (err) {
- showPopup('Error verifying coupon.', 'error');
+ } catch (err: any) {
+ console.error('Redeem error:', err);
+ showPopup(`Error verifying coupon: ${err.message}`, 'error');
} finally {
setLoading(false);
}
};
-
const showPopup = (message: string, type: 'success' | 'error' = 'success') => {
setPopup({ message, type });
setTimeout(() => setPopup(null), 3000);
};
- if (status === 'loading') return
Loading...
;
- if (!session) return
;
-
- return (
-
-
-
-

- {!redeemed && (
-
- )}
- {(redeemed && couponCode) && (
-
- {couponCode}
-
)}
+ if (status === 'loading') {
+ return (
+
+ );
+ }
-
(Scratch to Redeem)
-
-
- {loading &&
Redeeming...
}
+ if (!session) {
+ return
;
+ }
+ return (
+
+ {/* Coupon Container */}
+
+
+

+ {!redeemed && (
+
+ )}
+ {redeemed && couponCode && (
+
+
+
SUCCESS!
+
+ {couponCode}
+
+
+
+ )}
+
+
+
+ {/* Info Text */}
+
+ (Scratch to Redeem)
+
+
+ {/* Loading Text */}
+ {loading && (
+
+ )}
+
+ {/* Popup */}
{popup && (
)}
-
);
-}
+}
\ No newline at end of file
diff --git a/app/dashboard/dashboard.module.css b/app/dashboard/dashboard.module.css
deleted file mode 100644
index 784f412..0000000
--- a/app/dashboard/dashboard.module.css
+++ /dev/null
@@ -1,610 +0,0 @@
-.page {
- min-height: 100vh;
- padding: 2rem;
- background: #121214;
- color: #cfd8dc;
- display: flex;
- flex-direction: column;
- align-items: center;
-}
-
-.tagline {
- font-size: medium;
- color: #e4d838;
- margin-top: 0;
- margin-bottom: 0.5rem;
- user-select: none;
- text-align: left;
- width: 100%;
-}
-
-.header {
- width: 100%;
- margin-bottom: 2rem;
- display: flex;
- justify-content: space-between;
- align-items: center;
- border-bottom: 1px solid #222d38;
- padding-bottom: 0.5rem;
-}
-
-.title {
- font-weight: 700;
- font-size: 1.5rem;
- color: #fcfcfc;
- user-select: none;
-}
-
-.playfabId {
- font-size: 1rem;
- color: #ffffff71;
- font-weight: 600;
- user-select: text;
- background: #08151c;
- padding: 0.25rem 0.75rem;
- border-radius: 12px;
- letter-spacing: 0.05em;
-}
-
-.error {
- margin: 1rem auto;
- color: #ff5555;
- background: #330000;
- padding: 0.75rem 1rem;
- border-radius: 8px;
- font-weight: 600;
- text-align: center;
- user-select: none;
- box-shadow: 0 0 8px #ff5555;
-}
-
-.loading {
- margin: 1rem auto;
- color: #fcfcfc;
- font-weight: 600;
- user-select: none;
- text-align: center;
-}
-
-.grid {
- width: 100%;
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
- gap: 1.5rem;
-}
-
-.card {
- position: relative;
- background: #0000006c;
- border-radius: 12px;
- padding: 1.5rem 2rem;
- width: 100%;
- max-width: 400px;
- margin-bottom: 2rem;
- z-index: 0;
-}
-
-.card:hover {
- transition: all .3s ease;
- background: linear-gradient(#3d3d3db9, #27272760);
- box-shadow:
- 0 0 5px rgba(98, 171, 255, 0.15),
- inset 0 0 15px rgba(19, 121, 255, 0.1);
-}
-
-.cardTitle {
- margin-bottom: 1rem;
- font-weight: 700;
- font-size: 1.5rem;
- color: #fcfcfc;
- border-bottom: 2px solid #ffffff71;
- padding-bottom: 0.5rem;
- user-select: none;
-}
-
-.empty {
- color: #5c677d;
- font-style: italic;
- user-select: none;
-}
-
-.statList,
-.currencyList,
-.userDataList {
- list-style: none;
- padding: 0;
- margin: 0;
- max-height: 180px;
- overflow-y: auto;
- scrollbar-width: thin;
- scrollbar-color: #fcfcfc transparent;
-}
-
-.statItem,
-.currencyItem,
-.userDataItem {
- display: flex;
- justify-content: space-between;
- padding: 0.4rem 0;
- border-bottom: 1px solid #222d38;
- font-weight: 600;
- user-select: text;
-}
-
-.statName,
-.currencyCode,
-.userDataKey {
- color: #ffffff71;
- letter-spacing: 0.05em;
-}
-
-.statValue,
-.currencyAmount,
-.userDataValue {
- color: #cfd8dc;
- font-weight: 700;
-}
-
-.statList::-webkit-scrollbar,
-.currencyList::-webkit-scrollbar,
-.userDataList::-webkit-scrollbar {
- width: 8px;
-}
-
-.statList::-webkit-scrollbar-thumb,
-.currencyList::-webkit-scrollbar-thumb,
-.userDataList::-webkit-scrollbar-thumb {
- background: #fcfcfc;
- border-radius: 4px;
-}
-
-@media (max-width: 480px) {
- .page {
- padding: 1rem;
- }
-
- .title {
- font-size: 2rem;
- }
-}
-
-.skeleton {
- background-color: #e0e0e0;
- border-radius: 4px;
- position: relative;
- overflow: hidden;
- margin-bottom: 8px;
- min-height: 20px;
-}
-
-.skeleton::after {
- content: "";
- position: absolute;
- top: 0;
- left: -150px;
- height: 100%;
- width: 150px;
- background: linear-gradient(90deg,
- transparent,
- rgba(255, 255, 255, 0.3),
- transparent);
- animation: loading 1.5s infinite;
-}
-
-@keyframes loading {
- 100% {
- left: 100%;
- }
-}
-
-.skeleton,
-.skeleton-title,
-.skeleton-item,
-.skeleton-chart {
- position: relative;
- overflow: hidden;
- background-color: #868686;
- border-radius: 4px;
- user-select: none;
- transition: background-color 0.3s ease;
-}
-
-.skeleton::after,
-.skeleton-title::after,
-.skeleton-item::after,
-.skeleton-chart::after {
- content: "";
- position: absolute;
- top: 0;
- left: -150px;
- height: 100%;
- width: 150px;
- background: linear-gradient(90deg,
- rgba(255, 255, 255, 0) 0%,
- rgba(255, 255, 255, 0.4) 50%,
- rgba(255, 255, 255, 0) 100%);
- animation: shimmer 1.5s infinite;
- will-change: left;
-}
-
-@keyframes shimmer {
- 0% {
- left: -150px;
- }
-
- 100% {
- left: 100%;
- }
-}
-
-.skeleton-title {
- width: 60%;
- height: 24px;
- margin-bottom: 12px;
-}
-
-.skeleton-item {
- width: 100%;
- height: 18px;
- margin-bottom: 8px;
-}
-
-.skeleton-chart {
- width: 100%;
- height: 200px;
- border-radius: 8px;
-}
-
-@keyframes pulse {
-
- 0%,
- 100% {
- opacity: 1;
- }
-
- 50% {
- opacity: 0.6;
- }
-}
-
-.skeleton,
-.skeleton-title,
-.skeleton-item,
-.skeleton-chart {
- animation: pulse 1.5s ease-in-out infinite;
-}
-
-.friendPlayfabID {
- color: rgb(110, 110, 110);
-}
-
-.inventoryList,
-.friendList {
- list-style: none;
- padding: 0;
- margin: 0;
-}
-
-.inventoryItem,
-.friendItem {
- padding: 0.5rem 0;
- border-bottom: 1px solid #222;
- font-size: 0.9rem;
-}
-
-.inventoryItem:last-child,
-.friendItem:last-child {
- border-bottom: none;
-}
-
-.inventoryList,
-.friendsList {
- list-style: none;
- padding-left: 0;
-}
-
-.inventoryItem,
-.friendItem {
- padding: 6px 0;
- border-bottom: 1px solid #ddd;
-}
-
-.status {
- margin-top: 8px;
- font-weight: 600;
- color: green;
-}
-
-.copyButton {
- background: none;
- border: none;
- cursor: pointer;
- font-size: 1rem;
- margin-left: 8px;
- color: #0070f3;
- transition: color 0.2s ease;
-}
-
-.copyButton:hover {
- color: #0051a3;
-}
-
-.copiedText {
- position: absolute;
- margin-left: 10px;
- color: green;
- font-weight: 600;
- font-size: 0.8rem;
-}
-
-.skeleton {
- background-color: #ddd;
- border-radius: 4px;
- animation: pulse 1.5s ease-in-out infinite;
- user-select: none;
-}
-
-@keyframes pulse {
- 0% {
- opacity: 1;
- }
-
- 50% {
- opacity: 0.4;
- }
-
- 100% {
- opacity: 1;
- }
-}
-.inventoryList {
- list-style: none;
- padding: 0;
- margin: 0;
- display: grid;
- gap: 1rem;
-}
-
-.inventoryItem {
- background: #121212;
- border: 1px solid #2a2a2a;
- padding: 1rem;
- border-radius: 12px;
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
- transition: background 0.2s ease;
-}
-
-.inventoryItem:hover {
- background: #1a1a1a;
-}
-
-.currencySelect {
- background: #1f1f1f;
- border: 1px solid #3a3a3a;
- color: #fff;
- padding: 0.5rem 1rem;
- border-radius: 6px;
- font-size: 0.6rem;
- appearance: none;
- outline: none;
-}
-
-.buyButton {
- background-color: #720f28;
- color: #ffffff;
- padding: 0.45rem 1rem;
- border-radius: 2px;
- border: none;
- cursor: pointer;
- transition: background 0.2s ease;
-}
-
-.buyButton:hover {
- background-color: #720f286b;
-}
-
-.skeleton-title {
- height: 1.2rem;
- width: 40%;
- background: #2b2b2b;
- border-radius: 6px;
- margin-bottom: 1rem;
-}
-
-.skeleton-item {
- height: 4rem;
- background: #2b2b2b;
- border-radius: 10px;
- margin-bottom: 1rem;
-}
-
-
-/* Skeleton Loading for Store Cards */
-.storeCardSkeleton {
- padding: 1.5rem 2rem;
- border-radius: 12px;
- background: #0000006c;
-}
-
-.skeletonTitle {
- width: 60%;
- height: 24px;
- margin-bottom: 12px;
- border-radius: 4px;
- background-color: #868686;
- position: relative;
- overflow: hidden;
- user-select: none;
-}
-
-.skeletonItem {
- width: 100%;
- height: 18px;
- margin-bottom: 8px;
- border-radius: 4px;
- background-color: #868686;
- position: relative;
- overflow: hidden;
- user-select: none;
-}
-
-.skeletonButton {
- width: 100%;
- height: 36px;
- margin-top: 1rem;
- border-radius: 12px;
- background-color: #868686;
- position: relative;
- overflow: hidden;
- user-select: none;
-}
-
-.skeletonTitle::after,
-.skeletonItem::after,
-.skeletonButton::after {
- content: "";
- position: absolute;
- top: 0;
- left: -150px;
- height: 100%;
- width: 150px;
- background: linear-gradient(90deg,
- rgba(255, 255, 255, 0) 0%,
- rgba(255, 255, 255, 0.4) 50%,
- rgba(255, 255, 255, 0) 100%);
- animation: shimmer 1.5s infinite;
- will-change: left;
-}
-
-@keyframes shimmer {
- 0% {
- left: -150px;
- }
-
- 100% {
- left: 100%;
- }
-}
-
-.useButton {
- background-color: #ffb22c;
- color: #222d38;
- font-weight: 700;
- padding: 0.4rem 1rem;
- border-radius: 10px;
- border: none;
- cursor: pointer;
- font-size: 1rem;
- margin-left: 12px;
- transition: background 0.2s, color 0.2s;
- box-shadow: 0 2px 8px #0002;
- user-select: none;
-}
-
-.useButton:hover:not(:disabled) {
- background-color: #ff9500;
- color: #fff;
-}
-
-.useButton:disabled {
- background-color: #bdbdbd;
- color: #888;
- cursor: not-allowed;
- opacity: 0.7;
-}
-
-.focusedOverlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100vw;
- height: 100vh;
- background: rgba(0, 0, 0, 0.5);
- z-index: 2000;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.focusedCard {
- background: #0e0d0d;
- color: #222d38;
- border-radius: 16px;
- padding: 2.5rem 2rem 2rem 2rem;
- min-width: 340px;
- min-height: 240px;
- max-width: 90vw;
- max-height: 90vh;
- overflow-y: auto;
- box-shadow: 0 8px 32px #0004;
- position: relative;
- display: flex;
- flex-direction: column;
- align-items: flex-start;
-}
-
-.focusedClose {
- position: absolute;
- top: 16px;
- right: 16px;
- background: none;
- border: none;
- font-size: 2rem;
- cursor: pointer;
- color: #222d38;
- z-index: 10;
- transition: color 0.2s;
-}
-
-.focusedClose:hover {
- color: #ff073a;
-}
-
-.couponItem {
- display: flex;
- justify-content: space-between;
- align-items: center;
- background: #121212;
- border: 1px solid #2a2a2a;
- padding: 0.75rem 1rem;
- border-radius: 10px;
- margin-bottom: 0.75rem;
- transition: background 0.2s;
-}
-
-.couponItem:hover {
- background: #1a1a1a;
-}
-
-.couponInfo {
- display: flex;
- flex-direction: column;
-}
-
-.couponLabel {
- font-size: 0.7rem;
- color: #aaa;
- margin-bottom: 0.25rem;
-}
-
-.couponCode {
- font-size: .5rem;
- font-weight: 600;
- color: #868686;
-}
-
-.redeemButton {
- background: #592891;
- color: #d8d8d8;
- border: none;
- padding: 0.3rem .5rem;
- border-radius: 2px;
- cursor: pointer;
- transition: background 0.2s;
-}
-
-.redeemButton:hover {
- background: #331555;
-}
\ No newline at end of file
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
index d4c18eb..c64f9d4 100644
--- a/app/dashboard/page.tsx
+++ b/app/dashboard/page.tsx
@@ -2,46 +2,39 @@
import { useSession } from 'next-auth/react';
import SignIn from '@/components/auth/SignIn';
-import styles from './dashboard.module.css';
import { useEffect, useState, useRef } from 'react';
import {
playFabLoginWithAzureAD,
getUserDataFromPlayFab,
- getPlayerStatistics,
getUserInventory,
- getFriendsList,
getStoreItems,
purchaseStoreItem,
useInventoryItem,
- updatePlayFabUserData
+ updatePlayFabUserData,
+ getPlayerStatistics,
} from '@/lib/playfab/playfab';
import { generateCouponCode } from '@/lib/ticket/ticket';
-import { Bar, Pie } from 'react-chartjs-2';
import Popup from '../../components/popup/Popup';
import { useRouter } from 'next/navigation';
-import {
- Chart as ChartJS,
- CategoryScale,
- LinearScale,
- BarElement,
- Title,
- Tooltip,
- Legend,
- ArcElement,
-} from 'chart.js';
-import { Doughnut } from 'react-chartjs-2';
-
-ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, ArcElement);
+// Dashboard Components
+import HeroSection from '@/components/dashboard/HeroSection';
+import PlaytimeChart from '@/components/dashboard/PlaytimeChart';
+import Inventory from '@/components/dashboard/Inventory';
+import Store from '@/components/dashboard/Store';
+import GoldenTickets from '@/components/dashboard/GoldenTickets';
+import VirtualCurrencies from '@/components/dashboard/VirtualCurrencies';
+import PlayerStatsChart from '@/components/dashboard/PlayerStatsChart';
const titleId = process.env.NEXT_PUBLIC_PLAYFAB_TITLE_ID ?? '';
interface PlayerStat {
StatisticName: string;
- Value: number;
+ Value: string;
Version?: number;
}
+
interface UserDataRecord {
Value: string;
LastUpdated?: string;
@@ -58,11 +51,6 @@ interface InventoryItem {
RemainingUses?: number;
}
-interface Friend {
- FriendPlayFabId: string;
- Username?: string;
- TitleDisplayName?: string;
-}
interface StoreItem {
ItemId: string;
@@ -71,7 +59,6 @@ interface StoreItem {
VirtualCurrencyPrices?: { [currencyCode: string]: number };
}
-
const arenaTaglines = [
"The Ultimate Coffee Break Showdown",
"Where Office Legends Are Made",
@@ -99,8 +86,6 @@ export default function Dashboard() {
const [virtualCurrency, setVirtualCurrency] = useState
({});
const [userData, setUserData] = useState>({});
const [inventoryItems, setInventoryItems] = useState([]);
- const [friends, setFriends] = useState([]);
- const [friendAddStatus, setFriendAddStatus] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [sessionTicket, setSessionTicket] = useState(null);
@@ -112,12 +97,9 @@ export default function Dashboard() {
const [popup, setPopup] = useState<{ message: string, type: 'success' | 'error' } | null>(null);
const popupTimeoutRef = useRef(null);
-
const catalogVersion = '1';
const storeId = 'Ticket Store';
-
-
useEffect(() => {
if (status !== 'authenticated' || !session) return;
@@ -127,7 +109,6 @@ export default function Dashboard() {
const loginAndLoadData = async () => {
setLoading(true);
setError(null);
- setFriendAddStatus(null);
try {
let _sessionTicket = null;
@@ -169,7 +150,6 @@ export default function Dashboard() {
}
}
-
setPlayFabId(_playFabId);
setSessionTicket(_sessionTicket);
@@ -191,11 +171,12 @@ export default function Dashboard() {
await handleUnAuthorized(userDataRes.error, loginAndLoadData);
}
-
- // Player Stats
+ // Player Statistics
const statsRes = await getPlayerStatistics(_sessionTicket, titleId);
if (statsRes.data?.Statistics) {
setPlayerStats(statsRes.data.Statistics);
+ } else {
+ setPlayerStats([]); // Set empty array if no stats
}
// Inventory & Virtual Currency
@@ -205,11 +186,6 @@ export default function Dashboard() {
setInventoryItems(inventoryRes.data.Inventory || []);
}
- // Friends
- const friendsRes = await getFriendsList(_sessionTicket, titleId);
- if (friendsRes.data?.Friends) {
- setFriends(friendsRes.data.Friends);
- }
// Store
const storeRes = await getStoreItems(_sessionTicket, titleId, storeId, catalogVersion);
@@ -217,7 +193,6 @@ export default function Dashboard() {
setStoreItems(storeRes.data.Store);
}
-
} catch (err: any) {
console.log("UnAuthorized !");
const errorMessage = err?.message || 'Unknown error';
@@ -259,7 +234,14 @@ export default function Dashboard() {
}
if (status === 'loading') {
- return Loading session...
;
+ return (
+
+
+
+
Loading your arena...
+
+
+ );
}
if (!session) {
@@ -274,137 +256,10 @@ export default function Dashboard() {
});
};
- const playerStatsChartData = {
- labels: playerStats.map((stat) => stat.StatisticName),
- datasets: [
- {
- label: 'Value',
- data: playerStats.map((stat) => stat.Value),
- backgroundColor: [
- '#ff073a',
- '#2655FF',
- ],
- borderColor: [
- '#ffff',
- ],
- borderWidth: 1,
- },
- ],
- };
-
-
const totalSeconds = parseInt(process.env.NEXT_PUBLIC_DAILY_PLAYTIME_LIMIT || '0');
-
const playtimeSecondsStr = userData.playtime_seconds || '0';
const playtimeSeconds = parseInt(playtimeSecondsStr, 10) || 0;
- const data = {
- labels: ['Played', 'Remaining'],
- datasets: [
- {
- data: [playtimeSeconds, totalSeconds - playtimeSeconds],
- backgroundColor: ['#1a73e8', '#e0e0e0'],
- borderWidth: 0,
- cutout: '80%',
- },
- ],
- };
-
- const options = {
- responsive: true,
- cutout: '80%',
- plugins: {
- legend: {
- display: false,
- },
- tooltip: {
- callbacks: {
- label: function (context: any) {
- const label = context.label || '';
- const value = context.parsed || 0;
- if (label === 'Played') {
- return `${label}: ${value} sec (${Math.floor(value / 60)} min)`;
- }
- if (label === 'Remaining') {
- return `${label}: ${value} sec (${Math.floor(value / 60)} min)`;
- }
- return `${label}: ${value}`;
- },
- },
- },
- },
- };
-
-
-
-
- const virtualCurrencyLabels = Object.keys(virtualCurrency);
- const virtualCurrencyValues = Object.values(virtualCurrency);
- const virtualCurrencyChartData = {
- labels: virtualCurrencyLabels,
- datasets: [
- {
- label: 'Arena Economy',
- data: virtualCurrencyValues,
- backgroundColor: [
- '#FFB22C',
- '#ff073a',
- ],
- borderWidth: 1,
- },
- ],
- };
-
- const StatisticNameWithEmoji = (name: string): string => {
- let prefix: string;
- switch (name) {
- case "Games Played":
- prefix = "🎲 "
- break;
- case "Trophies":
- prefix = "🏆 ";
- break;
- default:
- prefix = ""
- break;
- }
- return prefix + name;
- }
-
- const handleBuyStoreItem = async (item: StoreItem, price: number, selectedCurrency: string) => {
- if (!sessionTicket) return;
- setIsBuying(true);
-
- try {
- const res = await purchaseStoreItem(
- sessionTicket,
- titleId,
- storeId,
- catalogVersion,
- item.ItemId,
- price,
- selectedCurrency
- );
-
- if (res?.error) {
- showPopup(`Purchase failed: ${res.errorMessage || 'Unknown error'}`, 'error');
- } else {
- showPopup(`Purchased ${item.DisplayName || item.ItemId} with ${selectedCurrency}!`, 'success');
-
- const refreshedInventory = await getUserInventory(sessionTicket, titleId);
- if (refreshedInventory?.data) {
- setInventoryItems(refreshedInventory.data.Inventory || []);
- setVirtualCurrency(refreshedInventory.data.VirtualCurrency || {});
- }
- }
- } catch (err) {
- showPopup('Purchase failed due to a network or unexpected error.', 'error');
- console.error(err);
- } finally {
- setIsBuying(false);
- }
- };
-
const handleUseInventoryItem = async (item: InventoryItem) => {
if (!sessionTicket) return;
try {
@@ -451,8 +306,180 @@ export default function Dashboard() {
popupTimeoutRef.current = setTimeout(() => setPopup(null), 3000);
};
+ const handleRedeemCoupon = async (coupon: string) => {
+ try {
+ showPopup(`Redeeming golden ticket: ${coupon}`, 'success');
+
+ // Here you would typically send the coupon to your backend for processing
+ // For now, we'll just remove it from the local state
+ let coupons: string[] = [];
+ try {
+ coupons = userData.Coupons ? JSON.parse(userData.Coupons) : [];
+ } catch {
+ coupons = [];
+ }
+
+ // Remove the redeemed coupon
+ const updatedCoupons = coupons.filter(c => c !== coupon);
+
+ if (sessionTicket) {
+ await updatePlayFabUserData(sessionTicket, titleId, {
+ Coupons: JSON.stringify(updatedCoupons)
+ });
+
+ // Update local state
+ setUserData((prev) => ({
+ ...prev,
+ Coupons: JSON.stringify(updatedCoupons)
+ }));
+ }
+
+ showPopup(`Golden ticket redeemed successfully! Check your email for details.`, 'success');
+ } catch (error) {
+ showPopup('Failed to redeem golden ticket. Please try again.', 'error');
+ console.error('Redeem error:', error);
+ }
+ };
+
+
+ const handlePurchaseItem = async (item: any, currency: string) => {
+ if (!sessionTicket) {
+ showPopup('Please log in to make purchases.', 'error');
+ return;
+ }
+
+ try {
+ setIsBuying(true);
+ console.log('Starting purchase process:', {
+ item: item.ItemId,
+ displayName: item.DisplayName,
+ currency,
+ storeId,
+ catalogVersion
+ });
+
+ showPopup(`Purchasing ${item.DisplayName || item.ItemId} with ${currency}...`, 'success');
+
+ // Calculate the price in the selected currency
+ const originalPrice = item.VirtualCurrencyPrices ? Object.values(item.VirtualCurrencyPrices)[0] || 0 : 0;
+ const originalCurrency = item.VirtualCurrencyPrices ? Object.keys(item.VirtualCurrencyPrices)[0] || 'AC' : 'AC';
+
+ console.log('Item price details:', {
+ itemId: item.ItemId,
+ virtualCurrencyPrices: item.VirtualCurrencyPrices,
+ originalPrice,
+ originalCurrency,
+ selectedCurrency: currency
+ });
+
+ let finalPrice = originalPrice;
+ let actualCurrency = currency;
+
+ // Check if the item actually supports the selected currency
+ if (item.VirtualCurrencyPrices && !item.VirtualCurrencyPrices[currency]) {
+ console.log('Item does not support selected currency, using conversion...');
+
+ // Apply conversion rate (you can customize these rates)
+ const conversionRates: { [key: string]: number } = {
+ 'AC': 1, 'GC': 0.1, 'TC': 5, 'SC': 1.5, 'BC': 2, 'PC': 0.05
+ };
+ const originalRate = Number(conversionRates[originalCurrency]) || 1;
+ const targetRate = Number(conversionRates[currency]) || 1;
+ const numericOriginalPrice = typeof originalPrice === 'number' ? originalPrice : Number(originalPrice) || 0;
+ finalPrice = Math.ceil(numericOriginalPrice * (originalRate / targetRate));
+
+ console.log('Currency conversion:', {
+ originalPrice,
+ originalCurrency,
+ targetCurrency: currency,
+ originalRate,
+ targetRate,
+ finalPrice
+ });
+ } else if (item.VirtualCurrencyPrices && item.VirtualCurrencyPrices[currency]) {
+ // Item natively supports this currency
+ finalPrice = item.VirtualCurrencyPrices[currency];
+ console.log('Using native currency price:', finalPrice, currency);
+ }
+
+ // Check if player has enough currency
+ const playerAmount = Number(virtualCurrency?.[currency]) || 0;
+ if (playerAmount < Number(finalPrice)) {
+ showPopup(`Insufficient ${currency}. You need ${finalPrice} but have ${playerAmount}.`, 'error');
+ return;
+ }
+
+ console.log('Making purchase request:', {
+ sessionTicket: sessionTicket ? 'present' : 'missing',
+ titleId,
+ storeId,
+ catalogVersion,
+ itemId: item.ItemId,
+ finalPrice,
+ currency
+ });
+
+ // For multi-currency purchases, we need to handle this differently
+ // If the item doesn't natively support the selected currency, we should:
+ // 1. First convert the player's currency to the item's native currency
+ // 2. Then purchase with the native currency
+
+ let purchaseResult;
+
+ if (item.VirtualCurrencyPrices && item.VirtualCurrencyPrices[currency]) {
+ // Item natively supports this currency - direct purchase
+ console.log('Direct purchase with native currency');
+ purchaseResult = await purchaseStoreItem(sessionTicket, titleId, storeId, catalogVersion, item.ItemId, Number(finalPrice), currency);
+ } else {
+ // Need to use original currency for purchase
+ console.log('Purchase requires currency conversion - using original currency for API call');
+
+ // For now, let's try purchasing with the original currency and original price
+ // In a real implementation, you might want to implement currency exchange first
+ purchaseResult = await purchaseStoreItem(sessionTicket, titleId, storeId, catalogVersion, item.ItemId, Number(originalPrice), originalCurrency);
+
+ // If successful, deduct the converted amount from player's selected currency
+ if (purchaseResult && !purchaseResult.error) {
+ console.log('Purchase successful, need to handle currency conversion on backend');
+ // Note: In a real implementation, you'd handle the currency conversion
+ // through PlayFab's AddUserVirtualCurrency and SubtractUserVirtualCurrency APIs
+ }
+ }
+
+ console.log('Purchase result:', purchaseResult);
+
+ if (purchaseResult?.error) {
+ const errorMsg = purchaseResult.error.errorMessage || purchaseResult.errorMessage || 'Unknown error';
+ const errorCode = purchaseResult.error.code || purchaseResult.error || 'Unknown code';
+ showPopup(`Purchase failed: ${errorMsg} (Code: ${errorCode})`, 'error');
+ console.error('Purchase error details:', purchaseResult.error || purchaseResult);
+
+ // Add more specific error handling
+ if (errorMsg.toLowerCase().includes('item not found')) {
+ showPopup(`Item "${item.ItemId}" not found in store "${storeId}". Please check PlayFab configuration.`, 'error');
+ } else if (errorMsg.toLowerCase().includes('insufficient')) {
+ showPopup(`Insufficient ${currency}. You need ${finalPrice} but have ${playerAmount}.`, 'error');
+ }
+ } else {
+ showPopup(`Successfully purchased ${item.DisplayName || item.ItemId} for ${finalPrice} ${currency}!`, 'success');
+
+ // Refresh inventory and currency data
+ const refreshedInventory = await getUserInventory(sessionTicket, titleId);
+ if (refreshedInventory?.data) {
+ setInventoryItems(refreshedInventory.data.Inventory || []);
+ setVirtualCurrency(refreshedInventory.data.VirtualCurrency || {});
+ }
+ }
+ } catch (error) {
+ showPopup('Purchase failed due to a network error. Please try again.', 'error');
+ console.error('Purchase error:', error);
+ } finally {
+ setIsBuying(false);
+ }
+ };
+
return (
-
+
{popup && (
setPopup(null)}
/>
)}
- {tagline && # Arena: {tagline}
}
-
-
Welcome 🎉, {session.user?.name ?? 'Player'}
-
- {playFabId && (
-
- ID: {playFabId}{' '}
-
- {copied && Copied!}
-
- )}
+
+
+ {/* Hero Section */}
+
+
- {error &&
Error: {error}
}
-
-
- {/* Player Statistics Card */}
-
- Statistics
- {loading ? (
- <>
-
- {[...Array(1)].map((_, i) => (
-
- ))}
-
- >
- ) : playerStats.length === 0 ? (
- No statistics available.
- ) : (
- <>
-
- {playerStats.map(({ StatisticName, Value }) => (
- -
- {StatisticNameWithEmoji(StatisticName)}
- {Value}
-
- ))}
-
-
- >
- )}
-
-
- {/* Inventory Card */}
-
- Inventory
- {loading ? (
- <>
-
- {[...Array(1)].map((_, i) => (
-
- ))}
- >
- ) : inventoryItems.length === 0 ? (
- No items in inventory.
- ) : (
-
- {inventoryItems.map((item) => (
- -
- {item.DisplayName || item.ItemId} - Qty: {item.RemainingUses ?? 1}
-
-
- ))}
-
- )}
-
-
- {/* Friends Card */}
-
- Friends
-
- {friendAddStatus && {friendAddStatus}
}
-
- {loading ? (
- <>
-
- {[...Array(1)].map((_, i) => (
-
- ))}
- >
- ) : friends.length === 0 ? (
- No friends found.
- ) : (
-
- )}
-
-
- {/* Playtime Card */}
-
- Playtime
-
- {loading ? (
- <>
-
- {[...Array(1)].map((_, i) => (
-
- ))}
-
- >
- ) : (
- <>
-
-
- {`Played: ${Math.floor(playtimeSeconds / 60)} min / ${Math.floor(totalSeconds / 60)} min`}
-
- >)}
-
-
-
- {/* Virtual Currency Card */}
-
- Economy
- {loading ? (
- <>
-
- {[...Array(1)].map((_, i) => (
-
- ))}
-
- >
- ) : virtualCurrencyLabels.length === 0 ? (
- No economy available.
- ) : (
- <>
-
- {virtualCurrencyLabels.map((currency) => (
- -
- {currency}
-
- {virtualCurrency[currency].toLocaleString('en-IN')}
-
-
- ))}
-
-
- >
- )}
-
-
- {/* Store */}
-
- Store
-
- {loading ? (
- <>
-
- {[...Array(3)].map((_, i) => (
-
- ))}
- >
- ) : storeItems.length === 0 ? (
- No store items available.
- ) : (
-
- {storeItems.map((item) => {
- const prices = item.VirtualCurrencyPrices || {};
- const itemCurrencies = Object.keys(prices);
- const selectedCurrency = currencySelections[item.ItemId] || itemCurrencies[0];
- const price = prices[selectedCurrency];
-
- return (
- -
-
- {item.DisplayName || item.ItemId}
- {/* Catalog: {item.CatalogVersion || catalogVersion} */}
-
- {/* Currency selector */}
-
-
-
-
-
- Price: {price.toLocaleString('en-IN')} {selectedCurrency}
-
-
-
-
- );
- })}
-
- )}
-
-
- {/* Coupons Section */}
-
- My Coupons
- {userData.Coupons && JSON.parse(userData.Coupons).length > 0 ? (
-
- {JSON.parse(userData.Coupons).map((code: string, idx: number) => (
- -
-
- Coupon:
- {code}
-
-
-
- ))}
-
-
- ) : (
- No coupons yet.
- )}
-
+
+ {/* Main Content */}
+
+ {error && (
+
+ )}
+
+
+
+
['Games Played', 'GamesPlayed', 'Games', 'TotalGames', 'GameCount', 'Matches', 'MatchesPlayed'].includes(s.StatisticName))?.Value || '0')}
+ trophies={parseInt(playerStats.find(s => ['Trophies', 'Achievements', 'Awards'].includes(s.StatisticName))?.Value || '0')}
+ loading={loading}
+ />
+ {/* Playtime Chart */}
+
+
+ {/* Virtual Currencies */}
+
+
+
+ {/* Content Grid */}
+
+
+ {/* Golden Tickets */}
+ {
+ try {
+ return userData.Coupons ? JSON.parse(userData.Coupons) : [];
+ } catch {
+ return [];
+ }
+ })()}
+ loading={loading}
+ onRedeem={handleRedeemCoupon}
+ />
+
+
+
+
+
+
+
+ {/* Inventory */}
+
+
+
+
+
+
+
+
);
-}
+}
\ No newline at end of file
diff --git a/app/globals.css b/app/globals.css
index b49cb36..0078e6a 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,23 +1,199 @@
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
+:root {
+ /* Light Theme Colors */
+ --color-primary-50: #f8fafc;
+ --color-primary-100: #f1f5f9;
+ --color-primary-200: #e2e8f0;
+ --color-primary-300: #cbd5e1;
+ --color-primary-400: #94a3b8;
+ --color-primary-500: #64748b;
+ --color-primary-600: #475569;
+ --color-primary-700: #334155;
+ --color-primary-800: #1e293b;
+ --color-primary-900: #0f172a;
+ --color-primary-950: #020617;
+ --color-secondary-50: #f8fafc;
+ --color-secondary-100: #f1f5f9;
+ --color-secondary-200: #e2e8f0;
+ --color-secondary-300: #cbd5e1;
+ --color-secondary-400: #94a3b8;
+ --color-secondary-500: #64748b;
+ --color-secondary-600: #475569;
+ --color-secondary-700: #334155;
+ --color-secondary-800: #1e293b;
+ --color-secondary-900: #0f172a;
+ --color-secondary-950: #020617;
-:root {
- /* #121214 */
- /* 1A1A1E */
- --background: #121214;
- --foreground: #ffffff;
+ --color-accent-50: #f0fdf4;
+ --color-accent-100: #dcfce7;
+ --color-accent-200: #bbf7d0;
+ --color-accent-300: #86efac;
+ --color-accent-400: #4ade80;
+ --color-accent-500: #22c55e;
+ --color-accent-600: #16a34a;
+ --color-accent-700: #15803d;
+ --color-accent-800: #166534;
+ --color-accent-900: #14532d;
+ --color-accent-950: #052e16;
+
+ /* Light Theme Backgrounds */
+ --color-background-primary: #ffffff;
+ --color-background-secondary: #f8fafc;
+ --color-background-tertiary: #f1f5f9;
+ --color-background-card: #ffffff;
+ --color-background-muted: #f8fafc;
+
+ /* Light Theme Text */
+ --color-text-primary: #0f172a;
+ --color-text-secondary: #334155;
+ --color-text-muted: #64748b;
+ --color-text-accent: #475569;
+
+ /* Borders and Shadows */
+ --color-border-light: #e2e8f0;
+ --color-border-medium: #cbd5e1;
+ --color-border-accent: #94a3b8;
+
+ /* Shadow Definitions */
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
+ --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
+
+ /* Success, Warning, Error Colors */
+ --color-success: #22c55e;
+ --color-success-bg: #f0fdf4;
+ --color-warning: #f59e0b;
+ --color-warning-bg: #fffbeb;
+ --color-error: #ef4444;
+ --color-error-bg: #fef2f2;
}
+/* Dark Theme Colors */
@media (prefers-color-scheme: dark) {
:root {
- --background: #0a0a0a;
- --foreground: #ededed;
+ --color-primary-50: #020617;
+ --color-primary-100: #0f172a;
+ --color-primary-200: #1e293b;
+ --color-primary-300: #334155;
+ --color-primary-400: #475569;
+ --color-primary-500: #64748b;
+ --color-primary-600: #94a3b8;
+ --color-primary-700: #cbd5e1;
+ --color-primary-800: #e2e8f0;
+ --color-primary-900: #f1f5f9;
+ --color-primary-950: #f8fafc;
+
+ --color-secondary-50: #020617;
+ --color-secondary-100: #0f172a;
+ --color-secondary-200: #1e293b;
+ --color-secondary-300: #334155;
+ --color-secondary-400: #475569;
+ --color-secondary-500: #64748b;
+ --color-secondary-600: #94a3b8;
+ --color-secondary-700: #cbd5e1;
+ --color-secondary-800: #e2e8f0;
+ --color-secondary-900: #f1f5f9;
+ --color-secondary-950: #f8fafc;
+
+ /* Dark Theme Backgrounds */
+ --color-background-primary: #0f172a;
+ --color-background-secondary: #1e293b;
+ --color-background-tertiary: #334155;
+ --color-background-card: #1e293b;
+ --color-background-muted: #334155;
+
+ /* Dark Theme Text */
+ --color-text-primary: #f8fafc;
+ --color-text-secondary: #cbd5e1;
+ --color-text-muted: #94a3b8;
+ --color-text-accent: #64748b;
+
+ /* Dark Theme Borders */
+ --color-border-light: #334155;
+ --color-border-medium: #475569;
+ --color-border-accent: #64748b;
+
+ /* Dark Theme Shadows */
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.3);
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -4px rgb(0 0 0 / 0.3);
+ --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.3), 0 8px 10px -6px rgb(0 0 0 / 0.3);
+
+ /* Dark Theme Success, Warning, Error Colors */
+ --color-success: #4ade80;
+ --color-success-bg: #14532d;
+ --color-warning: #fbbf24;
+ --color-warning-bg: #92400e;
+ --color-error: #f87171;
+ --color-error-bg: #991b1b;
}
}
+/* Dark mode class for manual toggle */
+.dark {
+ --color-primary-50: #020617;
+ --color-primary-100: #0f172a;
+ --color-primary-200: #1e293b;
+ --color-primary-300: #334155;
+ --color-primary-400: #475569;
+ --color-primary-500: #64748b;
+ --color-primary-600: #94a3b8;
+ --color-primary-700: #cbd5e1;
+ --color-primary-800: #e2e8f0;
+ --color-primary-900: #f1f5f9;
+ --color-primary-950: #f8fafc;
+
+ --color-secondary-50: #020617;
+ --color-secondary-100: #0f172a;
+ --color-secondary-200: #1e293b;
+ --color-secondary-300: #334155;
+ --color-secondary-400: #475569;
+ --color-secondary-500: #64748b;
+ --color-secondary-600: #94a3b8;
+ --color-secondary-700: #cbd5e1;
+ --color-secondary-800: #e2e8f0;
+ --color-secondary-900: #f1f5f9;
+ --color-secondary-950: #f8fafc;
+
+ /* Dark Theme Backgrounds */
+ --color-background-primary: #0e0e0e;
+ --color-background-secondary: #2b2b2f;
+ --color-background-tertiary: #2d3032;
+ --color-background-card: #292626;
+ --color-background-muted: #3c3c47;
+
+ /* Dark Theme Text */
+ --color-text-primary: #f8fafc;
+ --color-text-secondary: #cbd5e1;
+ --color-text-muted: #94a3b8;
+ --color-text-accent: #64748b;
+
+ /* Dark Theme Borders */
+ --color-border-light: #334155;
+ --color-border-medium: #475569;
+ --color-border-accent: #64748b;
+
+ /* Dark Theme Shadows */
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.3);
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -4px rgb(0 0 0 / 0.3);
+ --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.3), 0 8px 10px -6px rgb(0 0 0 / 0.3);
+
+ /* Dark Theme Success, Warning, Error Colors */
+ --color-success: #4ade80;
+ --color-success-bg: #14532d;
+ --color-warning: #fbbf24;
+ --color-warning-bg: #92400e;
+ --color-error: #f87171;
+ --color-error-bg: #991b1b;
+}
+
html,
body {
max-width: 100vw;
@@ -25,11 +201,12 @@ body {
}
body {
- color: var(--foreground);
- background: var(--background);
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ color: var(--color-text-primary);
+ background: var(--color-background-primary);
+ font-family: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
+ transition: background-color 0.3s ease, color 0.3s ease;
}
* {
@@ -43,8 +220,113 @@ a {
text-decoration: none;
}
+/* Custom scrollbar */
+::-webkit-scrollbar {
+ width: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--color-background-secondary);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--color-primary-400);
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--color-primary-500);
+}
+
+/* Selection colors */
+::selection {
+ background: var(--color-accent-200);
+ color: var(--color-text-primary);
+}
+
+::-moz-selection {
+ background: var(--color-accent-200);
+ color: var(--color-text-primary);
+}
+
+/* Dark mode selection colors */
+@media (prefers-color-scheme: dark) {
+ ::selection {
+ background: var(--color-accent-700);
+ color: var(--color-text-primary);
+ }
+
+ ::-moz-selection {
+ background: var(--color-accent-700);
+ color: var(--color-text-primary);
+ }
+}
+
+.dark ::selection {
+ background: var(--color-accent-700);
+ color: var(--color-text-primary);
+}
+
+.dark ::-moz-selection {
+ background: var(--color-accent-700);
+ color: var(--color-text-primary);
+}
+
+/* Utility classes for theme-aware components */
+.bg-primary {
+ background-color: var(--color-background-primary);
+}
+
+.bg-secondary {
+ background-color: var(--color-background-secondary);
+}
+
+.bg-card {
+ background-color: var(--color-background-card);
+}
+
+.text-primary {
+ color: var(--color-text-primary);
+}
+
+.text-secondary {
+ color: var(--color-text-secondary);
+}
+
+.text-muted {
+ color: var(--color-text-muted);
+}
+
+.border-light {
+ border-color: var(--color-border-light);
+}
+
+.border-medium {
+ border-color: var(--color-border-medium);
+}
+
+.border-accent {
+ border-color: var(--color-border-accent);
+}
+
+/* Focus styles for better accessibility */
+*:focus-visible {
+ outline: 2px solid var(--color-accent-500);
+ outline-offset: 2px;
+}
+
+/* Smooth transitions for theme changes */
+* {
+ transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease;
+}
+
+/* Dark mode specific focus styles */
@media (prefers-color-scheme: dark) {
- html {
- color-scheme: dark;
+ *:focus-visible {
+ outline-color: var(--color-accent-400);
}
+}
+
+.dark *:focus-visible {
+ outline-color: var(--color-accent-400);
}
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
index e41d62f..05969f9 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -32,21 +32,20 @@ export default async function RootLayout({
const session = await getServerSession(authOptions);
return (
-
-
+
+
-
+
-
+
{children}
-
diff --git a/app/leaderboard/leaderboard.module.css b/app/leaderboard/leaderboard.module.css
deleted file mode 100644
index bb79f4e..0000000
--- a/app/leaderboard/leaderboard.module.css
+++ /dev/null
@@ -1,138 +0,0 @@
-.page {
- height: 90vh;
- /* full viewport height */
- display: flex;
- flex-direction: column;
- padding: 1rem 2rem;
- background-color: #121212;
- color: white;
- box-sizing: border-box;
-}
-
-.info {
- font-size: x-small;
- color: rgba(255, 255, 255, 0.616);
-}
-
-.heading {
- text-align: left;
- margin: 0;
- padding-left: 16px;
- font-size: 2rem;
- font-weight: bold;
- margin-top: 1rem;
- margin-bottom: 1rem;
-}
-
-.splitLayout {
- flex: 1;
- /* take remaining vertical space */
- display: flex;
- gap: 1rem;
- overflow: hidden;
-}
-
-.leftPanel {
- flex: 1;
- /* half screen */
- background-color: transparent;
- border-radius: 8px;
- overflow: hidden;
- /* Ensure canvas fills this div */
- display: flex;
-}
-
-.rightPanel {
- flex: 1;
- /* half screen */
- display: flex;
- flex-direction: column;
- background-color: #1f1f1f;
- border-radius: 8px;
- padding: 1rem;
- overflow-y: auto;
-}
-
-/* Sort controls */
-.sortControls {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- margin-bottom: 1rem;
-}
-
-.sortControls label {
- font-weight: 500;
-}
-
-.sortControls select {
- background: #333;
- border: none;
- color: white;
- padding: 0.3rem 0.6rem;
- border-radius: 4px;
- cursor: pointer;
-}
-
-/* Table styles */
-.table {
- flex: 1;
- overflow-y: auto;
-}
-
-.row {
- display: grid;
- grid-template-columns: 1fr 3fr 2fr;
- padding: 0.5rem 1rem;
- gap: 1.5rem;
- border-bottom: 1px solid #333;
- align-items: center;
-}
-
-.rowHeader {
- color: #ffffff;
-}
-
-.header {
- font-weight: 700;
- border-bottom: 2px solid #555;
-}
-
-.skeletonRow {
- animation: pulse 1.5s infinite ease-in-out;
-}
-
-.skeletonBox {
- height: 1rem;
- background: #444;
- border-radius: 4px;
-}
-
-.animatedRow:hover {
- background-color: #333;
- transition: background-color 0.3s ease;
-}
-
-/* Pulse animation for skeleton */
-@keyframes pulse {
-
- 0%,
- 100% {
- background-color: #444;
- }
-
- 50% {
- background-color: #555;
- }
-}
-
-.playerinfo {
- color: rgba(255, 255, 255, 0.555);
- font-size: .8rem;
- font-weight: 700;
-}
-
-.sortOptions {
- color: rgb(255, 255, 255);
- font-size: .8rem;
-}
\ No newline at end of file
diff --git a/app/leaderboard/page.tsx b/app/leaderboard/page.tsx
index 783f42b..f9e397f 100644
--- a/app/leaderboard/page.tsx
+++ b/app/leaderboard/page.tsx
@@ -2,13 +2,13 @@
import { useSession } from 'next-auth/react';
import SignIn from '@/components/auth/SignIn';
-import styles from './leaderboard.module.css';
import { useEffect, useState } from 'react';
import {
getLeaderboard,
playFabLoginWithAzureAD,
} from '@/lib/playfab/playfab';
import dynamic from 'next/dynamic';
+import LeaderboardChart from '@/components/dashboard/LeaderboardChart';
const Model = dynamic(() => import('@/components/Model'), {
ssr: false,
@@ -97,8 +97,19 @@ export default function LeaderBoard() {
(b[sortFieldMap[sortBy]] as number) - (a[sortFieldMap[sortBy]] as number)
);
+
if (status === 'loading') {
- return
Loading session...
;
+ return (
+
+
+
+
+
+
Loading Leaderboard...
+
+
+
+ );
}
if (!session) {
@@ -106,61 +117,129 @@ export default function LeaderBoard() {
}
return (
-
-
Leaderboard
# weekly
-
- {/* Split container */}
-
- {/* Left: 3D Model */}
-
- {leaders[0] &&
}
+
+ {/* Header */}
+
+
+
+
+
+
+ Leaderboard
+
+
Weekly competition rankings
+
+
+
+
- {/* Right: Leaderboard Table */}
-
-
-
Sort by:
-
+
+
+ {/* Champion Showcase */}
+
+
+
+
+
+ {leaders[0] ? (
+
+ ) : (
+
+ )}
+
+
-
-
-
Rank
-
Player
-
{sortBy}
+ {/* Leaderboard Chart */}
+
+ {/* Controls */}
+
+
+
+
+
+
+ Rankings Visualization
+
+
+
+
+ View by:
+
+
+
+
+
+
+
- {loading
- ? Array.from({ length: 5 }).map((_, i) => (
-
- ))
- : sortedLeaders.map((player) => (
-
-
{player.rank}
-
{player.name}
-
{player[sortFieldMap[sortBy]]}
+ {/* Chart */}
+ {loading ? (
+
+ ) : (
+
+ )}
);
-}
+}
\ No newline at end of file
diff --git a/app/page.tsx b/app/page.tsx
index 9584e42..182ad5a 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -17,8 +17,13 @@ export default function Home() {
if (status === 'loading') {
return (
-
- Loading...
+
);
}
@@ -28,8 +33,13 @@ export default function Home() {
}
return (
-
- Redirecting...
+
+
+
+
+
Redirecting to Dashboard...
+
+
);
}
diff --git a/app/profile/page.tsx b/app/profile/page.tsx
index 4152c79..f2ff74d 100644
--- a/app/profile/page.tsx
+++ b/app/profile/page.tsx
@@ -2,7 +2,6 @@
import { useSession, signOut } from 'next-auth/react';
import SignIn from '@/components/auth/SignIn';
-import styles from './profile.module.css';
import { useEffect, useState } from 'react';
import { addFriend } from '@/lib/playfab/playfab';
import { Game } from '@/lib/db';
@@ -17,8 +16,24 @@ export default function Profile() {
const [myGames, setMyGames] = useState
([]);
const [gamesLoading, setGamesLoading] = useState(true);
+ useEffect(() => {
+ if (session) {
+ fetchMyGames();
+ }
+ }, [session]);
+
if (status === 'loading') {
- return Loading...
;
+ return (
+
+ );
}
if (!session) {
@@ -50,186 +65,264 @@ export default function Profile() {
} else {
alert('Failed to delete game.');
}
- } catch (err) {
- console.error(err);
- alert('Error deleting game.');
+ } catch (e) {
+ console.error(e);
}
};
-
const handleAddFriend = async () => {
- const sessionTicket = sessionStorage.getItem('playfabSessionTicket');
- const titleId = process.env.NEXT_PUBLIC_PLAYFAB_TITLE_ID;
-
- if (!sessionTicket || !titleId) {
- setFeedback('Missing session ticket or title ID');
+ if (!inputValue.trim()) {
+ setFeedback('Please enter a value.');
return;
}
try {
- let res;
-
- if (method === 'id') {
- res = await addFriend(sessionTicket, titleId, inputValue);
- } else if (method === 'email') {
- res = await addFriend(sessionTicket, titleId, undefined, inputValue);
- } else {
- res = await addFriend(sessionTicket, titleId, undefined, undefined, inputValue);
- }
-
- if (res.code === 200) {
- setFeedback('Friend added successfully!');
+ const result = await addFriend(method, inputValue);
+ if (result.success) {
+ setFeedback('Friend request sent successfully!');
setInputValue('');
} else {
- setFeedback(res.errorMessage || 'Could not add friend.');
+ setFeedback(result.message || 'Failed to send friend request.');
}
- } catch (err: any) {
- setFeedback(err?.errorMessage || 'Error occurred while adding friend.');
+ } catch (error) {
+ setFeedback('An error occurred while sending the friend request.');
}
};
- useEffect(() => {
- fetchMyGames();
- }, []);
+ const getPlaceholder = () => {
+ switch (method) {
+ case 'id': return 'Enter PlayFab ID';
+ case 'username': return 'Enter username';
+ case 'email': return 'Enter email address';
+ default: return '';
+ }
+ };
return (
-
-
-
-
- {session.user?.image && (
-

- )}
-
-
{session.user?.name ?? ''}
-
{session.user?.email ?? ''}
-
+
+ {/* Header */}
+
+
+
+
+
+
+
+ Profile
+
+
Manage your account and game preferences
+
+
-
-
Add Friends
-
-
-
-
-
-
-
-
-
-
-
- {method === 'id' && 'ID'}
- {method === 'username' && 'Username'}
- {method === 'email' && 'Email'}
-
-
setInputValue(e.target.value)}
- />
-
-
-
- {showHelp && (
-
-
Select how you'd like to search for your friend - you can get the info on their dashboard.
+
+
+ {/* Profile Information */}
+
+
+
+
+
+

+
+
+
+ {session.user?.name ?? 'Anonymous'}
+
+
+ {session.user?.email ?? 'No email'}
+
+
- )}
-
-
-
- {feedback &&
{feedback}
}
-
+
-
-
My Publishings
-
-
- {gamesLoading ? (
-
Loading...
- ) : myGames.length === 0 ? (
-
You haven't published any games yet.
- ) : (
-
-
-
- | Title |
- Description |
- Actions |
-
-
-
- {myGames.map((game) => (
-
- | {game.title} |
- {game.description?.slice(0, 90)}... |
-
+ {/* Add Friends Section */}
+
+
+
+
+
+ {/* Method Selection */}
+
+ {(['id', 'username', 'email'] as const).map((m) => (
-
+
+ {/* Input */}
+
+ setInputValue(e.target.value)}
+ placeholder={getPlaceholder()}
+ className="w-full px-3 py-2 rounded-lg bg-slate-700/50 backdrop-blur-sm text-white placeholder-slate-400 focus:bg-slate-600/50 focus:outline-none transition-all duration-200"
+ />
+
+
+ {/* Help Button */}
+
+
- |
+
+ {showHelp && (
+
+
PlayFab ID: Found in user's dashboard
+
Username: Display name in the game
+
Email: Account email address
+
+ )}
+
+
+ {feedback && (
+
+ {feedback}
+
+ )}
+
+
+
+
-
- ))}
-
-
- )}
-
-
-
+ {/* My Games */}
+
+
+
+
+
+
+
+ {myGames.length} Games
+
+
+ {gamesLoading ? (
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
+ ) : myGames.length === 0 ? (
+
+
+
No games published yet
+
Start creating and publishing your games!
+
+ ) : (
+
+
+
+
+ | Game Name |
+ Type |
+ Status |
+ Actions |
+
+
+
+ {myGames.map((game) => (
+
+ |
+
+ |
+
+
+ {game.type!.toUpperCase()}
+
+ |
+
+
+ Published
+
+ |
+
+
+
+ View
+
+
+
+ |
+
+ ))}
+
+
+
+ )}
+
+
+
+
+
);
-}
+}
\ No newline at end of file
diff --git a/app/profile/profile.module.css b/app/profile/profile.module.css
deleted file mode 100644
index 74cfc44..0000000
--- a/app/profile/profile.module.css
+++ /dev/null
@@ -1,319 +0,0 @@
-.page {
- min-height: 100vh;
- background: #0e0e10;
- color: white;
- display: flex;
- flex-direction: column;
- gap: 2rem;
- padding-left: 3rem;
- padding-top: 2rem;
-}
-
-.pageHeader {
- display: flex;
- justify-content: flex-start;
- align-items: center;
- margin-bottom: 1rem;
-}
-
-
-.pageHeader h2 {
- font-size: 2rem;
- font-weight: 700;
- color: #ffffff;
- margin: 0;
-}
-
-.profileLayout {
- display: flex;
- gap: 2rem;
- align-items: center;
- flex-wrap: wrap;
- max-width: 900px;
- width: 100%;
- padding: 2rem;
- animation: fadeIn 0.4s ease-in-out;
- justify-content: flex-start;
-}
-
-.avatar {
- border-radius: 50%;
- object-fit: cover;
- width: 120px;
- height: 120px;
- border: 3px solid #7a71ff;
- filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.3));
- transition: transform 0.3s ease, filter 0.3s ease;
-}
-
-.avatar:hover {
- transform: scale(1.05);
- filter: drop-shadow(0 0 6px rgba(0, 0, 0, 0.5));
-}
-
-
-.details {
- display: flex;
- flex-direction: column;
- justify-content: center;
- gap: 0.5rem;
- text-align: left;
-}
-
-.name {
- font-size: 1.75rem;
- font-weight: 700;
- color: #ffffffad;
-}
-
-.email {
- font-size: .8rem;
- color: #bbbbbb;
-}
-
-.signOutButton {
- margin-top: 1rem;
- background: linear-gradient(135deg, #b96666, #d43737);
- color: #fff;
- border: none;
- padding: 0.6rem 1.25rem;
- border-radius: 8px;
- font-weight: 700;
- cursor: pointer;
- transition: all 0.3s ease;
- width: fit-content;
- box-shadow: 0 0 8px rgba(248, 113, 113, 0.4);
-}
-
-.signOutButton:hover {
- background: linear-gradient(135deg, #ef4444, #b91c1c);
- color: #fff;
- box-shadow: 0 0 12px rgba(185, 28, 28, 0.6);
- transform: scale(1.05);
-}
-
-
-@keyframes fadeIn {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-.addFriendsContainer {
- animation: fadeIn 0.4s ease-in-out;
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.AddFriends {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 12px;
- padding: 1rem;
- background-color: #1e1e1e;
- border-radius: 5px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
- width: fit-content;
-}
-
-.AddFriendsInput {
- width: 250px;
- padding: 10px 14px;
- border: none;
- border-radius: 10px;
- outline: none;
- background-color: #2c2c2e;
- color: #b6b6b6;
- font-size: 13px;
-}
-
-.AddFriendsInput::placeholder {
- color: #b6b6b6;
- font-size: 13px;
-}
-
-.AddFriendsButton {
- padding: 10px 20px;
- background: #3b82f6;
- color: #fff;
- border: none;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- transition: all .3s ease;
- filter: grayscale(1);
-}
-
-.AddFriendsButton:hover {
- transition: all .3s ease;
- background: #3b82f6;
- filter: grayscale(0);
-}
-
-.AddFriendsHelp {
- padding: 5px 5px;
- background: #ffe600;
- color: #000000;
- border: none;
- border-radius: 20px;
- cursor: pointer;
- transition: all .3s ease;
-}
-
-.AddFriendsHelp:hover {
- transition: all .3s ease;
- background: #ffe600;
- box-shadow: 0px 0px 10px rgb(255, 255, 255);
-}
-
-
-.tooltipWrapper {
- position: relative;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.tooltip {
- position: absolute;
- top: -60px;
- left: 50%;
- transform: translateX(-50%) scale(0.9);
- background-color: #2d2d30;
- color: #ffffff;
- padding: 12px 16px;
- border-radius: 8px;
- white-space: nowrap;
- font-size: 0.85rem;
- font-weight: 500;
- opacity: 0;
- animation: fadeInTooltip 0.3s ease forwards;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
- z-index: 20;
- width: max-content;
- text-align: center;
- line-height: 1.4;
-}
-
-@keyframes fadeInTooltip {
- 0% {
- opacity: 0;
- transform: translateX(-50%) scale(0.9);
- }
-
- 100% {
- opacity: 1;
- transform: translateX(-50%) scale(1);
- }
-}
-
-.methodTabs {
- display: flex;
- gap: 12px;
- margin-bottom: 1rem;
-}
-
-.methodTab {
- padding: 8px 16px;
- border: none;
- border-radius: 20px;
- background-color: #2c2c2e;
- color: white;
- cursor: pointer;
- transition: all 0.2s ease;
- font-size: 0.85rem;
- font-weight: 600;
-}
-
-.methodTab:hover {
- background-color: #3b3b3e;
-}
-
-.methodTab.active {
- background-color: #3b82f6;
- color: #fff;
-}
-
-.inputLabel {
- font-size: 0.8rem;
- font-weight: 600;
- color: #bfbfbf;
- margin-right: 8px;
- white-space: nowrap;
-}
-
-.myGamesContainer {
- animation: fadeIn 0.4s ease-in-out;
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.gamesTable {
- width: max-content;
- border-collapse: collapse;
- background-color: #1e1e1e;
- border-radius: 8px;
- overflow: hidden;
- font-size: 0.9rem;
-}
-
-.gamesTable th,
-.gamesTable td {
- padding: 12px 16px;
- text-align: left;
- border-bottom: 1px solid #333;
-}
-
-.gamesTable th {
- background-color: #2c2c2e;
- color: #ffffff;
- font-weight: 600;
-}
-
-.gamesTable td {
- color: #bbbbbb;
-}
-
-.gamesTable tr:hover {
- background-color: #2e2e30;
-}
-
-.viewButton {
- background-color: #3b82f6;
- border: none;
- color: white;
- padding: 6px 12px;
- border-radius: 6px;
- cursor: pointer;
- font-weight: 600;
- transition: background-color 0.3s ease;
-}
-
-.viewButton:hover {
- background-color: #2563eb;
-}
-
-.deleteButton {
- margin-left: 0.5rem;
- padding: 6px 12px;
- background-color: #ef4444;
- color: white;
- border: none;
- border-radius: 6px;
- cursor: pointer;
- font-size: 0.85rem;
- transition: background-color 0.2s;
-}
-
-.deleteButton:hover {
- background-color: #dc2626;
-}
\ No newline at end of file
diff --git a/components/Model.tsx b/components/Model.tsx
index f67030d..bf7e8fd 100644
--- a/components/Model.tsx
+++ b/components/Model.tsx
@@ -165,7 +165,7 @@ function SpaceModel() {
export default function SceneWithCanvasTextAndModel({ name = '' }) {
return (
-
+