Skip to content

Commit

Permalink
feat: Impement Game match room (#69)
Browse files Browse the repository at this point in the history
* refactor: Extract chat message type

* refactor: Restructure route configuration

- Move routes/index.tsx to routes.tsx for simpler structure
- Update route paths with more semantic naming convention

* feat: Add React Router v7 future flags for partial feature migration

- Enabled flag in RouterProvider for smoother transitions
- Disabled incompatible flags to maintain compatibility with react-router-dom v6.28.0

* design: Change Canvas UI design

* design: Improve canvas resolution and aspect ratio handling

- Add fixed canvas resolution (1280x720)
- Wrap canvas in relative container for better positioning
- Change aspect ratio to 4:3 for better drawing space

* feat: Implement responsive game room layout and page (GameRoom)

- Add three-column layout for desktop view (users/canvas/chat)
- Implement mobile-first responsive design with breakpoints(sm, md, lg, xl)
- Add scrollable areas for user list and chat messages
- Adjust QuizTitle and timer positioning
- Improve overall component spacing and alignments

* feat: Update responsive breakpoints from sm to lg for mobile-first layouts

- Adjusted breakpoints in all UI components (QuizTitle, CanvasUI, InkGauge etc.)
- Modified responsive styles to trigger at lg breakpoint for better mobile experience
- Updated layout transitions and component sizes for smoother mobile-to-desktop behavior
- Ensured consistent breakpoint usage across components

* design: Adjust GameRoom container for consistent layout across screen sizes

- Set fixed min-height to prevent layout shift on smaller screens
- Remove overflow-scroll to maintain layout integrity
- Update viewport height calculations for different breakpoints
- Add responsive padding and border radius adjustments

* design: Adjust GameLayout height

* design: Update QuizTitle component with consistent styling and timer positioning

* Merge branch 'develop' into feature/#53
  • Loading branch information
rhino-ty authored Nov 14, 2024
1 parent 8a2eb50 commit f86e8c7
Show file tree
Hide file tree
Showing 14 changed files with 260 additions and 141 deletions.
30 changes: 8 additions & 22 deletions client/src/components/canvas/CanvasUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,6 @@ import {
import { CanvasEventHandlers, DrawingMode } from '@/types/canvas.types';
import { cn } from '@/utils/cn';

const canvasContainerVariants = cva(
'bg-white relative flex flex-col sm:rounded-lg sm:border-4 border-violet-500 sm:shadow-xl',
{
variants: {
size: {
default: 'w-full max-w-3xl',
fullWidth: 'w-full',
},
},
defaultVariants: {
size: 'default',
},
},
);

const toolbarVariants = cva('flex items-center justify-center gap-3 border-violet-950 bg-eastbay-400 p-2', {
variants: {
position: {
Expand Down Expand Up @@ -93,7 +78,7 @@ interface ColorButton {
onClick: () => void;
}

interface CanvasProps extends HTMLAttributes<HTMLDivElement>, VariantProps<typeof canvasContainerVariants> {
interface CanvasProps extends HTMLAttributes<HTMLDivElement> {
canvasRef: RefObject<HTMLCanvasElement>;
isDrawable: boolean;
colors: ColorButton[];
Expand Down Expand Up @@ -155,11 +140,11 @@ const Canvas = forwardRef<HTMLDivElement, CanvasProps>(
aria-label={isDrawable ? '그림판' : '그림 보기'}
{...canvasEvents}
/>
</div>
<div
className={cn('absolute right-1', toolbarPosition === 'floating' ? 'top-1' : 'bottom-[12%] sm:bottom-[10%]')}
>
<InkGauge remainingPixels={inkRemaining} maxPixels={maxPixels} />
{isDrawable && (
<div className={cn('absolute bottom-1 right-1')}>
<InkGauge remainingPixels={inkRemaining} maxPixels={maxPixels} />
</div>
)}
</div>

{isDrawable && colors.length > 0 && (
Expand Down Expand Up @@ -256,7 +241,8 @@ Canvas.displayName = 'Canvas';

export {
Canvas,
canvasContainerVariants,
type CanvasProps,
type DrawingMode,
toolbarVariants,
colorButtonVariants,
controlButtonVariants,
Expand Down
1 change: 0 additions & 1 deletion client/src/components/canvas/GameCanvasExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ const GameCanvas = ({ role, maxPixels = 100000 }: GameCanvasProps) => {
return (
<Canvas
canvasRef={canvasRef}
className="min-w-[280px]"
isDrawable={isDrawable}
colors={isDrawable ? COLORS : []}
// toolbarPosition="floating"
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/canvas/InkGauge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const InkGauge = ({ remainingPixels, maxPixels, className }: InkGaugeProps) => {
const ratio = useMemo(() => Math.max(0, Math.min(1, remainingPixels / maxPixels)), [remainingPixels, maxPixels]);

return (
<div className={cn('relative h-12 w-4 sm:h-24 sm:w-8', className)}>
<div className={cn('relative h-12 w-4 sm:h-14 sm:w-5 xl:h-16 xl:w-6 2xl:h-20 2xl:w-8', className)}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 28 82"
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/chat/ChatBubbleUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const ChatBubble = ({ className, variant, content, nickname, ...props }: ChatBub
className={cn('flex', isOtherUser ? 'flex-col items-start gap-0.5' : 'justify-end')}
>
{isOtherUser && (
<span className="text-xs text-eastbay-50" aria-hidden="true">
<span className="text-sm text-stroke-sm" aria-hidden="true">
{nickname}
</span>
)}
Expand Down
26 changes: 8 additions & 18 deletions client/src/components/chat/Chatting.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,24 @@
import { HTMLAttributes } from 'react';
import { Input } from '../ui/Input';
import { ChatBubble } from './ChatBubbleUI';
import { Message } from '@/types/chat.types';
import { cn } from '@/utils/cn';

interface Message {
nickname: string;
content: string;
isOthers: boolean;
id: number;
}

export interface ChattingProps extends HTMLAttributes<HTMLDivElement> {
messages: Message[];
}

const Chatting = ({ messages, className, ...props }: ChattingProps) => {
return (
<section className={cn('flex w-full flex-col items-center justify-center', className)} {...props}>
<p className="mb-7 text-xl text-eastbay-50">
<section className={cn('flex h-full w-full flex-col gap-2 overflow-y-scroll', className)} {...props}>
<p className="mb-7 text-center text-xl text-eastbay-50">
여기에다가 답하고
<br /> 채팅할 수 있습니다.
</p>
<div className="flex w-full flex-col gap-2">
{messages.map((message: Message) => {
const { nickname, content, id } = message;
if (message.isOthers) return <ChatBubble content={content} nickname={nickname} key={id} />;
else return <ChatBubble content={content} variant="secondary" key={id} />;
})}
</div>
<Input placeholder="답을 입력해주세요." className="mt-1" />
{messages.map((message: Message) => {
const { nickname, content, id } = message;
if (message.isOthers) return <ChatBubble content={content} nickname={nickname} key={id} />;
else return <ChatBubble content={content} variant="secondary" key={id} />;
})}
</section>
);
};
Expand Down
14 changes: 7 additions & 7 deletions client/src/components/ui/QuizTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,31 @@ const QuizTitle = ({ className, currentRound, totalRound, remainingTime, title,
<>
<div
className={cn(
'relative flex w-full items-center justify-center border-violet-950 bg-violet-500 p-1.5 text-stroke-sm sm:rounded-lg sm:border-2 sm:p-3.5',
'relative flex w-full max-w-screen-sm items-center justify-center border-violet-950 bg-violet-500 p-1.5 sm:rounded-lg sm:border-2 sm:p-2.5',
className,
)}
{...props}
>
{/* 라운드 정보 */}
<p className="absolute left-4 text-xs sm:left-3.5 sm:text-xl">
<p className="absolute left-2 text-xs text-stroke-sm sm:text-sm md:text-base lg:left-3.5 xl:text-lg">
<span>{currentRound}</span>
<span> of </span>
<span>{totalRound}</span>
</p>

{/* 제시어 */}
<h2 className="text-2xl sm:text-4xl">{title}</h2>
<h2 className="text-xl text-stroke-md lg:text-2xl xl:text-3xl">{title}</h2>

{/* 타이머 */}
<div className="absolute -right-0 -top-4 w-[4.25rem] sm:-right-10 sm:-top-11 sm:w-[8.75rem]">
<div className="absolute -right-0 -top-[1.125rem] h-16 w-16 sm:-top-[1.3rem] sm:right-0 sm:h-20 sm:w-20 lg:-right-7 lg:-top-5 lg:w-20 xl:-right-[1.85rem] xl:-top-7 xl:w-24 2xl:-right-8 2xl:-top-9 2xl:w-28">
<div className="relative">
{remainingTime > 10 ? (
<img src={Timer} alt="타이머" className="h-full w-full" />
<img src={Timer} alt="타이머" className="object-fill" width={128} height={128} />
) : (
<img src={flashingTimer} alt="타이머" className="h-full w-full" />
<img src={flashingTimer} alt="타이머" className="object-fill" width={128} height={128} />
)}

<span className="absolute inset-0 top-1/2 ml-[0.1rem] flex -translate-y-1/3 items-center justify-center text-base text-stroke-md sm:ml-1 sm:text-[2rem]">
<span className="absolute inset-0 top-1/2 flex -translate-y-1/3 items-center justify-center text-base text-stroke-md sm:text-xl lg:ml-1 lg:text-2xl xl:text-3xl 2xl:text-[2rem]">
{remainingTime}
</span>
</div>
Expand Down
77 changes: 47 additions & 30 deletions client/src/components/ui/UserInfoCard.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { cva, type VariantProps } from 'class-variance-authority';
import clsx from 'clsx';
import profilePlaceholder from '@/assets/profile-placeholder.png';
import { ReadyStatus, UserRank } from '@/types/userInfo.types';
import { cn } from '@/utils/cn';
import getCrownImage from '@/utils/getCrownImage';

const userInfoCardVariants = cva('flex duration-200 gap-2 sm:transition-colors', {
const userInfoCardVariants = cva('flex duration-200 gap-2 lg:transition-colors', {
variants: {
status: {
// 대기 상태 - 기본 상태
notReady: 'bg-transparent sm:bg-eastbay-400 text-white',
notReady: 'bg-transparent lg:bg-eastbay-400 text-white',
// 준비 완료 상태
ready: 'bg-transparent sm:bg-violet-500 text-white',
ready: 'bg-transparent lg:bg-violet-500 text-white',
// 게임 진행 중 상태
gaming: 'bg-transparent sm:bg-eastbay-400 text-white',
gaming: 'bg-transparent lg:bg-eastbay-400 text-white',
},
},
defaultVariants: {
Expand Down Expand Up @@ -96,21 +95,23 @@ const UserInfoCard = ({
<div
className={cn(
userInfoCardVariants({ status }),
'sm:h-[5.5rem] sm:w-[18.25rem] sm:items-center sm:justify-between sm:rounded-lg sm:border-2 sm:border-violet-950 sm:p-3',
'h-20 w-12 items-center justify-between p-2',
// 모바일
'h-20 w-20 items-center',
// 데스트톱
'lg:aspect-[3/1] lg:w-full lg:items-center lg:justify-between lg:rounded-lg lg:border-2 lg:border-violet-950 lg:p-1 xl:p-3',
className,
)}
>
<div className="flex flex-col items-center justify-center sm:flex-row sm:gap-3">
<div className="flex flex-col items-center justify-center lg:flex-row lg:gap-3">
{/* 프로필 이미지 섹션 */}
<div className="relative">
<div className="flex h-12 w-12 items-center justify-center overflow-hidden rounded-full border-2 border-violet-950 bg-white/20 sm:h-14 sm:w-14 sm:rounded-xl">
<div className="relative mb-1 lg:m-0">
<div className="flex h-12 w-12 items-center justify-center overflow-hidden rounded-full border-2 border-violet-950 bg-white/20 lg:h-14 lg:w-14 lg:rounded-xl">
<img src={profileImage || profilePlaceholder} alt="사용자 프로필" />
{/* 모바일 상태 오버레이 */}
{status !== 'gaming' ? (
<div
className={clsx(
'absolute inset-0 flex items-center justify-center rounded-full transition-all duration-300 sm:hidden',
className={cn(
'absolute inset-0 flex items-center justify-center rounded-full transition-all duration-300 lg:hidden',
{
'bg-violet-500/80 opacity-100': status === 'ready',
'bg-transparent opacity-0': status !== 'ready',
Expand All @@ -120,7 +121,7 @@ const UserInfoCard = ({
{status === 'ready' && <span className="text-xs text-stroke-sm">준비</span>}
</div>
) : (
<div className="absolute inset-0 flex items-center justify-center rounded-full bg-black/50 sm:hidden">
<div className="absolute inset-0 flex items-center justify-center rounded-full bg-black/50 lg:hidden">
<span className="text-xl font-bold text-white text-stroke-sm">{score}</span>
</div>
)}
Expand All @@ -129,36 +130,52 @@ const UserInfoCard = ({
<img
src={crownImage}
alt={`${rank}등 사용자`}
className="absolute -right-1 -top-3 h-7 w-auto rotate-[30deg] sm:-right-5 sm:-top-7 sm:h-12"
className="absolute -right-1 -top-3 h-7 w-auto rotate-[30deg] lg:-right-5 lg:-top-7 lg:h-12"
/>
)}
</div>
</div>

{/* 사용자 정보 섹션 */}
<div className="flex -translate-y-1 flex-col items-center sm:translate-y-0 sm:items-start">
<div className="h-3 text-stroke-sm sm:h-auto">
<span className="text-xs text-chartreuseyellow-400 sm:text-2xl">{username}</span>
<div className="relative flex -translate-y-1 flex-col text-center lg:translate-y-0 lg:items-start">
<div className="relative h-3 text-stroke-sm lg:h-auto">
<div
title={username}
className={cn(
// 기본 & 모바일 스타일
'w-20 truncate text-xs text-chartreuseyellow-400',
// 데스크톱
'lg:w-auto lg:max-w-28 lg:text-base',
'xl:max-w-[9.5rem] xl:text-lg',
'2xl:max-w-52 2xl:text-xl',
)}
>
{username}
</div>
</div>
<div className="h-3 text-stroke-sm sm:h-auto">
<span className="text-[0.625rem] text-gray-50 sm:text-base">{role}</span>
<div className="h-3 text-stroke-sm lg:h-auto">
<div
title={role}
className={cn(
// 기본 & 모바일 스타일
'w-20 truncate text-[0.625rem] text-gray-50',
// 데스크톱
'lg:w-auto lg:max-w-28 lg:text-sm',
'xl:max-w-[9.5rem] xl:text-base',
'2xl:max-w-52',
)}
>
{role}
</div>
</div>
</div>
</div>

{/* 데스크탑 점수/상태 표시 섹션 */}
<div className="hidden items-center gap-2 sm:flex">
<div className="hidden items-center gap-2 lg:flex">
{score !== undefined && (
<div
className={clsx(
'flex h-10 items-center justify-center rounded-lg border-2 border-violet-950 bg-halfbaked-200',
{
'px-3': score < 10,
'px-1.5': score >= 10,
},
)}
>
<div className="translate-x-[0.05rem] text-2xl leading-5 text-eastbay-950">{score}</div>
<div className="flex aspect-square h-8 items-center justify-center rounded-lg border-2 border-violet-950 bg-halfbaked-200 xl:h-10">
<div className="translate-x-[0.05rem] leading-5 text-eastbay-950 lg:text-lg xl:text-2xl">{score}</div>
</div>
)}

Expand Down
12 changes: 5 additions & 7 deletions client/src/layouts/GameLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@ import { Logo } from '@/components/ui/Logo';

const GameLayout = () => {
return (
<div className="flex min-h-screen flex-col bg-gradient-to-b from-violet-950 via-violet-800 to-fuchsia-800">
<div className="flex min-h-screen flex-col justify-start bg-gradient-to-b from-violet-950 via-violet-800 to-fuchsia-800 lg:py-5">
{/* 상단 헤더 */}
<header className="flex justify-center p-4">
<Logo variant="side" className="" />
<header className="flex items-center justify-center">
<Logo variant="side" />
</header>

<main className="flex flex-1 items-center justify-center px-4">
<div className="aspect-[16/9] w-full max-w-[1200px] overflow-hidden rounded-lg bg-eastbay-900/80">
<Outlet />
</div>
<main className="mx-auto">
<Outlet />
</main>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion client/src/layouts/RootLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Button } from '@/components/ui/Button';

const RootLayout = () => {
return (
<div className="relative min-h-screen bg-violet-950 bg-fixed antialiased">
<div className="relative min-h-screen min-w-80 bg-violet-950 bg-fixed antialiased">
{/* 상단 네비게이션 영역: Help 아이콘 컴포넌트 */}
<nav className="fixed right-4 top-4 z-30 animate-bounce xs:right-8 xs:top-8">
<Button variant="transperent" size="icon">
Expand Down
4 changes: 2 additions & 2 deletions client/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { createRoot } from 'react-dom/client';
import '@/index.css';
import { RouterProvider } from 'react-router-dom';
import App from '@/App.tsx';
import { router } from '@/routes/index.tsx';
import { router } from '@/routes';

createRoot(document.getElementById('root')!).render(
<StrictMode>
<App>
<RouterProvider router={router} />
<RouterProvider router={router} future={{ v7_startTransition: true }} />
</App>
</StrictMode>,
);
Loading

0 comments on commit f86e8c7

Please sign in to comment.