Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@types/express-session": "^1.18.0",
"cookie-session": "^2.1.0",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-session": "^1.18.0",
"jsonwebtoken": "^9.0.2",
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"dependencies": {
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-slot": "^1.0.2",
Expand Down
39 changes: 39 additions & 0 deletions apps/frontend/src/components/ExitGameModel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
AlertDialogAction,
AlertDialogCancel,
AlertDialogTitle,
AlertDialogHeader,
AlertDialogFooter,
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogDescription,
} from './ui/alert-dialog';

const ExitGameModel = ({ onClick } : {onClick : () => void}) => {

return (
<AlertDialog>
<AlertDialogTrigger className='w-24 h-12 bg-stone-800 rounded-md font-semibold text-white'>Exit</AlertDialogTrigger>
<AlertDialogContent className='bg-stone-800 border-stone-800'>
<AlertDialogHeader>
<AlertDialogTitle className='text-white font-mono'>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription className='text-white font-mono'>
This action cannot be undone. This will be considered as a loss.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className='font-mono bg-[#739552] text-white font-semibold hover:bg-[#b2e084] hover:text-gray-700 border-none'>Continue</AlertDialogCancel>
<AlertDialogAction
onClick={onClick}
className='bg-[#e2e6aa] min-w-20 text-gray-900 hover:text-slate-100 hover:bg-[#bbc259] font-semibold'
>
Exit
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

export default ExitGameModel;
139 changes: 139 additions & 0 deletions apps/frontend/src/components/ui/alert-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import * as React from 'react';
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';

import { cn } from '@/lib/utils';
import { buttonVariants } from '@/components/ui/button';

const AlertDialog = AlertDialogPrimitive.Root;

const AlertDialogTrigger = AlertDialogPrimitive.Trigger;

const AlertDialogPortal = AlertDialogPrimitive.Portal;

const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className,
)}
{...props}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;

const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className,
)}
{...props}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;

const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-2 text-center sm:text-left',
className,
)}
{...props}
/>
);
AlertDialogHeader.displayName = 'AlertDialogHeader';

const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className,
)}
{...props}
/>
);
AlertDialogFooter.displayName = 'AlertDialogFooter';

const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold', className)}
{...props}
/>
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;

const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName;

const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;

const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: 'outline' }),
'mt-2 sm:mt-0',
className,
)}
{...props}
/>
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;

export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};
69 changes: 42 additions & 27 deletions apps/frontend/src/screens/Game.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const GAME_ADDED = 'game_added';
export const USER_TIMEOUT = 'user_timeout';
export const GAME_TIME = 'game_time';
export const GAME_ENDED = 'game_ended';
export const EXIT_GAME = 'exit_game';
export enum Result {
WHITE_WINS = 'WHITE_WINS',
BLACK_WINS = 'BLACK_WINS',
Expand All @@ -33,14 +34,14 @@ export interface GameResult {
by: string;
}


const GAME_TIME_MS = 10 * 60 * 1000;

import { useRecoilValue, useSetRecoilState } from 'recoil';

import { movesAtom, userSelectedMoveIndexAtom } from '@repo/store/chessBoard';
import GameEndModal from '@/components/GameEndModal';
import { Waitopponent } from '@/components/ui/waitopponent';
import ExitGameModel from '@/components/ExitGameModel';

const moveAudio = new Audio(MoveSound);

Expand All @@ -61,10 +62,7 @@ export const Game = () => {
const [added, setAdded] = useState(false);
const [started, setStarted] = useState(false);
const [gameMetadata, setGameMetadata] = useState<Metadata | null>(null);
const [result, setResult] = useState<
GameResult
| null
>(null);
const [result, setResult] = useState<GameResult | null>(null);
const [player1TimeConsumed, setPlayer1TimeConsumed] = useState(0);
const [player2TimeConsumed, setPlayer2TimeConsumed] = useState(0);

Expand Down Expand Up @@ -132,7 +130,8 @@ export const Game = () => {

case GAME_ENDED:
const wonBy = message.payload.status === 'COMPLETED' ?
message.payload.result !== 'DRAW' ? 'CheckMate' : 'Draw' : 'Timeout';
message.payload.result !== 'DRAW' ? 'CheckMate' : 'Draw'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we improve this? it's too nasted...


 let wonBy;
  switch (message.payload.status) {
    case 'COMPLETED':
      wonBy = message.payload.result !== 'DRAW' ? 'CheckMate' : 'Draw';
      break;
    case 'PLAYER_EXIT':
      wonBy = 'Player Exit';
      break;
    default:
      wonBy = 'Timeout';
  }

  setResult({
    result: message.payload.result,
    by: wonBy,
    blackPlayer: message.payload.blackPlayer,
    whitePlayer: message.payload.whitePlayer,
  });
  setStarted(false);

: message.payload.status === 'PLAYER_EXIT'? 'Player Exit' : 'Timeout';
setResult({
result: message.payload.result,
by: wonBy,
Expand All @@ -148,8 +147,8 @@ export const Game = () => {
blackPlayer: message.payload.blackPlayer,
whitePlayer: message.payload.whitePlayer,
});

setStarted(false);

break;

case USER_TIMEOUT:
Expand Down Expand Up @@ -226,6 +225,18 @@ export const Game = () => {
);
};

const handleExit = () => {
socket?.send(
JSON.stringify({
type: EXIT_GAME,
payload: {
gameId,
},
}),
);
navigate('/');
};

if (!socket) return <div>Connecting...</div>;

return (
Expand Down Expand Up @@ -253,26 +264,24 @@ export const Game = () => {
<div>
{started && (
<div className="mb-4">
<div className="flex justify-between">
<UserAvatar
name={
<div className="flex justify-between">
<UserAvatar
name={
user.id === gameMetadata?.whitePlayer?.id
? gameMetadata?.blackPlayer?.name
: gameMetadata?.whitePlayer?.name ?? ''
}
/>
{getTimer(
user.id === gameMetadata?.whitePlayer?.id
? gameMetadata?.blackPlayer?.name
: gameMetadata?.whitePlayer?.name ?? ''
}
/>
{getTimer(
user.id === gameMetadata?.whitePlayer?.id
? player2TimeConsumed
: player1TimeConsumed,
)}
</div>
? player2TimeConsumed
: player1TimeConsumed,
)}
</div>
</div>
)}
<div>
<div
className={`w-full flex justify-center text-white`}
>
<div className={`w-full flex justify-center text-white`}>
<ChessBoard
started={started}
gameId={gameId ?? ''}
Expand Down Expand Up @@ -306,10 +315,12 @@ export const Game = () => {
</div>
</div>
<div className="rounded-md pt-2 bg-bgAuxiliary3 flex-1 overflow-auto h-[95vh]">
{!started && (
<div className="pt-8 flex justify-center w-full">
{!started? (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this, not getting this condition.

why we show

<div className="p-8 flex justify-center w-full">
                  <ExitGameModel onClick={() => handleExit()} />
                </div>

if not started

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the game is not started the Play button and Waiting loader will render, if it's started the Exit Button will render.
The

you have mentioned above is the component of Exit button.

Please let me know if it seems complex at first glance, i'll replace the ternary with if else.

<div className="p-8 flex justify-center w-full">
{added ? (
<div className="text-white"><Waitopponent/></div>
<div className="text-white">
<Waitopponent />
</div>
) : (
gameId === 'random' && (
<Button
Expand All @@ -326,6 +337,10 @@ export const Game = () => {
)
)}
</div>
) : (
<div className="p-8 flex justify-center w-full">
<ExitGameModel onClick={() => handleExit()} />
</div>
)}
<div>
<MovesTable />
Expand Down
6 changes: 5 additions & 1 deletion apps/ws/src/Game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { db } from './db';
import { randomUUID } from 'crypto';
import { socketManager, User } from './SocketManager';

type GAME_STATUS = 'IN_PROGRESS' | 'COMPLETED' | 'ABANDONED' | 'TIME_UP';
type GAME_STATUS = 'IN_PROGRESS' | 'COMPLETED' | 'ABANDONED' | 'TIME_UP' | 'PLAYER_EXIT';
type GAME_RESULT = "WHITE_WINS" | "BLACK_WINS" | "DRAW";

const GAME_TIME_MS = 10 * 60 * 60 * 1000;
Expand Down Expand Up @@ -313,6 +313,10 @@ export class Game {
}, timeLeft);
}

async exitGame(user : User) {
this.endGame('PLAYER_EXIT', user.userId === this.player2UserId ? 'WHITE_WINS' : 'BLACK_WINS');
}

async endGame(status: GAME_STATUS, result: GAME_RESULT) {
const updatedGame = await db.game.update({
data: {
Expand Down
11 changes: 11 additions & 0 deletions apps/ws/src/GameManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
GAME_ALERT,
GAME_ADDED,
GAME_ENDED,
EXIT_GAME,
} from './messages';
import { Game, isPromoting } from './Game';
import { db } from './db';
Expand Down Expand Up @@ -98,6 +99,16 @@ export class GameManager {
}
}

if (message.type === EXIT_GAME){
const gameId = message.payload.gameId;
const game = this.games.find((game) => game.gameId === gameId);

if (game) {
game.exitGame(user);
this.removeGame(game.gameId)
}
}

if (message.type === JOIN_ROOM) {
const gameId = message.payload?.gameId;
if (!gameId) {
Expand Down
1 change: 1 addition & 0 deletions apps/ws/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export const GAME_ENDED = 'game_ended';
export const GAME_ALERT = 'game_alert';
export const GAME_ADDED = 'game_added';
export const GAME_TIME = 'game_time';
export const EXIT_GAME = 'exit_game';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "GameStatus" ADD VALUE 'PLAYER_EXIT';
Loading