Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
9 changes: 5 additions & 4 deletions apps/client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import RoomPage from "@/pages/room/RoomPage";
import NotFoundPage from "@/pages/not-found/NotFoundPage";
import HomePage from "@/pages/home/HomePage";

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Navigate to="/room/prototype" replace />} />
<Route path="/room/prototype" element={<RoomPage />} />
<Route path="*" element={<NotFoundPage />} />
<Route path="/" element={<HomePage />} />
<Route path="/room/prototype" element={<RoomPage />} />
<Route path="*" element={<NotFoundPage />} />
Comment on lines 10 to 12
Copy link
Collaborator

Choose a reason for hiding this comment

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

여기 path가 이제 /room/:roomCode 이런식으로 되어야 하지 않나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

그러네요 이 부분 수정하겠습니다

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

클라이언트에서 room id 를 사용하고 있는 부분이 많은데 store 부분은 머지한 코드에서 다시 수정하는 게 나을 것 같습니다
Room path 는 /rooms/:roomCode 로 수정했습니다 (관례적으로 rooms 를 많이 사용해서 복수형을 사용했습니다)

</Routes>
</BrowserRouter>
);
Expand Down
15 changes: 15 additions & 0 deletions apps/client/src/pages/home/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Hero } from "./sections/Hero";
import { ActionCards } from "./sections/ActionCards";
import { FeatureCards } from "./sections/FeatureCards";

export default function MainPage() {
return (
<div className="min-h-screen flex flex-col items-center justify-center p-4 relative overflow-hidden">
<div className="relative z-10 w-full max-w-4xl">
<Hero />
<ActionCards />
<FeatureCards />
</div>
</div>
);
}
49 changes: 49 additions & 0 deletions apps/client/src/pages/home/cards/ActionCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { LucideIcon } from "lucide-react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/shared/ui/card";
import { cardColorSchemes } from "../constants/card-color-schemes";

interface ActionCardProps {
icon: LucideIcon;
title: string;
description: string;
colorKey: string;
children?: React.ReactNode;
}

export function ActionCard({
icon: Icon,
title,
description,
colorKey,
children,
}: ActionCardProps) {
const colors = cardColorSchemes[colorKey];

return (
<Card
className={`bg-gray-50 border-gray-200 shadow-md ${colors.hoverBorderColor} transition-colors duration-200`}
>
<CardHeader className="pb-4 flex flex-col items-center text-center gap-3">
<div
className={`${colors.iconBg} border ${colors.borderColor} w-12 h-12 rounded-full flex items-center justify-center`}
>
<Icon className={`h-6 w-6 ${colors.iconColor}`} />
</div>

<CardTitle className="text-xl text-gray-800">{title}</CardTitle>
<CardDescription className="text-gray-600 text-sm">
{description}
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col items-center justify-center py-6">
{children}
</CardContent>
</Card>
);
}
36 changes: 36 additions & 0 deletions apps/client/src/pages/home/cards/FeatureCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { LucideIcon } from "lucide-react";
import { Card, CardDescription, CardHeader, CardTitle } from "@/shared/ui/card";
import { cardColorSchemes } from "../constants/card-color-schemes";

interface FeatureCardProps {
icon: LucideIcon;
title: string;
description: string;
colorKey: string;
}

export function FeatureCard({
icon: Icon,
title,
description,
colorKey,
}: FeatureCardProps) {
const colors = cardColorSchemes[colorKey];

return (
<Card className={`border-transparent shadow-none ${colors.cardBg}`}>
<CardHeader className="flex flex-col items-center text-center gap-2">
<div
className={`${colors.iconBg} border ${colors.borderColor} w-10 h-10 rounded-full flex items-center justify-center`}
>
<Icon className={`h-5 w-5 ${colors.iconColor}`} />
</div>

<CardTitle className="text-base text-gray-800">{title}</CardTitle>
<CardDescription className="text-gray-600 text-xs font-mono">
{description}
</CardDescription>
</CardHeader>
</Card>
);
}
130 changes: 130 additions & 0 deletions apps/client/src/pages/home/components/RoomCodeInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { useRef } from "react";
import type { KeyboardEvent, ClipboardEvent } from "react";

export const ROOM_CODE_LENGTH = 6;

interface RoomCodeInputProps {
value: string[];
onChange: (code: string[]) => void;
hasError?: boolean;
onSubmit?: () => void;
length?: number;
}

export function RoomCodeInput({
value,
onChange,
hasError = false,
onSubmit,
length = ROOM_CODE_LENGTH,
}: RoomCodeInputProps) {
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);

