Commit 0af072d4 by Arjun Jhukal

updated the game listing at admin

parent 55e6fde1
......@@ -2,6 +2,16 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'sweepstake.webjuwa.com',
port: '',
pathname: '/storage/**',
},
],
},
};
export default nextConfig;
......@@ -17,6 +17,7 @@
"next": "15.5.3",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-quill-new": "3.4.6",
"react-redux": "^9.2.0",
"yup": "^1.7.0"
},
......@@ -3832,6 +3833,12 @@
"node": ">=0.10.0"
}
},
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
......@@ -3839,6 +3846,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-diff": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
"license": "Apache-2.0"
},
"node_modules/fast-glob": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
......@@ -5205,6 +5218,19 @@
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
"license": "MIT"
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
......@@ -5647,6 +5673,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parchment": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz",
"integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==",
"license": "BSD-3-Clause"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
......@@ -5834,6 +5866,35 @@
],
"license": "MIT"
},
"node_modules/quill": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz",
"integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==",
"license": "BSD-3-Clause",
"dependencies": {
"eventemitter3": "^5.0.1",
"lodash-es": "^4.17.21",
"parchment": "^3.0.0",
"quill-delta": "^5.1.0"
},
"engines": {
"npm": ">=8.2.3"
}
},
"node_modules/quill-delta": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz",
"integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==",
"license": "MIT",
"dependencies": {
"fast-diff": "^1.3.0",
"lodash.clonedeep": "^4.5.0",
"lodash.isequal": "^4.5.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/react": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
......@@ -5867,6 +5928,21 @@
"integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==",
"license": "MIT"
},
"node_modules/react-quill-new": {
"version": "3.4.6",
"resolved": "https://registry.npmjs.org/react-quill-new/-/react-quill-new-3.4.6.tgz",
"integrity": "sha512-S2kEAwoKRo+xUIAEpb94fwiPe2QU3FmwIfQ+7Lkchf+izPa2nRu1mr4i4QxyVYg8TjHDryDUiOEYZuFEV45QFA==",
"license": "MIT",
"dependencies": {
"lodash-es": "^4.17.21",
"quill": "~2.0.2"
},
"peerDependencies": {
"quill-delta": "^5.1.0",
"react": "^16 || ^17 || ^18 || ^19",
"react-dom": "^16 || ^17 || ^18 || ^19"
}
},
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
......
......@@ -19,7 +19,8 @@
"react-dom": "19.1.0",
"react-redux": "^9.2.0",
"yup": "^1.7.0",
"@wandersonalwes/iconsax-react": "0.0.10"
"@wandersonalwes/iconsax-react": "0.0.10",
"react-quill-new": "3.4.6"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
......
"use client";
import { PATH } from '@/routes/PATH';
import { Box, Button, Typography } from '@mui/material'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/navigation';
import React from 'react'
export default function VerifyEmail() {
const router = useRouter();
return (
<Box className="max-w-[520px] mx-auto flex flex-col gap-3 items-center text-center">
<Image src={"/assets/images/verify-email.png"} alt='' width={180} height={140} />
......@@ -11,7 +16,9 @@ export default function VerifyEmail() {
get the fun started</h1>
{/* <Typography variant="h1" className='font-[700]'></Typography> */}
<p className='text-[14px] leading-[120%] font-normal lg:text-[16px] mb-4'>Check the link sent to <strong className='underline text-secondary'>abc@gmail.com</strong> to activate your account.</p>
<Button fullWidth size="large" type="submit" variant="contained" color="primary" className='!mb-6'>
<Button fullWidth size="large" type="submit" variant="contained" color="primary" className='!mb-6' onClick={() => {
router.replace(PATH.DASHBOARD.ROOT)
}}>
Verify now
</Button>
<h2 className='text-[20px] lg:text-[28px] leading-[120%] font-bold'>Don’t see the email?</h2>
......
import PageHeader from '@/components/molecules/PageHeader'
import { PATH } from '@/routes/PATH'
import AddGameForm from '@/components/pages/dashboard/adminDashboard/games/AddGameForm'
import React from 'react'
export default function AddGame() {
return (
<PageHeader title="Add Game" />
<>
<PageHeader title="Add Game" />
<AddGameForm />
</>
)
}
import PageHeader from '@/components/molecules/PageHeader'
import AddGameForm from '@/components/pages/dashboard/adminDashboard/games/AddGameForm'
import React from 'react'
export default function EditGame() {
return (
<>
<PageHeader title="Add Game" />
<AddGameForm />
</>
)
}
"use client";
import PageHeader from '@/components/molecules/PageHeader'
import AdminGameList from '@/components/pages/dashboard/adminDashboard/games'
import { PATH } from '@/routes/PATH'
import React from 'react'
......@@ -6,6 +9,7 @@ export default function AdminGames() {
return (
<div className="admin__game__root">
<PageHeader title="Games" cta={{ label: "Add New Game", url: PATH.ADMIN.GAMES.ADD_GAME.ROOT }} />
<AdminGameList />
</div>
)
}
......@@ -6,13 +6,13 @@ import React from 'react'
export default function ProviderWrapper({ children }: { children: React.ReactNode }) {
return (
<ClientProvider>
<ThemeContextProvider>
<ThemeCustomization>
<ThemeContextProvider>
<ThemeCustomization>
<ClientProvider>
{children}
<Toast />
</ThemeCustomization>
</ThemeContextProvider>
</ClientProvider>
</ClientProvider>
</ThemeCustomization>
</ThemeContextProvider>
)
}
......@@ -6,10 +6,11 @@
--black: #000;
--gray: #E0E0E3;
--light-gray: #F3F4F6;
--primary-light: #71717A;
--primary-light: #F8B1FE;
--primary: #B801C0;
--primary-dark: #3A013F;
--title: #0E0E11;
--para-light: #71717A;
/* Additional variables for dark mode */
--primary-grad: linear-gradient(90deg, #B801C0 0%, #E046DC 100%);
......@@ -26,7 +27,7 @@
--black: #FFF;
--gray: #2D2D30;
--light-gray: #1F1F23;
--primary-light: #A0A0A7;
--primary-light: #F8B1FE;
--primary: #D958DF;
--primary-dark: #7D0182;
--title: #F0F0F0;
......@@ -38,6 +39,7 @@
--text-regular: rgba(255, 255, 255, 0.80);
--text-title: rgba(255, 255, 255, 0.90);
--gray-scale: #7E7181;
--para-light: #71717A;
}
/* Map CSS vars to Tailwind theme tokens */
......@@ -55,6 +57,7 @@
--color-text-regular: var(--text-regular);
--color-text-title: var(--text-title);
--color-gray-scale: var(--gray-scale);
--color-para-light: var(--para-light)
}
@layer base {
......@@ -142,4 +145,12 @@
border-top: 1px solid rgba(255, 255, 255, 0.30);
background: linear-gradient(90deg, rgba(105, 162, 157, 0.20) 0%, rgba(147, 224, 217, 0.20) 100%);
}
body {
@apply !bg-white;
}
.MuiInputBase-input {
padding: 0 !important;
}
}
\ No newline at end of file
......@@ -9,10 +9,10 @@ export const metadata: Metadata = {
};
const inter = Inter({
subsets: ['latin'],
fallback: ['sans-serif'],
weight: ['300', '400', '500', '700'],
adjustFontFallback: false
subsets: ['latin'],
fallback: ['sans-serif'],
weight: ['300', '400', '500', '700'],
adjustFontFallback: false
});
export default function RootLayout({
......
"use client";
import { InputLabel } from "@mui/material";
import React from "react";
interface InputFileProps {
name: string;
label: string;
value: File | File[] | null;
onChange: (file: File | File[] | null) => void;
onBlur?: () => void;
required?: boolean;
accept?: string;
error?: string | boolean;
touched?: boolean;
multiple?: boolean;
}
export default function InputFile({
name,
label,
value,
onChange,
onBlur,
required,
accept,
error,
touched,
multiple = false,
}: InputFileProps) {
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files) return;
if (multiple) {
onChange(Array.from(files));
} else {
onChange(files[0]);
}
};
return (
<div className="input__field">
<InputLabel className="block text-sm font-semibold mb-2">
{label} {required && <span className="text-red-500">*</span>}
</InputLabel>
<label className="flex items-center justify-start border border-gray-300 gap-2 rounded-lg cursor-pointer hover:border-primary transition py-2 px-3">
<span className="bg-[#D8D8DD] text-title px-2 py-[2px] text-sm ">
Choose File
</span>
<span className="truncate text-gray-500">
{Array.isArray(value)
? value.map((f) => f.name).join(", ") || "No file chosen"
: value?.name || "No file chosen"}
</span>
<input
type="file"
name={name}
accept={accept}
hidden
multiple={multiple}
onChange={handleFileChange}
onBlur={onBlur}
/>
</label>
{touched && error && (
<span className="text-red-500 text-xs mt-1">{error}</span>
)}
</div>
);
}
"use client";
import { InputLabel } from "@mui/material";
import React from "react";
interface Option {
value: string;
label: string;
}
interface SelectFieldProps {
name: string;
label: string;
value: string;
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
onBlur?: (e: React.FocusEvent<HTMLSelectElement>) => void;
required?: boolean;
options: Option[];
error?: boolean;
touched?: boolean;
}
export default function SelectField({
name,
label,
value,
onChange,
onBlur,
required,
options,
error,
touched,
}: SelectFieldProps) {
return (
<div className="input__field">
<InputLabel className="block text-sm font-semibold mb-2">
{label} {required && <span className="text-red-500">*</span>}
</InputLabel>
<select
name={name}
value={value}
onChange={onChange}
onBlur={onBlur}
className="w-full border text-[14px] border-gray-300 rounded-lg px-3 py-[10px] focus:border-primary outline-0"
>
<option value="">Select the Type of Game</option>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
{touched && error && (
<span className="text-red-500 text-xs mt-1">{error}</span>
)}
</div>
);
}
......@@ -18,7 +18,7 @@ export default function Breadcrumb() {
return (
<Stack direction="row" spacing={1} alignItems="center" className='mb-2'>
<Link href="/" className='text-title font-[500] text-[10px] lg:text-[12px] leading-[120%]'>
<Link href="/" className='text-title font-medium text-[10px] lg:text-[12px] leading-[120%]'>
Home
</Link>
{breadcrumbs.map((crumb, idx) => (
......@@ -26,11 +26,11 @@ export default function Breadcrumb() {
<ArrowRight2 size={10} />
{idx === breadcrumbs.length - 1 ? (
<strong className='text-title !font-[500] text-[10px] lg:text-[12px] leading-[120%]' >
<strong className='text-title !font-medium text-[10px] lg:text-[12px] leading-[120%]' >
{crumb.label}
</strong>
) : (
<Link href={crumb.href} className='text-title font-[500] text-[10px] lg:text-[12px] leading-[120%]'>
<Link href={crumb.href} className='text-title font-medium text-[10px] lg:text-[12px] leading-[120%]'>
{crumb.label}
</Link>
)}
......
'use client';
// next
import dynamic from 'next/dynamic';
// material-ui
import Box from '@mui/material/Box';
// third-party
import 'react-quill-new/dist/quill.snow.css';
import { useState } from 'react';
// project imports
const ReactQuill = dynamic(() => import('react-quill-new'), { ssr: false });
interface Props {
value?: string;
editorMinHeight?: number;
onChange?: (value: string) => void;
}
export default function ReactQuillEditor({ value, editorMinHeight = 135, onChange }: Props) {
return (
<Box
sx={(theme) => ({
'& .quill': {
// bgcolor: 'background.paper',
// ...theme.applyStyles('dark', { bgcolor: 'secondary.main' }),
borderRadius: '4px',
'& .ql-toolbar': {
// bgcolor: 'secondary.100',
// ...theme.applyStyles('dark', { bgcolor: 'secondary.light' }),
borderColor: 'divider',
borderTopLeftRadius: '8px',
borderTopRightRadius: '8px'
},
// '& .ql-snow .ql-picker': {
// ...theme.applyStyles('dark', { color: 'secondary.500' })
// },
// '& .ql-snow .ql-stroke': {
// ...theme.applyStyles('dark', { stroke: theme.palette.secondary.main })
// },
'& .ql-container': {
// bgcolor: 'transparent',
// ...theme.applyStyles('dark', { bgcolor: 'background.default' }),
// borderColor: `${theme.palette.secondary.light} !important`,
borderBottomLeftRadius: '8px',
borderBottomRightRadius: '8px',
'& .ql-editor': { minHeight: editorMinHeight }
},
// ...(theme.direction === ThemeDirection.RTL && {
// '& .ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg': {
// right: '0%',
// left: 'inherit'
// }
// })
}
})}
>
<ReactQuill {...(value && { value })} {...(onChange && { onChange })} />
</Box>
);
}
"use client";
import React from "react";
import { CloseCircle } from "@wandersonalwes/iconsax-react";
import { closeToast } from "@/slice/toastSlice";
import { useAppDispatch, useAppSelector } from "@/hooks/hook";
import { CloseCircle } from "@wandersonalwes/iconsax-react";
export default function Toast() {
const { variant, message, isActive, autoTimeout, duration } = useAppSelector(
......@@ -17,9 +17,10 @@ export default function Toast() {
const timeout = setTimeout(() => {
dispatch(closeToast());
}, duration || 3000);
return () => clearTimeout(timeout);
}
}, [isActive, duration, dispatch, autoTimeout]);
}, [isActive, duration, autoTimeout, dispatch]);
if (!isActive) return null;
......@@ -34,10 +35,11 @@ export default function Toast() {
return (
<div
className={`fixed top-4 right-4 flex w-full max-w-sm items-start gap-3 rounded-xl border-l-4 px-4 py-3 shadow-lg transition-all duration-300 animate-in slide-in-from-right data-[state=closed]:slide-out-to-right data-[state=closed]:fade-out
${variantStyles[currentVariant]}`}
className={`fixed top-4 right-4 flex max-w-sm w-full items-start gap-3 rounded-xl border-l-4 px-4 py-3 shadow-lg transition-all duration-300 animate-in slide-in-from-right
data-[state=closed]:slide-out-to-right data-[state=closed]:fade-out
${variantStyles[currentVariant]}`}
>
<div className="flex flex-col flex-1">
<div className="flex flex-1 flex-col">
{variant && (
<h4 className="text-sm font-semibold">
{variant.charAt(0).toUpperCase() + variant.slice(1).toLowerCase()}
......@@ -45,10 +47,11 @@ export default function Toast() {
)}
{message && <p className="text-sm leading-snug">{message}</p>}
</div>
<button
type="button"
onClick={() => dispatch(closeToast())}
className="text-current hover:opacity-70 transition-opacity"
className="max-w-fit p-0 text-current transition-opacity hover:opacity-70"
>
<CloseCircle size="32" color="#FF8A65" />
</button>
......
......@@ -49,6 +49,10 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open'
flexShrink: 0,
whiteSpace: 'nowrap',
boxSizing: 'border-box',
'& .MuiDrawer-paper': {
backgroundColor: theme.palette.common.white,
color: theme.palette.text.primary,
},
variants: [
{
props: ({ open }) => open,
......
......@@ -56,7 +56,6 @@ export default function RegisterPage() {
password_confirmation: values.confirmPassword,
}).unwrap();
console.log("response", response)
dispatch(
showToast({
message: "User Registerd Successfully",
......
"use client";
import { useGetAllGamesQuery } from '@/services/gameApi'
import { GameItem } from '@/types/game';
import Image from 'next/image';
import Link from 'next/link';
import React from 'react'
function GameSkeleton() {
return (
<div className="bg-[#F4F6FC] p-4 lg:p-6 rounded-[16px] animate-pulse">
{/* Title Skeleton */}
<div className="h-4 w-3/4 bg-gray-300 rounded mb-4"></div>
{/* Type & Provider Skeleton */}
<div className="flex justify-between items-center mb-6">
<div className="w-[40%]">
<div className="h-3 w-1/2 bg-gray-200 rounded mb-1"></div>
<div className="h-3 w-3/4 bg-gray-300 rounded"></div>
</div>
<span className='h-[24px] w-[1px] bg-gray'></span>
<div className="w-[40%]">
<div className="h-3 w-1/2 bg-gray-200 rounded mb-1"></div>
<div className="h-3 w-3/4 bg-gray-300 rounded"></div>
</div>
</div>
{/* Active Players Skeleton */}
<div className="h-3 w-1/3 bg-gray-300 rounded mb-4"></div>
{/* Image Skeleton */}
<div className="relative aspect-[300/256] rounded-[12px] overflow-hidden mb-4 bg-gray-300"></div>
{/* Button Skeleton */}
<div className="h-8 w-1/3 bg-gray-300 rounded-full"></div>
</div>
);
}
export default function AdminGameList() {
const { data, isLoading } = useGetAllGamesQuery();
console.log(data);
return (
<div className="admin__games grid md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-6">
{isLoading &&
Array.from({ length: 8 }).map((_, idx) => <GameSkeleton key={idx} />)}
{!isLoading &&
data?.data?.data?.map((game: GameItem) => (
<div
key={game.id}
className="admin__game__card bg-[#F4F6FC] p-4 lg:p-6 rounded-[16px]"
>
{/* Game Name */}
<h2 className="text-16 leading-[120%] font-bold mb-4">
{game.name}
</h2>
{/* Type & Provider */}
<ul className="flex justify-between items-center mb-6">
<li className="w-[40%]">
<p className="mb-1 text-[14px] leading-[120%] text-para-light">
Type
</p>
<strong className="text-[14px] leading-[120%] font-[500] text-title">
{game.category || "N/A"}
</strong>
</li>
<span className="h-[24px] w-[1px] bg-gray"></span>
<li className="w-[40%]">
<p className="mb-1 text-[14px] leading-[120%] text-para-light">
Provider
</p>
<strong className="text-[14px] leading-[120%] font-[500] text-title">
{game.provider}
</strong>
</li>
</ul>
{/* Active Players */}
{game.activePlayers && (
<strong className="text-bold block mb-4 text-[12px]">
Active Players:{" "}
<span className="bg-primary-light rounded-[20px] py-1 px-2">
{game.activePlayers}
</span>
</strong>
)}
{/* Thumbnail */}
<div className="admin_game_image relative aspect-[300/256] rounded-[12px] overflow-hidden mb-4">
<Image
src={typeof game.thumbnail === "string" && game.thumbnail
? game.thumbnail
: "/assets/images/fallback.png"
}
alt={game.name || "Game Thumbnail"}
fill
className="object-cover"
/>
</div>
{/* CTA */}
<Link
href={`/game/${game.id}`}
className="ss-btn bg-primary-grad text-white max-w-fit"
>
Game Overview
</Link>
</div>
))}
</div>
)
}
......@@ -2,16 +2,19 @@ import { authApi } from "@/services/authApi";
import { configureStore } from "@reduxjs/toolkit";
import auth from "@/slice/authSlice";
import toastSlice from "@/slice/toastSlice";
import { gameApi } from "@/services/gameApi";
export const store = configureStore({
reducer: {
auth,
toastSlice,
[authApi.reducerPath]: authApi.reducer,
[gameApi.reducerPath]: gameApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
.concat(authApi.middleware)
.concat(gameApi.middleware)
})
......
"use client";
import { useAppSelector } from "@/hooks/hook";
import auth from "@/slice/authSlice";
import { useAppDispatch, useAppSelector } from "@/hooks/hook";
import auth, { clearTokens } from "@/slice/authSlice";
import { useRouter } from "next/navigation";
import React, { useEffect } from "react";
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
if (!exp) return true;
const now = Math.floor(Date.now() / 1000); // current time in seconds
return exp < now;
} catch (error) {
console.error("Failed to decode token:", error);
return true; // treat invalid token as expired
}
}
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);
console.log(user);
useEffect(() => {
if (!token || isTokenExpired(token)) {
dispatch(clearTokens());
router.replace("/login");
return;
}
if (!user) {
router.replace("/login");
}
}, [user, router]);
}, [token, user, dispatch, router]);
if (!user) return null;
......
import { createApi } from "@reduxjs/toolkit/query/react";
import { baseQuery } from "./baseQuery";
import { GameProps, GameResponseProps } from "@/types/game";
import { GlobalResponse } from "@/types/config";
export const gameApi = createApi({
reducerPath: "gameApi",
baseQuery: baseQuery,
tagTypes: ["games"],
endpoints: (builder) => ({
addGame: builder.mutation<GlobalResponse, FormData>({
query: (body) => ({
url: "/api/admin/add-game",
method: "POST",
body: body
}),
invalidatesTags: ["games"]
}),
getAllGames: builder.query<GameResponseProps, void>({
query: () => ({
url: '/api/admin/games',
method: 'GET',
}),
providesTags: ['games']
}),
getGameById: builder.query<GameProps[], { id: number | string }>({
query: ({ id }) => ({
url: `/api/admin/games/${id}`,
method: 'GET',
}),
}),
updateGameById: builder.mutation<GameProps[], { id: number }>({
query: ({ id }) => ({
url: `/api/admin/games/${id}`,
method: "POST"
})
}),
deleteGameById: builder.mutation<GlobalResponse, { id: number }>({
query: ({ id }) => ({
url: `/api/admin/games/${id}`,
method: "DELETE"
})
})
})
})
export const { useAddGameMutation, useGetAllGamesQuery, useLazyGetGameByIdQuery, useUpdateGameByIdMutation, useDeleteGameByIdMutation } = gameApi;
\ No newline at end of file
'use client';
import { Provider, ReactNode, useState } from 'react';
// next
import { useServerInsertedHTML } from 'next/navigation';
// material-ui
import createCache from '@emotion/cache';
import { CacheProvider as DefaultCacheProvider } from '@emotion/react';
import type { EmotionCache, Options as OptionsOfCreateCache } from '@emotion/cache';
type NextAppDirEmotionCacheProviderProps = {
/** This is the options passed to createCache() from 'import createCache from "@emotion/cache"' */
options: Omit<OptionsOfCreateCache, 'insertionPoint'>;
/** By default <CacheProvider /> from 'import { CacheProvider } from "@emotion/react"' */
CacheProvider?: Provider<EmotionCache>;
children: ReactNode;
};
// This implementation is taken from https://github.com/garronej/tss-react/blob/main/src/next/appDir.tsx
export function NextAppDirEmotionCacheProvider({
options,
CacheProvider = DefaultCacheProvider as Provider<EmotionCache>,
children
}: NextAppDirEmotionCacheProviderProps) {
const [{ cache, flush }] = useState(() => {
const cache = createCache(options);
cache.compat = true;
const prevInsert = cache.insert;
let inserted: { name: string; isGlobal: boolean }[] = [];
cache.insert = (...args) => {
const [selector, serialized] = args;
if (cache.inserted[serialized.name] === undefined) {
inserted.push({
name: serialized.name,
isGlobal: !selector
});
}
return prevInsert(...args);
};
const flush = () => {
const prevInserted = inserted;
inserted = [];
return prevInserted;
};
return { cache, flush };
});
useServerInsertedHTML(() => {
const names = flush();
if (names.length === 0) {
return null;
}
let styles = '';
let dataEmotionAttribute = cache.key;
const globals: {
name: string;
style: string;
}[] = [];
names.forEach(({ name, isGlobal }) => {
const style = cache.inserted[name];
if (typeof style !== 'boolean') {
if (isGlobal) {
globals.push({ name, style: style || '' });
} else {
styles += style;
dataEmotionAttribute += ` ${name}`;
}
}
});
return (
<>
{globals.map(({ name, style }) => (
<style key={name} data-emotion={`${cache.key}-global ${name}`} dangerouslySetInnerHTML={{ __html: style }} />
))}
{styles && <style data-emotion={dataEmotionAttribute} dangerouslySetInnerHTML={{ __html: styles }} />}
</>
);
});
return <CacheProvider value={cache}>{children}</CacheProvider>;
}
......@@ -4,53 +4,25 @@ import { StyledEngineProvider, ThemeProvider } from '@mui/material/styles'
import React from 'react'
import Palette from './palette';
import { ThemeMode } from '@/config';
import { NextAppDirEmotionCacheProvider } from './emotionCache';
export default function ThemeCustomization({ children }: { children: React.ReactNode }) {
const globalStyles = {
':root': {
'--white': '#FFF',
'--black': '#000',
'--gray': '#E0E0E3',
'--light-gray': '#F3F4F6',
'--primary-light': '#71717A',
'--primary': '#B801C0',
'--primary-dark': '#3A013F',
'--text': '#0E0E11',
'--primary-grad': 'linear-gradient(90deg, #B801C0 0%, #E046DC 100%)',
'--secondary-grad': 'linear-gradient(90deg, #69A29D 0%, #93E0D9 100%)',
'--secondary': '#93E0D8',
'--text-regular': 'rgba(0, 0, 0, 0.80)',
'--text-title': 'rgba(0, 0, 0, 0.90)',
'--gray-scale': '#7E7181',
},
'.dark': {
'--white': '#000',
'--black': '#FFF',
'--gray': '#2D2D30',
'--light-gray': '#1F1F23',
'--primary-light': '#A0A0A7',
'--primary': '#D958DF',
'--primary-dark': '#7D0182',
'--text': '#F0F0F0',
'--primary-grad': 'linear-gradient(90deg, #B100B8 0%, #F335ED 100%)',
'--secondary-grad': 'linear-gradient(90deg, #69A29D 0%, #93E0D9 100%)',
'--secondary': '#93E0D8',
'--text-regular': 'rgba(255, 255, 255, 0.80)',
'--text-title': 'rgba(255, 255, 255, 0.90)',
'--gray-scale': '#7E7181',
},
};
const theme = Palette(ThemeMode.LIGHT);
return (
<StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}>
<CssBaseline enableColorScheme />
<GlobalStyles styles={globalStyles}
/>
{children}
</ThemeProvider>
<NextAppDirEmotionCacheProvider options={{ key: 'mui' }}>
<ThemeProvider theme={theme}>
<CssBaseline enableColorScheme />
<GlobalStyles styles={globalStyles}
/>
{children}
</ThemeProvider>
</NextAppDirEmotionCacheProvider>
</StyledEngineProvider>
)
}
......@@ -69,7 +69,8 @@ export default function Palette(mode: ThemeMode) {
MuiInputLabel: {
styleOverrides: {
root: {
color: 'rgba(255, 255, 255, 0.80)',
// color: 'rgba(255, 255, 255, 0.80)',
color: titleColors[0],
fontFamily: '"Hiragino Sans"',
fontSize: '12px',
fontWeight: 400,
......@@ -82,7 +83,6 @@ export default function Palette(mode: ThemeMode) {
MuiOutlinedInput: {
styleOverrides: {
root: {
// display: "block",
width: "100%",
borderRadius: '27px',
background: 'rgba(118, 107, 120, 0.55)',
......@@ -96,7 +96,7 @@ export default function Palette(mode: ThemeMode) {
},
},
input: {
padding: '12px 16px',
padding: '8px 12px',
'&::placeholder': {
color: 'rgba(255, 255, 255, 0.2)',
fontWeight: 300,
......@@ -105,12 +105,66 @@ export default function Palette(mode: ThemeMode) {
},
},
},
MuiInput: {
defaultProps: {
disableUnderline: true,
},
styleOverrides: {
root: {
width: "100%",
borderRadius: "var(--border-radius-lg, 10px)",
border: "1px solid var(--Gray, #E0E0E3)",
background: "transparent",
color: "inherit",
padding: "8px 12px",
"&:hover": {
borderColor: "#B801C0",
},
"&.Mui-focused": {
borderColor: "#B801C0",
},
},
input: {
p0dding: 0,
"&::placeholder": {
color: "var(--Gray, #999)",
fontWeight: 400,
fontSize: "14px",
},
},
},
},
MuiTextField: {
styleOverrides: {
root: {
"& .MuiInputBase-root": {
borderRadius: "8px",
background: "#FFF",
},
},
},
},
MuiSelect: {
styleOverrides: {
root: {
borderRadius: "8px",
background: "#FFF",
"&.Mui-focused": {
borderColor: "#B801C0",
},
},
},
},
MuiButton: {
styleOverrides: {
root: {
borderRadius: '27px',
padding: '12px 24px',
textAlign: 'center',
textTransform: 'capitalize',
width: '100%',
'&:disabled': {
opacity: 0.5,
......
......@@ -25,4 +25,10 @@ export type ImageProps = {
alt: string;
width?: string;
height?: string;
}
export interface GlobalResponse {
status: string;
data: [],
message: string
}
\ No newline at end of file
export interface GameProps {
name: string;
category?: string;
description: string;
thumbnail?: File | null;
screenshots?: File | File[] | null;
subgames?: File | File[] | null;
api: string;
provider: string;
profit?: string;
}
export interface GameItem extends GameProps {
id: number;
activePlayers?: string | number
}
export interface Pagination {
total: number;
count: number;
per_page: number;
current_page: number;
total_pages: number;
}
export interface GameResponseProps {
data: {
data: GameItem[];
pagination: Pagination;
}
message: string;
success: boolean;
}
// ✅ Initial Values for Formik
export const gameInitialValues: GameProps = {
name: "",
category: "",
description: "",
thumbnail: null,
screenshots: [],
subgames: [],
api: "",
provider: "",
profit: "",
};
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