Skip to content
Merged
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
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="/rooms/:roomCode" element={<RoomPage />} />
<Route path="*" element={<NotFoundPage />} />
</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