const handleChange = (index: number, inputValue: string) => {
if (inputValue !== "" && !isValidRoomCodeChar(inputValue)) return;

const newCode = [...value];
newCode[index] = inputValue;
onChange(newCode);

// Move to next field after typing a character
if (inputValue !== "" && index < length - 1) {
inputRefs.current[index + 1]?.focus();
}
};

const handleKeyDown = (index: number, e: KeyboardEvent<HTMLInputElement>) => {
const inputValue = (e.target as HTMLInputElement).value;

// Backspace handling
// If current field is empty, move to previous field and clear it
if (e.key === "Backspace") {
if (inputValue === "" && index > 0) {
e.preventDefault();
const newCode = [...value];
newCode[index - 1] = "";
onChange(newCode);
inputRefs.current[index - 1]?.focus();
}
}

// Left arrow
if (e.key === "ArrowLeft" && index > 0) {
e.preventDefault();
inputRefs.current[index - 1]?.focus();
}

// Right arrow
if (e.key === "ArrowRight" && index < length - 1) {
e.preventDefault();
inputRefs.current[index + 1]?.focus();
}

// Enter key to submit
if (e.key === "Enter") {
const roomCode = value.join("");
if (roomCode.length === length && onSubmit) {
onSubmit();
}
}
};

// Paste handling
const handlePaste = (index: number, e: ClipboardEvent<HTMLInputElement>) => {
e.preventDefault();

const pastedData = e.clipboardData
.getData("text")
.toUpperCase()
.slice(0, length);

// Filter only alphanumeric characters
const validChars = pastedData
.split("")
.filter((char) => isValidRoomCodeChar(char));

if (validChars.length === 0) return;

const newCode = [...value];

// Fill from current index
validChars.forEach((char, i) => {
const targetIndex = index + i;
if (targetIndex < length) {
newCode[targetIndex] = char;
}
});

onChange(newCode);

// Set new focus
const nextIndex = Math.min(index + validChars.length, length - 1);
inputRefs.current[nextIndex]?.focus();
};

return (
<div className="flex gap-1 sm:gap-2 justify-center">
{value.map((digit, index) => (
<input
key={index}
ref={(el) => {
inputRefs.current[index] = el;
}}
type="text"
maxLength={1}
value={digit}
onChange={(e) => handleChange(index, e.target.value)}
onKeyDown={(e) => handleKeyDown(index, e)}
onPaste={(e) => handlePaste(index, e)}
className={`w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 text-center text-base sm:text-xl md:text-2xl font-semibold font-mono border-2 ${
hasError ? "border-red-500" : "border-gray-400"
} focus:border-gray-900 focus:outline-none transition-colors uppercase caret-transparent`}
/>
))}
</div>
);
}

const isValidRoomCodeChar = (char: string): boolean => {
return /^[a-zA-Z0-9]$/.test(char);
};
61 changes: 61 additions & 0 deletions apps/client/src/pages/home/constants/card-color-schemes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
export interface CardColorScheme {
cardBg: string;
iconBg: string;
borderColor: string;
iconColor: string;
hoverCardBg: string;
hoverBorderColor: string;
}

const blue: CardColorScheme = {
cardBg: "bg-blue-50",
iconBg: "bg-blue-100",
borderColor: "border-blue-200",
iconColor: "text-blue-500",
hoverCardBg: "hover:bg-blue-50",
hoverBorderColor: "hover:border-blue-400",
};

const green: CardColorScheme = {
cardBg: "bg-green-50",
iconBg: "bg-green-100",
borderColor: "border-green-200",
iconColor: "text-green-500",
hoverCardBg: "hover:bg-green-50",
hoverBorderColor: "hover:border-green-400",
};

const purple: CardColorScheme = {
cardBg: "bg-purple-50",
iconBg: "bg-purple-100",
borderColor: "border-purple-200",
iconColor: "text-purple-500",
hoverCardBg: "hover:bg-purple-50",
hoverBorderColor: "hover:border-purple-400",
};

const orange: CardColorScheme = {
cardBg: "bg-orange-50",
iconBg: "bg-orange-100",
borderColor: "border-orange-200",
iconColor: "text-orange-500",
hoverCardBg: "hover:bg-orange-50",
hoverBorderColor: "hover:border-orange-400",
};

const red: CardColorScheme = {
cardBg: "bg-red-50",
iconBg: "bg-red-100",
borderColor: "border-red-200",
iconColor: "text-red-500",
hoverCardBg: "hover:bg-red-50",
hoverBorderColor: "hover:border-red-400",
};

export const cardColorSchemes: Record<string, CardColorScheme> = {
blue,
green,
purple,
orange,
red,
} as const;
Loading