Skip to content

Commit

Permalink
style: Update design (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
romantech authored Feb 18, 2024
1 parent f73ab7a commit 026f594
Show file tree
Hide file tree
Showing 26 changed files with 152 additions and 123 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,22 @@

#### Gameplay GIF

![gameplay](https://github.com/romantech/tic-tac-toe/assets/8604840/86e3e194-2c9a-4509-8188-4e33462f0247)
![gameplay](https://github.com/romantech/tic-tac-toe/assets/8604840/fe46607c-c8a3-4f05-9c1d-a0912f2f4577)

#### Home Screen

![mobile-1](https://github.com/romantech/tic-tac-toe/assets/8604840/872c4e63-9a5e-46ff-96bb-1767e7dca5c6)
![mobile-1](https://github.com/romantech/tic-tac-toe/assets/8604840/51d8b01a-9768-4134-ad7b-2f5c52134d20)

#### Game Configuration

![mobile-2](https://github.com/romantech/tic-tac-toe/assets/8604840/fb7350f2-9e04-493b-a29a-36bed9bf37b8)
![mobile-2](https://github.com/romantech/tic-tac-toe/assets/8604840/ade907fd-b16a-417e-a1f1-207a8d84e84e)

#### Game Screen

![mobile-3](https://github.com/romantech/tic-tac-toe/assets/8604840/dbd5f0cb-e772-4526-9ad2-33e9488495dd)
![mobile-3](https://github.com/romantech/tic-tac-toe/assets/8604840/43bf3611-58cf-45f0-8f25-b42fc00cc126)

#### Match History

![mobile-4](https://github.com/romantech/tic-tac-toe/assets/8604840/2293292b-c699-43f7-b381-01ca68f828d2)
![mobile-4](https://github.com/romantech/tic-tac-toe/assets/8604840/03a30cfe-b1d6-4195-abfb-296b11ca741e)

> Source of game sound effects from [Neave Interactive](https://neave.com)
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<!-- 파비콘 -->
<link href="/favicon.png" rel="icon" sizes="512x512" type="image/png" />
<!-- iOS 에서 웹앱을 홈화면에 추가할 때 사용하는 아이콘 -->
<link href="/icon-maskable-512.png" rel="apple-touch-icon" sizes="512x512" type="image/png" />
<link href="/icon-512.png" rel="apple-touch-icon" sizes="512x512" type="image/png" />

<link href="/manifest.json" rel="manifest" />
<title>Tic-Tac-Toe</title>
Expand Down
Binary file modified public/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/icon-maskable-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
"type": "image/png",
"purpose": "any"
}
],
"start_url": "/",
Expand Down
1 change: 1 addition & 0 deletions src/assets/broom.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { default as GitHubSvg } from '@/assets/github.svg?react';
export { default as HomeSvg } from '@/assets/home.svg?react';
export { default as SpearOnSvg } from '@/assets/speaker-on.svg?react';
export { default as SpearOffSvg } from '@/assets/speaker-off.svg?react';
export { default as Broom } from '@/assets/broom.svg?react';
40 changes: 33 additions & 7 deletions src/components/common/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,47 @@ import { ComponentProps } from 'react';

import { clsx } from 'clsx';

type ButtonVariant = 'solid' | 'outline';

interface ButtonProps extends ComponentProps<'button'> {
variant?: ButtonVariant;
}

const getVariantClasses = (variant: ButtonVariant, disabled: boolean = false) => {
const classes = [];

if (disabled) {
classes.push('bg-slate-600/30 text-slate-500 cursor-not-allowed');
if (variant === 'outline') classes.push('border border-slate-500');
return classes.join(' ');
}

switch (variant) {
case 'solid':
classes.push('bg-slate-600/80 hover:bg-slate-600');
break;
case 'outline':
classes.push('border border-slate-500 hover:bg-slate-700/80');
break;
default:
throw new Error(`Invalid variant: ${variant}`);
}

return classes.join(' ');
};

export default function Button({
children,
disabled,
className,
variant = 'solid',
...buttonProps
}: ComponentProps<'button'>) {
}: ButtonProps) {
return (
<button
className={clsx(
'rounded-lg border-2 p-2 font-semibold transition-all hover:bg-slate-700',
{
'bg-slate-700 border-slate-500 text-slate-500 cursor-not-allowed': disabled,
'border-slate-200': !disabled,
},
className,
['rounded-md px-3 py-2 font-medium shadow-md transition-all duration-300', className],
getVariantClasses(variant, disabled),
)}
type="button"
disabled={disabled}
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/empty.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default function Empty({
}: PropsWithChildren<EmptyProps>) {
if (hidden) return null;
return (
<h1 className={clsx('text-5xl font-bold capitalize', className)} {...headingProps}>
<h1 className={clsx('text-4xl font-extrabold', className)} {...headingProps}>
{children}
</h1>
);
Expand Down
5 changes: 2 additions & 3 deletions src/components/common/icon-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,15 @@ export default function IconButton({
onClick,
}: PropsWithChildren<IconButtonProps>) {
if (!isValidElement(children)) {
console.error('children must be a valid react element');
return null;
throw new Error('children must be a valid react element');
}

return (
<button
type="button"
onClick={onClick}
className={clsx(
'rounded-full bg-slate-700 p-2 text-slate-200 transition-all duration-300 hover:bg-slate-600',
'rounded-full bg-slate-700 p-2 text-slate-200 shadow transition-all duration-300 hover:bg-slate-600/80',
className,
)}
>
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default function Layout({ children, className }: PropsWithChildren<Layout
<Box className={clsx('flex min-h-screen flex-col bg-slate-800 text-slate-200', className)}>
<header
className={clsx(
'sticky top-0 z-10 flex min-h-16 items-center justify-center gap-3 border-b border-slate-700/50 bg-slate-700/20 px-4 backdrop-blur',
'sticky top-0 z-10 flex min-h-16 items-center justify-center gap-3 bg-slate-700/10 px-4 shadow backdrop-blur',
className,
)}
>
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function Toggle({ className, size = 'md', ...inputProps }: Toggle
<input type="checkbox" className="peer sr-only" {...inputProps} />
<div
className={clsx(
'peer relative rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-1/2 after:-translate-y-1/2 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[""] peer-checked:bg-yellow-500 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:ring-4 peer-focus:ring-yellow-300 rtl:peer-checked:after:-translate-x-full dark:border-gray-600 dark:bg-slate-700 dark:peer-focus:ring-slate-500',
'peer relative rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-1/2 after:-translate-y-1/2 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[""] peer-checked:bg-cyan-500 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:ring-4 peer-focus:ring-yellow-300 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 rtl:peer-checked:after:-translate-x-full dark:border-gray-600 dark:bg-slate-500 dark:peer-focus:ring-slate-500',
sizeClasses[size],
)}
></div>
Expand Down
6 changes: 3 additions & 3 deletions src/components/forms/first-player-checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ export default function FirstPlayerCheckbox({ name, player, className }: FirstPl
<label
htmlFor={`firstPlayer-${player}`}
className={clsx(
'inline-block cursor-pointer rounded bg-gradient-to-r from-amber-900 to-amber-600 px-2 text-sm font-medium capitalize leading-7 text-slate-200 transition-all duration-300',
{ 'opacity-30 hover:opacity-60': firstPlayer !== player },
{ 'hover:opacity-90': firstPlayer === player },
'inline-block w-14 cursor-pointer rounded-md border border-slate-500 text-center capitalize leading-7 shadow-md transition-all duration-300',
{ 'text-slate-500 hover:bg-slate-700/50': firstPlayer !== player },
{ 'font-medium bg-slate-600/70 hover:bg-slate-600/85': firstPlayer === player },
)}
>
first
Expand Down
6 changes: 3 additions & 3 deletions src/components/forms/game-mode-radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ export default function GameModeRadio() {
<label
key={mode}
className={clsx(
'grid w-full cursor-pointer place-content-center gap-2 rounded bg-secondary bg-gradient-to-r from-amber-900 to-amber-600 p-2 font-semibold text-slate-200 transition-all duration-300',
'grid w-full cursor-pointer place-content-center gap-2 rounded-md border border-slate-500 p-2 shadow-md transition-all duration-300',
{
'opacity-30 hover:opacity-60': gameMode !== mode,
'hover:opacity-90': gameMode === mode,
'text-slate-500 hover:bg-slate-700/50': gameMode !== mode,
'font-medium bg-slate-600/70 hover:bg-slate-600/85': gameMode === mode,
},
)}
>
Expand Down
4 changes: 2 additions & 2 deletions src/components/forms/player-configs-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const PlayerLabel = ({ player, className, gameMode }: PlayerLabelProps) => {
<Box
as="h3"
className={clsx(
'min-w-[84px] whitespace-nowrap bg-slate-700 text-center font-medium leading-[46px]',
'min-w-[84px] whitespace-nowrap bg-gradient-to-r from-slate-700 to-slate-800 text-center font-medium leading-[46px]',
className,
)}
>
Expand All @@ -39,7 +39,7 @@ const PlayerConfig = ({ player, className, gameMode }: PlayerConfigProps) => {
)}
>
<PlayerLabel player={player} gameMode={gameMode} />
<div className="flex w-full items-center gap-3 px-3">
<div className="flex w-full items-center gap-2 px-2 text-sm">
<MarkTextInput name={`playerConfigs.${player}.mark`} />
<ColorPicker name={`playerConfigs.${player}.color`} />
<FirstPlayerCheckbox name={'firstPlayer'} player={player} />
Expand Down
4 changes: 2 additions & 2 deletions src/components/game/board-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ export default function BoardInfo({ createdAt, winner, className }: BoardInfoPro
return (
<header
className={clsx(
'flex h-10 items-center gap-2 px-2 text-sm capitalize',
'flex h-10 items-center gap-2 px-2 capitalize',
'rounded-t-xl bg-gradient-to-r from-slate-800 to-slate-600',
'border-x-2 border-t-2 border-slate-700',
className,
)}
>
<div className="flex w-5/12 items-center justify-center gap-2 font-semibold">
<div className="flex w-[35%] items-center justify-center gap-2">
<span>winner</span>
<div className="grid size-6 place-content-center rounded-full bg-slate-800 capitalize">
{winner ?? TIE_SYMBOL}
Expand Down
34 changes: 9 additions & 25 deletions src/components/game/board.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,7 @@
import { clsx } from 'clsx';

import { Square } from '@/components';
import { BoardType, TBoard, Winner } from '@/lib';

const getPlayModeSquareBorderStyle = (totalSquares: number, index: number) => {
const size = Math.sqrt(totalSquares);
const rightBorder = 'border-r-4';
const bottomBorder = 'border-b-4';

const classes: string[] = ['border-slate-300'];

const isLastSquareInRow = (index + 1) % size === 0;
if (!isLastSquareInRow) classes.push(rightBorder);

const isLastRowSquare = index >= totalSquares - size;
if (!isLastRowSquare) classes.push(bottomBorder);

return classes;
};
import { BoardType, getPlayModeSquareClasses, TBoard, Winner } from '@/lib';

interface BoardProps {
board: TBoard;
Expand All @@ -40,30 +24,30 @@ export default function Board({
const isViewMode = type === BoardType.View;

const boardClasses = clsx(
'grid aspect-square w-full max-w-md select-none overflow-hidden font-semibold',
[isViewMode && 'border-l-2 border-t-2 border-slate-700', className],
['grid aspect-square w-full max-w-md select-none overflow-hidden font-bold', className],
{ 'border-l-2 border-t-2 border-slate-700': isViewMode },
);

return (
<div className={boardClasses}>
{board.map(({ color, mark, sequence }, i) => {
const isHighlightIdx = winner.indices?.includes(i);
const squareClass = clsx(
isViewMode && 'border-b-2 border-r-2 border-slate-700',
!isViewMode && getPlayModeSquareBorderStyle(board.length, i),
);
const squareClasses = clsx({
'border-b-2 border-r-2 border-slate-700': isViewMode,
[getPlayModeSquareClasses(board.length, i)]: !isViewMode,
});

return (
<Square
key={i}
className={squareClass}
className={squareClasses}
highlight={isHighlightIdx}
dim={hasWinner && !isHighlightIdx}
size={Math.sqrt(board.length)}
color={color}
mark={mark}
onClick={() => handleClick?.(i)}
disabled={type !== BoardType.Play}
disabled={isViewMode}
sequence={sequence}
hideSequence={hideSequence || !sequence}
/>
Expand Down
10 changes: 3 additions & 7 deletions src/components/game/game-controller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,15 @@ export default function GameController({

return (
<div className={clsx('flex max-h-14 gap-3', className)}>
<section className="flex gap-3">
<section className="flex items-center gap-3">
<Button onClick={() => changeScreen(ScreenType.Settings)}>Config</Button>
<Button disabled={!controlStates.reset} onClick={handlers.reset}>
Reset
</Button>
</section>
<Divider className="my-3" />
<section className="flex gap-3">
<Button
disabled={!controlStates.undo}
onClick={handlers.undo}
className="min-w-[88px] capitalize"
>
<section className="flex items-center gap-2">
<Button disabled={!controlStates.undo} onClick={handlers.undo} className="capitalize">
{undoButtonText}
</Button>
<UndoStatus
Expand Down
10 changes: 6 additions & 4 deletions src/components/game/order-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@ import { ToggleProps } from '@/components/common/toggle';

interface OrderToggleProps extends ToggleProps {
className?: string;
disabled?: boolean;
}

export default function OrderToggle({ className, ...inputProps }: OrderToggleProps) {
export default function OrderToggle({ className, disabled, ...inputProps }: OrderToggleProps) {
return (
<Box
className={clsx(
'flex items-center gap-3 rounded-lg border-2 border-slate-200 px-3',
'flex items-center gap-3 rounded-md px-3 shadow-md',
{ 'bg-slate-600/30 text-slate-500': disabled, 'bg-slate-600/80': !disabled },
className,
)}
>
<span className="font-semibold capitalize">show order</span>
<Toggle className="ml-auto" {...inputProps} />
<span className="font-medium capitalize">show order</span>
<Toggle className="ml-auto" disabled={disabled} {...inputProps} />
</Box>
);
}
5 changes: 2 additions & 3 deletions src/components/game/square.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ComponentProps } from 'react';

import { clsx } from 'clsx';

import { BoardSize, getSequenceTextClasses, NBSP, TMark, TSequence } from '@/lib';
import { BoardSize, getSequenceTextClasses, TMark, TSequence } from '@/lib';

interface SquareProps extends ComponentProps<'button'> {
mark: TMark;
Expand Down Expand Up @@ -55,8 +55,7 @@ export default function Square({
className={buttonClasses}
{...buttonProps}
>
{/* layout shift 방지를 위해 논브레이크 스페이스를 기본값으로 지정 */}
<span className={markClasses}>{mark ?? NBSP}</span>
<span className={markClasses}>{mark}</span>
<span className={sequenceClasses}>{sequence}</span>
</button>
);
Expand Down
6 changes: 3 additions & 3 deletions src/components/game/undo-status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ interface StatusProps {

const Status = ({ mark, count, className }: StatusProps) => {
return (
<div className={clsx('flex items-center justify-center gap-1', className)}>
<p className="min-w-5 text-center">{mark}</p>
<p className="w-[68px]">{`Count : ${count}`}</p>
<div className={clsx('flex items-center justify-center gap-1 truncate', className)}>
<span className="inline-block min-w-5 text-center">{mark}</span>
<span className="inline-block min-w-16">{`Count ${count}`}</span>
</div>
);
};
3 changes: 2 additions & 1 deletion src/hooks/use-game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,10 @@ export const useGame = ({
}, [board, isSinglePlay, onBoardClick, size, winCondition]);

const hasMark = sequence.current.length > 0;
const isDraw = sequence.current.length === board.length;
const hasWinner = Boolean(winner.current.identifier);

const hasUndoCount = undoControls.getUndoCountBy(currentPlayer.current) > 0;
const isDraw = sequence.current.length === board.length && !hasWinner;

const enabledReset = hasMark || isUndoUsed;
const enableUndo = !hasWinner && hasMark && hasUndoCount && !isDraw;
Expand Down
Loading

0 comments on commit 026f594

Please sign in to comment.