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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ Thumbs.db
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json

# Git fetch metadata accidentally created in the workspace root
/FETCH_HEAD
5 changes: 2 additions & 3 deletions src/app/components/AuthLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function AuthLayout() {
<header className="relative z-10 mx-auto flex w-[min(1180px,calc(100vw-32px))] items-center justify-between py-5">
<Link
to="/"
className="flex items-center gap-3 no-underline"
className="codedock-brand-link flex items-center gap-3 no-underline"
style={{
color: "var(--white)",
fontSize: "24px",
Expand All @@ -46,8 +46,7 @@ export function AuthLayout() {
aria-label="랜딩으로 돌아가기"
>
<CoffeeLogo
className="h-14 w-14 flex-shrink-0"
style={{ filter: `drop-shadow(0 0 14px ${colors.primary}, 0.3))` }}
className="codedock-header-logo h-14 w-14 flex-shrink-0"
/>
<CodeDockWordmark accentColor={colors.primaryHex} />
</Link>
Expand Down
34 changes: 29 additions & 5 deletions src/app/components/CoffeeLogo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function CoffeeLogo({ className = "", style, alive = false, eyeX, eyeY, m
const isCtaMood = mood === "cta";
const isRiskMood = mood === "risk";
const isSuccessMood = mood === "success";
const mouthPath = isRiskMood ? "M166 202c7-8 17-8 24 0" : "M174 194c5 6 11 6 16 0";
const floatTransition = { duration: 4.2, repeat: Infinity, ease: "easeInOut" } as const;
const tinyTransition = { duration: 2.8, repeat: Infinity, ease: "easeInOut" } as const;
const activityDuration = 11.5;
Expand Down Expand Up @@ -109,6 +110,7 @@ export function CoffeeLogo({ className = "", style, alive = false, eyeX, eyeY, m
style={{ transformBox: "fill-box", transformOrigin: "50% 46%" }}
>
<motion.g
className="codedock-logo__ear codedock-logo__ear--left"
animate={alive ? leftEarAnimation : undefined}
transition={earTransition}
style={{ transformBox: "fill-box", transformOrigin: "42% 100%" }}
Expand All @@ -124,6 +126,7 @@ export function CoffeeLogo({ className = "", style, alive = false, eyeX, eyeY, m
</motion.g>

<motion.g
className="codedock-logo__ear codedock-logo__ear--right"
animate={alive ? rightEarAnimation : undefined}
transition={{ ...earTransition, delay: isRiskMood ? 0.08 : 0.14 }}
style={{ transformBox: "fill-box", transformOrigin: "58% 100%" }}
Expand All @@ -145,8 +148,9 @@ export function CoffeeLogo({ className = "", style, alive = false, eyeX, eyeY, m
strokeWidth="7"
/>

<motion.g style={alive ? { x: eyeX, y: eyeY } : undefined}>
<motion.g className="codedock-logo__eyes" style={alive ? { x: eyeX, y: eyeY } : undefined}>
<motion.g
className="codedock-logo__eye-pair"
animate={alive ? { scaleY: [1, 1, 0.18, 1, 1] } : undefined}
transition={{
duration: isRiskMood ? 2.7 : 4.8,
Expand All @@ -161,14 +165,33 @@ export function CoffeeLogo({ className = "", style, alive = false, eyeX, eyeY, m
</motion.g>
</motion.g>

{isRiskMood && (
<motion.g
className="codedock-logo__brows"
animate={alive ? { y: [0, -1, 0], opacity: [0.82, 1, 0.82] } : undefined}
transition={{ duration: 1.2, repeat: Infinity, ease: "easeInOut" }}
>
<path d="M141 157l23 7" stroke="#7A1420" strokeWidth="6" strokeLinecap="round" />
<path d="M213 157l-23 7" stroke="#7A1420" strokeWidth="6" strokeLinecap="round" />
</motion.g>
)}

<motion.path
d="M174 194c5 6 11 6 16 0"
className="codedock-logo__mouth"
d={mouthPath}
fill="none"
stroke="#0B1628"
stroke={isRiskMood ? "#7A1420" : "#0B1628"}
strokeWidth="6"
strokeLinecap="round"
animate={alive ? { y: [0, isCtaMood ? 1.8 : 1, 0], scaleX: isCtaMood ? [1, 1.28, 1] : [1, 1.04, 1] } : undefined}
transition={isCtaMood ? { duration: 2.15, repeat: Infinity, ease: "easeInOut" } : tinyTransition}
animate={
alive
? {
y: isRiskMood ? [0, -0.8, 0] : [0, isCtaMood ? 1.8 : 1, 0],
scaleX: isRiskMood ? [1, 1.08, 1] : isCtaMood ? [1, 1.28, 1] : [1, 1.04, 1],
}
: undefined
}
transition={isRiskMood ? { duration: 1.15, repeat: Infinity, ease: "easeInOut" } : isCtaMood ? { duration: 2.15, repeat: Infinity, ease: "easeInOut" } : tinyTransition}
style={{ transformBox: "fill-box", transformOrigin: "50% 50%" }}
/>

Expand Down Expand Up @@ -274,6 +297,7 @@ export function CoffeeLogo({ className = "", style, alive = false, eyeX, eyeY, m
</motion.g>

<motion.g
className="codedock-logo__mug"
animate={
alive
? isCtaMood
Expand Down
9 changes: 2 additions & 7 deletions src/app/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,9 @@ import { useTheme } from "../contexts/ThemeContext";

const navItems = [
{ path: "/workspace", label: "Dashboard" },
{ path: "/project", label: "Project" },
{ path: "/prs", label: "PRs" },
{ path: "/issues", label: "Issues" },
{ path: "/chat", label: "Workspace" },
{ path: "/api-spec", label: "API" },
{ path: "/erd", label: "ERD" },
{ path: "/docs", label: "Docs" },
];

const currentUser = {
Expand Down Expand Up @@ -84,7 +80,7 @@ export function Layout() {
<div className="relative mx-auto flex w-[min(1400px,100%)] items-center justify-between gap-4">
<Link
to="/"
className="flex items-center gap-3 no-underline transition-all duration-300"
className="codedock-brand-link flex items-center gap-3 no-underline transition-all duration-300"
style={{
color: "var(--white)",
fontSize: "26px",
Expand All @@ -97,8 +93,7 @@ export function Layout() {
aria-label="CodeDock 랜딩으로 이동"
>
<CoffeeLogo
className="h-16 w-16 flex-shrink-0"
style={{ filter: `drop-shadow(0 0 14px ${colors.primary}, 0.3))` }}
className="codedock-header-logo h-16 w-16 flex-shrink-0"
/>
<CodeDockWordmark accentColor={colors.primaryHex} />
</Link>
Expand Down
5 changes: 2 additions & 3 deletions src/app/components/PublicLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function PublicLayout() {
<div className="mx-auto flex w-[min(1180px,100%)] items-center justify-between gap-3">
<Link
to="/"
className="flex min-w-0 items-center gap-2 no-underline sm:gap-3"
className="codedock-brand-link flex min-w-0 items-center gap-2 no-underline sm:gap-3"
style={{
color: "var(--white)",
fontSize: "26px",
Expand All @@ -64,8 +64,7 @@ export function PublicLayout() {
aria-label="CodeDock 랜딩"
>
<CoffeeLogo
className="h-12 w-12 flex-shrink-0 sm:h-16 sm:w-16"
style={{ filter: `drop-shadow(0 0 14px ${colors.primary}, 0.3))` }}
className="codedock-header-logo h-12 w-12 flex-shrink-0 sm:h-16 sm:w-16"
/>
<CodeDockWordmark accentColor={colors.primaryHex} className="hidden sm:inline-flex" />
</Link>
Expand Down
105 changes: 93 additions & 12 deletions src/app/pages/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@ const loginDemoMessagesKo = [
{ speaker: "팀 채팅", text: "리뷰 기준 문서에 반영할게요.", tone: "#CFF8FF" },
];

const loginWelcomeMessagesKo = [
"오늘의 PR 산책은 제가 먼저 다녀왔어요.\n위험한 변경은 줄 세워두고, 문서는 따뜻하게 데워둘게요.",
"코드 바다에 오신 걸 환영합니다.\n리뷰 파도는 CodeDock이 잔잔하게 만들어둘게요.",
"커피는 없지만 컨텍스트는 준비됐어요.\nPR, 문서, 팀 대화까지 제가 한 화면에 착 붙여둘게요.",
"버그는 숨바꼭질을 좋아하죠.\n오늘은 CodeDock이 술래가 되어 먼저 찾아볼게요.",
"리뷰 대기열이 길어도 괜찮아요.\n제가 중요한 파일부터 콕 집어서 길을 열어둘게요.",
];

const loginWelcomeMessagesEn = [
"I already took today's PRs for a walk.\nI'll line up the risky changes and keep the docs warm.",
"Welcome aboard the code harbor.\nCodeDock will keep the review waves pleasantly calm.",
"No coffee here, but the context is ready.\nI'll keep PRs, docs, and team chat neatly docked.",
"Bugs love hide-and-seek.\nToday, CodeDock will count first and find them faster.",
"A long review queue is fine.\nI'll point out the important files and clear the runway.",
];

const loginTabDemosKo = [
{
icon: Sparkles,
Expand Down Expand Up @@ -76,7 +92,9 @@ export function LoginPage() {
const [showPassword, setShowPassword] = useState(false);
const [rememberMe, setRememberMe] = useState(true);
const [message, setMessage] = useState("");
const [isLoginButtonHovering, setIsLoginButtonHovering] = useState(false);
const [activeDemoIndex, setActiveDemoIndex] = useState(0);
const [welcomeMessageIndex] = useState(() => Math.floor(Math.random() * loginWelcomeMessagesKo.length));
const trimmedEmail = email.trim();
const emailAtIndex = trimmedEmail.indexOf("@");
const shouldRevealPassword = emailAtIndex > 0 && trimmedEmail.slice(emailAtIndex + 1).trim().length > 0;
Expand Down Expand Up @@ -133,14 +151,15 @@ export function LoginPage() {
},
]
: loginTabDemosKo;
const loginWelcomeMessage = isEnglish ? loginWelcomeMessagesEn[welcomeMessageIndex] : loginWelcomeMessagesKo[welcomeMessageIndex];
const loginCopy = {
access: isEnglish ? "CodeDock Access" : "CodeDock 접속",
login: isEnglish ? "Login" : "로그인",
accountTitle: isEnglish ? "Access your account" : "계정에 접속하기",
emailPrompt: isEnglish ? "Tell me the email to sign in with." : "접속할 이메일을 알려주세요.",
emailPrompt: isEnglish ? "Which email do you sign in with?" : "로그인할 이메일을 알려주세요.",
emailLabel: isEnglish ? "Email" : "이메일",
emailPlaceholder: "name@company.com",
passwordPrompt: isEnglish ? "Great. Enter your password too." : "좋아요. 비밀번호도 입력해주세요.",
passwordPrompt: isEnglish ? "Got it! Now enter your password." : "이메일 확인했어요! 비밀번호를 입력해주세요.",
passwordLabel: isEnglish ? "Password" : "비밀번호",
passwordPlaceholder: isEnglish ? "Enter password" : "비밀번호 입력",
remember: isEnglish ? "Keep me signed in" : "로그인 상태 유지",
Expand All @@ -154,11 +173,8 @@ export function LoginPage() {
hidePassword: isEnglish ? "Hide password" : "비밀번호 숨기기",
miniBrand: "CodeDock",
liveWorkspace: isEnglish ? "LIVE WORKSPACE" : "실시간 작업 공간",
welcomeEyebrow: isEnglish ? "Welcome aboard" : "환영합니다",
welcomeTitle: isEnglish ? "Welcome to the joyful world of coding." : "즐거운 코딩의 세계로 오신 걸 환영합니다.",
welcomeBody: isEnglish
? "CodeDock keeps reviews, documents, and team conversations flowing while you build."
: "리뷰와 문서, 팀 대화는 CodeDock이 정리할게요.\n오늘도 편하게 개발을 시작하세요.",
welcomeTitle: "Hello CodeDock!",
welcomeBody: loginWelcomeMessage,
};
const activeDemo = loginTabDemos[activeDemoIndex];
const demoCount = loginTabDemos.length;
Expand Down Expand Up @@ -248,11 +264,8 @@ export function LoginPage() {
exit={{ opacity: 0, y: -8, height: 0, filter: "blur(8px)" }}
transition={{ duration: 0.42, ease: "easeOut" }}
>
<p className="m-0 text-sm font-black tracking-tight" style={{ color: colors.primaryHex }}>
{loginCopy.welcomeEyebrow}
</p>
<h2
className="m-0 mt-2 leading-tight tracking-tight"
className="m-0 leading-tight tracking-tight"
style={{
color: "var(--white)",
fontSize: "clamp(30px, 4.4vw, 54px)",
Expand Down Expand Up @@ -727,6 +740,10 @@ export function LoginPage() {

<button
type="submit"
onMouseEnter={() => setIsLoginButtonHovering(true)}
onMouseLeave={() => setIsLoginButtonHovering(false)}
onFocus={() => setIsLoginButtonHovering(true)}
onBlur={() => setIsLoginButtonHovering(false)}
className="mt-1 flex h-14 w-full items-center justify-center gap-2 rounded-2xl border-0 px-6 tracking-tight transition hover:scale-[1.01]"
style={{
background: `linear-gradient(135deg, ${colors.primaryHex}, ${colors.secondary})`,
Expand All @@ -736,7 +753,7 @@ export function LoginPage() {
fontWeight: 950,
}}
>
{loginCopy.submit}
<LoginSubmitLabel isHovering={isLoginButtonHovering} defaultLabel={loginCopy.submit} />
<ArrowRight size={19} strokeWidth={2.5} />
</button>
</motion.div>
Expand Down Expand Up @@ -805,6 +822,70 @@ interface LoginChatPromptProps {
typingDelay?: number;
}

function LoginSubmitLabel({ isHovering, defaultLabel }: { isHovering: boolean; defaultLabel: string }) {
const targetText = "Hello CodeDock!";
const [visibleCount, setVisibleCount] = useState(0);
const visibleText = targetText.slice(0, visibleCount);

useEffect(() => {
if (!isHovering) {
setVisibleCount(0);
return;
}

setVisibleCount(0);
}, [isHovering]);

useEffect(() => {
if (!isHovering || visibleCount >= targetText.length) {
return;
}

const typingTimer = window.setTimeout(() => {
setVisibleCount((count) => Math.min(targetText.length, count + 1));
}, visibleCount === 0 ? 120 : 38);

return () => window.clearTimeout(typingTimer);
}, [isHovering, targetText.length, visibleCount]);

return (
<span className="inline-flex min-w-[150px] items-center justify-center text-center">
<AnimatePresence mode="wait" initial={false}>
{isHovering ? (
<motion.span
key="login-code-typing"
className="inline-flex items-center justify-center font-black"
initial={{ opacity: 0, y: 5, filter: "blur(4px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
exit={{ opacity: 0, y: -5, filter: "blur(4px)" }}
transition={{ duration: 0.16, ease: "easeOut" }}
style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" }}
>
<span>{visibleText}</span>
<motion.span
className="ml-1 inline-block h-5 w-1 rounded-full"
style={{ background: "#021014" }}
animate={{ opacity: [0, 1, 0] }}
transition={{ duration: 0.62, repeat: Infinity, ease: "easeInOut" }}
/>
</motion.span>
) : (
<motion.span
key="login-default-label"
className="inline-block"
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.16, ease: "easeOut" }}
>
{defaultLabel}
</motion.span>
)}
</AnimatePresence>
</span>
);
}

function LoginChatPrompt({ text, delay = 0, typingDelay = 620 }: LoginChatPromptProps) {
const { colors } = useTheme();
const [isTyping, setIsTyping] = useState(true);
Expand Down
Loading
Loading