Commit c62de5ef by Arjun Jhukal

updated the exclusive game detail page at user side

parent 0c86e33e
......@@ -15,10 +15,13 @@
"@tanstack/react-table": "^8.21.3",
"@wandersonalwes/iconsax-react": "0.0.10",
"formik": "^2.4.6",
"framer-motion": "^12.23.16",
"js-cookie": "^3.0.5",
"lightgallery": "^2.9.0-beta.1",
"next": "15.5.3",
"react": "^19.1.0",
"react-dom": "19.1.0",
"react-dropzone": "^14.3.8",
"react-quill-new": "3.4.6",
"react-redux": "^9.2.0",
"yup": "^1.7.0"
......@@ -26,6 +29,7 @@
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
......@@ -1858,6 +1862,13 @@
"@types/react": "*"
}
},
"node_modules/@types/js-cookie": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
"integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
......@@ -2747,6 +2758,15 @@
"node": ">= 0.4"
}
},
"node_modules/attr-accept": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
"integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
......@@ -3954,6 +3974,18 @@
"node": ">=16.0.0"
}
},
"node_modules/file-selector": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
"integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
"license": "MIT",
"dependencies": {
"tslib": "^2.7.0"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
......@@ -4052,6 +4084,33 @@
"react": ">=16.8.0"
}
},
"node_modules/framer-motion": {
"version": "12.23.16",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.16.tgz",
"integrity": "sha512-N81A8hiHqVsexOzI3wzkibyLURW1nEJsZaRuctPhG4AdbbciYu+bKJq9I2lQFzAO4Bx3h4swI6pBbF/Hu7f7BA==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.23.12",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
......@@ -4849,6 +4908,15 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
......@@ -5400,6 +5468,21 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/motion-dom": {
"version": "12.23.12",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.12.tgz",
"integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
}
},
"node_modules/motion-utils": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
......@@ -5960,6 +6043,23 @@
"react": "^19.1.0"
}
},
"node_modules/react-dropzone": {
"version": "14.3.8",
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz",
"integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==",
"license": "MIT",
"dependencies": {
"attr-accept": "^2.2.4",
"file-selector": "^2.1.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">= 10.13"
},
"peerDependencies": {
"react": ">= 16.8 || 18.0.0"
}
},
"node_modules/react-fast-compare": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
......
......@@ -16,10 +16,13 @@
"@tanstack/react-table": "^8.21.3",
"@wandersonalwes/iconsax-react": "0.0.10",
"formik": "^2.4.6",
"framer-motion": "^12.23.16",
"js-cookie": "^3.0.5",
"lightgallery": "^2.9.0-beta.1",
"next": "15.5.3",
"react": "^19.1.0",
"react-dom": "19.1.0",
"react-dropzone": "^14.3.8",
"react-quill-new": "3.4.6",
"react-redux": "^9.2.0",
"yup": "^1.7.0"
......@@ -27,6 +30,7 @@
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
......
// app/(dashboard)/exclusive-games/[id]/page.tsx
import ExlusiveGameDetail from "@/components/pages/dashboard/userDashboard/games/exclusiveGames/exclusiveGameDetail";
import { getSingleGame } from "@/serverApi/game";
// { params }: { params: { id: string } }
export default async function UserGameDetail() {
// const game = await getSingleGame(params.id);
import { cookies } from "next/headers";
export default async function UserGameDetail(props: { params: Promise<{ id: string }> }) {
const { id } = await props.params;
const game = await getSingleGame(id);
console.log(game);
return <ExlusiveGameDetail game={game} />
// return <ExlusiveGameDetail game={game.data} />;
return <h1>Game detail</h1>
}
"use client";
import React, { useState, useEffect, useRef } from "react";
import { motion } from "framer-motion";
interface ScreenShotSliderProps {
screenshots: string[];
className?: string;
speed?: number; // px per second
}
export default function ScreenShotSlider({
screenshots,
className = "",
speed = 50,
}: ScreenShotSliderProps) {
const [images, setImages] = useState<string[]>([]);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (screenshots.length === 0) return;
// Triple the array for seamless infinite scroll
setImages([...screenshots, ...screenshots, ...screenshots]);
}, [screenshots]);
if (images.length === 0) return null;
const itemWidth = 220; // 212px width + 8px margin
const totalWidth = screenshots.length * itemWidth;
const duration = totalWidth / speed;
return (
<div
ref={containerRef}
className={`relative w-full mx-auto overflow-hidden ${className}`}
style={{ height: "120px" }}
>
<motion.div
className="flex"
style={{
width: `${images.length * itemWidth}px`,
}}
initial={{ x: 0 }}
animate={{
x: -totalWidth
}}
transition={{
x: {
repeat: Infinity,
repeatType: "loop",
ease: "linear",
duration: duration,
},
}}
>
{images.map((src, index) => (
<motion.div
key={`${index}-${src}`}
className="flex-shrink-0"
style={{
width: "212px",
height: "120px",
marginRight: "8px"
}}
whileHover={{ scale: 1.05 }}
transition={{ duration: 0.2 }}
>
<img
src={src}
alt={`Screenshot ${(index % screenshots.length) + 1}`}
className="w-full h-full object-cover rounded-xl shadow-lg hover:shadow-xl transition-shadow duration-200"
draggable={false}
loading="lazy"
onError={(e) => {
e.currentTarget.src = "/assets/images/fallback.png";
}}
/>
</motion.div>
))}
</motion.div>
{/* Optional gradient overlays for smooth edges */}
<div className="absolute top-0 left-0 w-16 h-full bg-gradient-to-r from-white to-transparent pointer-events-none z-10" />
<div className="absolute top-0 right-0 w-16 h-full bg-gradient-to-l from-white to-transparent pointer-events-none z-10" />
</div>
);
}
\ No newline at end of file
// "use client";
import { Box, IconButton, InputAdornment, OutlinedInput } from "@mui/material";
import { SearchNormal } from "@wandersonalwes/iconsax-react";
import { Box, FormControl, IconButton, InputAdornment, OutlinedInput } from "@mui/material";
import { SearchNormal, SearchNormal1 } from "@wandersonalwes/iconsax-react";
import React from 'react'
export default function AdminSearchBar() {
return (
<Box>
<div className="inpute__field relative">
// <Box>
// <div className="inpute__field relative">
// <OutlinedInput
// type="search"
// placeholder="Search game"
// startAdornment={
// <InputAdornment position="start">
// <IconButton edge="start">
// <SearchNormal size={32} />
// </IconButton>
// </InputAdornment>
// }
// />
// </div>
// </Box>
<Box >
<FormControl >
<OutlinedInput
type="search"
placeholder="Search game"
id="header-search"
startAdornment={
<InputAdornment position="start">
<IconButton edge="start">
<SearchNormal size={32} />
</IconButton>
<InputAdornment position="start" sx={{ mr: -0.5 }}>
<SearchNormal1 size={16} />
</InputAdornment>
}
aria-describedby="header-search-text"
slotProps={{ input: { 'aria-label': 'weight' } }}
placeholder="Search Keyword"
sx={{ '& .MuiOutlinedInput-input': { pl: 1.5 } }}
/>
</div>
</FormControl>
</Box>
)
}
'use client';
import Cookies from 'js-cookie';
import React from 'react'
import AuthMessageBlock from '../authMessageBlock'
import { Box, InputLabel, OutlinedInput } from '@mui/material'
......@@ -63,6 +63,11 @@ export default function LoginPage() {
user: response.data?.user,
}),
);
Cookies.set('access_token', response?.data?.access_token, {
expires: 1,
secure: process.env.NODE_ENV === 'production',
sameSite: 'Strict',
});
router.replace(PATH.DASHBOARD.ROOT);
}
catch (e) {
......
import React from "react";
import { GameItem } from "@/types/game";
import { GameItem, SingleGameResponse } from "@/types/game";
import Image from "next/image";
import { renderHTML } from "@/utils/RenderHTML";
import { Box, Button } from "@mui/material";
import SilverCoinIcon from "@/icons/SilverCoinIcon";
import ScreenShotSlider from "@/components/molecules/Sliders/ScreenShotSlider";
import CustomLightGallery from "@/components/organism/LightGallery";
export default function ExlusiveGameDetail({ game }: { game: GameItem }) {
export default function ExlusiveGameDetail({ game }: { game: SingleGameResponse }) {
console.log(game);
return (
<div className="p-4">
<h1 className="text-2xl font-bold">{game.name}</h1>
<p className="text-gray-600">{game.description}</p>
<p className="text-sm">Provider: {game.provider}</p>
<>
<section className="detail__banner mb-8">
<div className="grid grid-cols-12 gap-8 lg:gap-20">
<div className="col-span-12 md:col-span-4">
<div className="aspect-[420/433] relative rounded-xl overflow-hidden">
<Image src={game.data.thumbnail || "/assets/images/fallback.png"} fill className="object-cover" alt={game.data.name} />
</div>
</div>
<div className="col-span-12 md:col-span-8">
<div className="content__wrapper flex flex-col gap-4">
<ul className="flex gap-4">
{game?.data?.provider ? <li className="text-[9px] lg:text-[12px] leading-[188%] font-[400] rounded-[8px] px-2 py-[2px] bg-yellow-300 text-title">Game Type : {game?.data?.provider}</li> : ""}
{game?.data?.subgames?.length ? <li className="text-[9px] lg:text-[12px] leading-[188%] font-[400] rounded-[8px] px-2 py-[2px] bg-yellow-300 text-title">{game?.data?.subgames.length}</li> : ""}
</ul>
<div className="general-content-box styled-list !text-white">
<h1 className="text-[2rem]">{game?.data?.name}</h1>
{renderHTML(game?.data?.description)}
</div>
<div className="action__group flex flex-wrap gap-2">
<Box sx={{
background: "linear-gradient(0deg, rgba(234, 47, 231, 0.10) 0%, rgba(234, 47, 231, 0.10) 100%)",
borderRadius: "16px"
}} className="flex justify-center items-center gap-2 py-4 px-6 min-w-[30%] ">
<SilverCoinIcon />
<div className="coins">
<strong className="text-[16px] leading-4 font-[600] block mb-1">20,000</strong>
<span className="text-[12px] block">Current Sweep Coins</span>
</div>
</Box>
<Box sx={{
borderRadius: "16px"
}} className="flex justify-center items-center gap-2 py-4 px-6 bg-secondary-grad text-title min-w-[30%] ">
<div className="coins">
<strong className="text-[16px] leading-4 font-[600] block mb-1">+ Deposit Coins</strong>
</div>
</Box>
<Box sx={{
borderRadius: "16px"
}} className="flex justify-center items-center gap-2 py-4 px-6 border border-secondary min-w-[30%] ">
<div className="coins">
<strong className="text-[16px] leading-4 font-[600] block mb-1 text-secondary">Withdraw Coins</strong>
</div>
</Box>
</div>
<Button variant="contained" color="primary">Play Now</Button>
{game?.data?.screenshots ? <ScreenShotSlider screenshots={game.data.screenshots} /> : ""}
</div>
</div>
</div>
</section >
{game?.data?.subgames ? <section className="exclusive__game__gallery">
<div className="section__title">
<h2 className="text-[14px] lg:text-[16px] mb-4">Panda Master Games</h2>
</div>
<CustomLightGallery images={game.data.subgames} column={7} aspectRatio="aspect-[212/120]" />
</section> : ""}
</>
);
}
"use client";
import { useAppDispatch, useAppSelector } from "@/hooks/hook";
import auth, { clearTokens } from "@/slice/authSlice";
import { clearTokens, setTokens } from "@/slice/authSlice";
import { useRouter } from "next/navigation";
import React, { useEffect } from "react";
import Cookies from "js-cookie";
function isTokenExpired(token: string): boolean {
try {
const payload = JSON.parse(atob(token.split(".")[1])); // decode JWT payload
const exp = payload.exp; // expiration time in seconds
const payload = JSON.parse(atob(token.split(".")[1]));
const exp = payload.exp;
if (!exp) return true;
const now = Math.floor(Date.now() / 1000); // current time in seconds
const now = Math.floor(Date.now() / 1000);
return exp < now;
} catch (error) {
console.error("Failed to decode token:", error);
return true; // treat invalid token as expired
return true;
}
}
export default function Private({ children }: { children: React.ReactNode }) {
const router = useRouter();
const dispatch = useAppDispatch();
const user = useAppSelector((state) => state.auth.user);
const token = useAppSelector((state) => state.auth.access_token);
useEffect(() => {
if (!token || isTokenExpired(token)) {
let accessToken = token || Cookies.get("access_token");
if (!accessToken || isTokenExpired(accessToken)) {
dispatch(clearTokens());
router.replace("/login");
return;
}
// ✅ optional: if Redux was empty, rehydrate it from cookie
if (!token && accessToken) {
dispatch(setTokens({ access_token: accessToken, user: user || null }));
}
if (!user) {
router.replace("/login");
}
......
// lib/serverApi.ts
import { GameResponseProps, SingleGameResponse } from "@/types/game";
import { serverBaseQuery } from "./serverBaseQuery";
import { store } from "@/hooks/store";
import { cookies } from "next/headers";
export async function getAllGames(): Promise<GameResponseProps> {
// No token required
return serverBaseQuery<GameResponseProps>("/api/get-games");
return serverBaseQuery("/api/get-games");
}
console.log(store.getState());
export async function getSingleGame(id: string): Promise<SingleGameResponse> {
return serverBaseQuery<SingleGameResponse>(`/api/game/${id}`, {
token: store.getState().auth.access_token,
const cookieStore = await cookies();
const access_token = cookieStore.get("access_token")?.value;
console.log(access_token);
return serverBaseQuery(`/api/game/${id}`, {
token: access_token,
withAuth: true,
});
}
\ No newline at end of file
......@@ -18,8 +18,6 @@ export async function serverBaseQuery<T>(
withAuth?: boolean;
} = {}
): Promise<T> {
console.log("state", token);
const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}${endpoint}`, {
method,
......
import { RoleProps, User } from "@/types/auth";
import { createSlice } from "@reduxjs/toolkit";
import Cookies from "js-cookie";
type Data = {
access_token: string,
user: User | null,
......@@ -60,6 +60,7 @@ export const authSlice = createSlice({
if (isBrowser) {
localStorage.removeItem("token");
}
Cookies.remove("access_token");
},
}
})
......
......@@ -60,8 +60,8 @@ export default function Palette(mode: ThemeMode) {
},
...paletteColor,
background: {
default: mode === ThemeMode.DARK ? grayColors[1] : "#fff",
paper: mode === ThemeMode.DARK ? grayColors[0] : "#fff"
default: mode === ThemeMode.DARK ? "#11011E" : "#fff",
paper: mode === ThemeMode.DARK ? "rgba(41, 1, 57, 0.81)" : "#fff"
},
text: {
primary: mode === ThemeMode.DARK ? titleColors[0] : titleColors[0],
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment