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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@headlessui/react": "^1.7.18",
"@heroicons/react": "^2.1.1",
"@knucklebones/common": "workspace:*",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-toggle-group": "^1.0.4",
"@use-gesture/react": "^10.3.0",
"clsx": "^2.1.0",
Expand Down
9 changes: 3 additions & 6 deletions apps/front/src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type * as React from 'react'
import { clsx } from 'clsx'
import { IconWrapper } from './IconWrapper'

export interface ButtonProps<E extends React.ElementType> {
// Not a fan of having this prop
Expand Down Expand Up @@ -46,13 +47,9 @@ export function Button<E extends React.ElementType = typeof defaultElement>({
props.className
)}
>
{leftIcon !== undefined && (
<div className='aspect-square h-6'>{leftIcon}</div>
)}
{leftIcon !== undefined && <IconWrapper>{leftIcon}</IconWrapper>}
<div className='translate-y-px'>{children}</div>
{rightIcon !== undefined && (
<div className='aspect-square h-6'>{rightIcon}</div>
)}
{rightIcon !== undefined && <IconWrapper>{rightIcon}</IconWrapper>}
</Component>
)
}
52 changes: 33 additions & 19 deletions apps/front/src/components/Dice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import * as React from 'react'
import { clsx } from 'clsx'
import { Transition } from '@headlessui/react'

type DiceVariant = 'base' | 'small'
interface DiceProps {
type DiceSize = 'medium' | 'small'
type DiceVariant = 'solid' | 'outline'

interface DiceStyle {
size?: DiceSize
variant?: DiceVariant
}

interface DiceProps extends DiceStyle {
value?: number
className?: string
count?: number
showUndefined?: boolean
variant?: DiceVariant
}
interface DotProps {
className?: string
Expand All @@ -18,28 +24,31 @@ const baseClassName =
'aspect-square h-12 portrait:md:h-16 landscape:md:h-12 landscape:lg:h-16'
const smallClassName = 'aspect-square h-6'

const VariantContext = React.createContext<DiceVariant>('base')
const DiceStyleContext = React.createContext<Required<DiceStyle>>({
size: 'medium',
variant: 'solid'
})

function DicePlaceholder({ className }: DotProps) {
const variant = React.useContext(VariantContext)
const { size } = React.useContext(DiceStyleContext)
return (
<div
className={clsx(className, {
[baseClassName]: variant === 'base',
[smallClassName]: variant === 'small'
[baseClassName]: size === 'medium',
[smallClassName]: size === 'small'
})}
></div>
)
}

function DiceContainer({ children }: React.PropsWithChildren) {
const variant = React.useContext(VariantContext)
const { size } = React.useContext(DiceStyleContext)
return (
<div
className={clsx(
'grid h-full w-full grid-cols-3 grid-rows-3 place-items-center p-1',
{
'lg:p-2': variant === 'base'
'lg:p-2': size === 'medium'
}
)}
>
Expand All @@ -49,16 +58,16 @@ function DiceContainer({ children }: React.PropsWithChildren) {
}

function Dot({ className }: DotProps) {
const variant = React.useContext(VariantContext)
const { size } = React.useContext(DiceStyleContext)
return (
<div
className={clsx(
'aspect-square rounded-full bg-slate-900 dark:bg-slate-200',
className,
{
'h-2 portrait:md:h-3 landscape:md:h-2 landscape:lg:h-3':
variant === 'base',
'h-1': variant === 'small'
size === 'medium',
'h-1': size === 'small'
}
)}
></div>
Expand Down Expand Up @@ -150,13 +159,17 @@ const DiceMap: Record<number | 'undefined', React.ComponentType> = {

function SimpleDice({ value, className, count = 1 }: DiceProps) {
const DiceValue = DiceMap[value ?? 'undefined']
const variant = React.useContext(VariantContext)
const { size, variant } = React.useContext(DiceStyleContext)
return (
<div
// cva pour rendre ça plus lisible avec une meilleur définition des variantes composites
className={clsx(
'flex select-none flex-row items-center justify-center rounded border',
className,
{
'border-slate-900 dark:border-slate-50': variant === 'outline'
},
variant === 'solid' && {
'border-stone-400 bg-stone-300 shadow-stone-400 dark:border-stone-600 dark:bg-stone-500 dark:shadow-stone-600':
count === 1,
'border-amber-400 bg-amber-300 shadow-amber-400 dark:border-amber-700 dark:bg-amber-600 dark:shadow-amber-700':
Expand All @@ -165,9 +178,9 @@ function SimpleDice({ value, className, count = 1 }: DiceProps) {
count === 3
},
{
[baseClassName]: variant === 'base',
shadow: variant === 'base',
[smallClassName]: variant === 'small'
[baseClassName]: size === 'medium',
shadow: size === 'medium',
[smallClassName]: size === 'small'
}
)}
>
Expand All @@ -181,7 +194,8 @@ export function Dice({
className,
count = 1,
showUndefined = false,
variant = 'base'
variant = 'solid',
size = 'medium'
}: DiceProps) {
// Temporarily store the dice value to keep the dice shown during the
// transition after it has been unset. While the animation is done, the cached
Expand All @@ -198,7 +212,7 @@ export function Dice({

return (
// Sharing the variant in a context so it's easier to drill it down
<VariantContext.Provider value={variant}>
<DiceStyleContext.Provider value={{ variant, size }}>
<Transition
show={Boolean(value) || showUndefined}
className={clsx('transition duration-100 ease-in-out', className)}
Expand All @@ -219,6 +233,6 @@ export function Dice({
{cachedValue === undefined && !showUndefined && (
<DicePlaceholder className={className} />
)}
</VariantContext.Provider>
</DiceStyleContext.Provider>
)
}
9 changes: 5 additions & 4 deletions apps/front/src/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Trans } from 'react-i18next'
import { CodeBracketIcon, EnvelopeIcon } from '@heroicons/react/24/outline'
import { IconWrapper } from './IconWrapper'

export function Footer() {
return (
Expand All @@ -23,19 +24,19 @@ export function Footer() {
target='_blank'
rel='noreferrer'
>
<div className='aspect-square h-6'>
<IconWrapper>
<CodeBracketIcon />
</div>
</IconWrapper>
</a>
<a
className='text-slate-900 transition-all hover:text-slate-900/80 dark:text-slate-200 dark:hover:text-slate-50/80'
href='mailto:contact@knucklebones.io'
target='_blank'
rel='noreferrer'
>
<div className='aspect-square h-6'>
<IconWrapper>
<EnvelopeIcon />
</div>
</IconWrapper>
</a>
</div>
</>
Expand Down
9 changes: 4 additions & 5 deletions apps/front/src/components/Game.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react'
import { useIsOnMobile } from '../hooks/detectDevice'
import { useNoIndex } from '../hooks/useNoIndex'
import { useGameWhileLoading } from './GameContext'
import { GameMode } from './GameMode'
import { GameOutcome } from './GameOutcome'
import { HowToPlayModal } from './HowToPlay'
import { Loading } from './Loading'
Expand All @@ -23,20 +24,18 @@ export function Game() {

const { errorMessage, clearErrorMessage } = gameStore

// Pas tip top je trouve, mais virtuellement ça marche
const gameOutcome = <GameOutcome />

return (
<>
<SideBarActions>
<HowToPlayModal />
<QRCodeModal />
<OutcomeHistory />
{isOnMobile && gameOutcome}
{isOnMobile && <GameOutcome />}
<GameMode />
</SideBarActions>
<div ref={gameRef} className='flex flex-col items-center justify-around'>
<PlayerTwoBoard />
{!isOnMobile && gameOutcome}
{!isOnMobile && <GameOutcome />}
<PlayerOneBoard />
<WarningToast message={errorMessage} onDismiss={clearErrorMessage} />
</div>
Expand Down
9 changes: 7 additions & 2 deletions apps/front/src/components/GameContext/useGameSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ export function useGameSetup() {
if (readyState === ReadyState.OPEN) {
initGame(
{ roomKey, playerId },
{ playerType: 'human', boType: state?.boType }
{
playerType: 'human',
boType: state?.boType,
gameMode: state?.gameMode ?? 'classic'
}
)
.then(async () => {
// À déplacer côté serveur
Expand All @@ -65,7 +69,8 @@ export function useGameSetup() {
{
playerType: 'ai',
difficulty: state?.difficulty,
boType: state?.boType
boType: state?.boType,
gameMode: state?.gameMode
}
)
}
Expand Down
30 changes: 30 additions & 0 deletions apps/front/src/components/GameMode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useTranslation } from 'react-i18next'
import { Button } from './Button'
import { Dice } from './Dice'
import { useGame } from './GameContext'
import { Popover, PopoverContent, PopoverTrigger } from './Popover'

export function GameMode() {
const { gameMode } = useGame()
const { t } = useTranslation()

return (
<Popover>
<PopoverTrigger>
{/* Va falloir adapter Button et IconButton pour passer des refs
Malheureusement c'est pas super compatible avec les composants
polymorphiques, mais en même temps, on en a pas tellement besoin ici */}
<Button
variant='ghost'
leftIcon={<Dice value={5} variant='outline' size='small' />}
>
{t('game-settings.game-mode.label')} :{' '}
{t(`game-settings.game-mode.${gameMode}`)}
</Button>
</PopoverTrigger>
<PopoverContent className='whitespace-pre-line'>
{t(`game-settings.game-mode.info`)}
</PopoverContent>
</Popover>
)
}
24 changes: 21 additions & 3 deletions apps/front/src/components/GameSettings/GameSettingsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,42 @@ import { v4 as uuidv4 } from 'uuid'
import {
type GameSettings,
type Difficulty,
type PlayerType
type PlayerType,
type GameMode
} from '@knucklebones/common'
import { Button } from '../Button'
import { Modal, type ModalProps } from '../Modal'
import { InfoPopover } from '../Popover'
import { type Option, ToggleGroup } from '../ToggleGroup'
import {
getBoTypeOptions,
getDifficultyOptions,
convertToBoType,
type StringBoType
type StringBoType,
getGameModeOptions
} from './options'

interface GameSettingProps<T> {
label: string
info?: string
options: Array<Option<T>>
value: T
onValueChange(value: T): void
}

function GameSetting<T>({
label,
info,
options,
value,
onValueChange
}: GameSettingProps<T>) {
return (
<div className='grid grid-cols-1 gap-2'>
<label className='text-lg md:text-xl'>{label}</label>
<div className='flex gap-2'>
<label className='text-lg md:text-xl'>{label}</label>
{info !== undefined && <InfoPopover>{info}</InfoPopover>}
</div>
{/* Accessibility? */}
<ToggleGroup
mandatory
Expand All @@ -58,6 +66,7 @@ export function GameSettingsModal({
}: GameSettingsProps) {
const [difficulty, setDifficulty] = React.useState<Difficulty>('medium')
const [boType, setBoType] = React.useState<StringBoType>('indefinite')
const [gameMode, setGameMode] = React.useState<GameMode>('classic')
const { t } = useTranslation()

return (
Expand All @@ -78,6 +87,14 @@ export function GameSettingsModal({
onValueChange={setBoType}
options={getBoTypeOptions()}
/>
<GameSetting
label={t('game-settings.game-mode.label')}
// Voir pour la mise en force (mode de jeu en gras)
info={t('game-settings.game-mode.info')}
value={gameMode}
onValueChange={setGameMode}
options={getGameModeOptions()}
/>
<Button
as={Link}
size='medium'
Expand All @@ -87,6 +104,7 @@ export function GameSettingsModal({
state={
{
playerType: playerType!,
gameMode,
boType: convertToBoType(boType),
difficulty: playerType === 'ai' ? difficulty : undefined
} satisfies GameSettings
Expand Down
19 changes: 18 additions & 1 deletion apps/front/src/components/GameSettings/options.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
import { t } from 'i18next'
import { type BoType, type Difficulty } from '@knucklebones/common'
import {
type GameMode,
type BoType,
type Difficulty
} from '@knucklebones/common'
import { type Option } from '../ToggleGroup'

export function getGameModeOptions(): Array<Option<GameMode>> {
return [
{
value: 'classic',
label: t('game-settings.game-mode.classic')
},
{
value: 'dice-pool',
label: t('game-settings.game-mode.dice-pool')
}
]
}

export function getDifficultyOptions(): Array<Option<Difficulty>> {
return [
{
Expand Down
Loading