@@ -24,6 +24,22 @@ const loginDemoMessagesKo = [
2424 { speaker : "팀 채팅" , text : "리뷰 기준 문서에 반영할게요." , tone : "#CFF8FF" } ,
2525] ;
2626
27+ const loginWelcomeMessagesKo = [
28+ "오늘의 PR 산책은 제가 먼저 다녀왔어요.\n위험한 변경은 줄 세워두고, 문서는 따뜻하게 데워둘게요." ,
29+ "코드 바다에 오신 걸 환영합니다.\n리뷰 파도는 CodeDock이 잔잔하게 만들어둘게요." ,
30+ "커피는 없지만 컨텍스트는 준비됐어요.\nPR, 문서, 팀 대화까지 제가 한 화면에 착 붙여둘게요." ,
31+ "버그는 숨바꼭질을 좋아하죠.\n오늘은 CodeDock이 술래가 되어 먼저 찾아볼게요." ,
32+ "리뷰 대기열이 길어도 괜찮아요.\n제가 중요한 파일부터 콕 집어서 길을 열어둘게요." ,
33+ ] ;
34+
35+ const loginWelcomeMessagesEn = [
36+ "I already took today's PRs for a walk.\nI'll line up the risky changes and keep the docs warm." ,
37+ "Welcome aboard the code harbor.\nCodeDock will keep the review waves pleasantly calm." ,
38+ "No coffee here, but the context is ready.\nI'll keep PRs, docs, and team chat neatly docked." ,
39+ "Bugs love hide-and-seek.\nToday, CodeDock will count first and find them faster." ,
40+ "A long review queue is fine.\nI'll point out the important files and clear the runway." ,
41+ ] ;
42+
2743const loginTabDemosKo = [
2844 {
2945 icon : Sparkles ,
@@ -76,7 +92,9 @@ export function LoginPage() {
7692 const [ showPassword , setShowPassword ] = useState ( false ) ;
7793 const [ rememberMe , setRememberMe ] = useState ( true ) ;
7894 const [ message , setMessage ] = useState ( "" ) ;
95+ const [ isLoginButtonHovering , setIsLoginButtonHovering ] = useState ( false ) ;
7996 const [ activeDemoIndex , setActiveDemoIndex ] = useState ( 0 ) ;
97+ const [ welcomeMessageIndex ] = useState ( ( ) => Math . floor ( Math . random ( ) * loginWelcomeMessagesKo . length ) ) ;
8098 const trimmedEmail = email . trim ( ) ;
8199 const emailAtIndex = trimmedEmail . indexOf ( "@" ) ;
82100 const shouldRevealPassword = emailAtIndex > 0 && trimmedEmail . slice ( emailAtIndex + 1 ) . trim ( ) . length > 0 ;
@@ -133,14 +151,15 @@ export function LoginPage() {
133151 } ,
134152 ]
135153 : loginTabDemosKo ;
154+ const loginWelcomeMessage = isEnglish ? loginWelcomeMessagesEn [ welcomeMessageIndex ] : loginWelcomeMessagesKo [ welcomeMessageIndex ] ;
136155 const loginCopy = {
137156 access : isEnglish ? "CodeDock Access" : "CodeDock 접속" ,
138157 login : isEnglish ? "Login" : "로그인" ,
139158 accountTitle : isEnglish ? "Access your account" : "계정에 접속하기" ,
140- emailPrompt : isEnglish ? "Tell me the email to sign in with. " : "접속할 이메일을 알려주세요." ,
159+ emailPrompt : isEnglish ? "Which email do you sign in with? " : "로그인할 이메일을 알려주세요." ,
141160 emailLabel : isEnglish ? "Email" : "이메일" ,
142161 emailPlaceholder : "name@company.com" ,
143- passwordPrompt : isEnglish ? "Great. Enter your password too ." : "좋아요. 비밀번호도 입력해주세요." ,
162+ passwordPrompt : isEnglish ? "Got it! Now enter your password." : "이메일 확인했어요! 비밀번호를 입력해주세요." ,
144163 passwordLabel : isEnglish ? "Password" : "비밀번호" ,
145164 passwordPlaceholder : isEnglish ? "Enter password" : "비밀번호 입력" ,
146165 remember : isEnglish ? "Keep me signed in" : "로그인 상태 유지" ,
@@ -154,11 +173,8 @@ export function LoginPage() {
154173 hidePassword : isEnglish ? "Hide password" : "비밀번호 숨기기" ,
155174 miniBrand : "CodeDock" ,
156175 liveWorkspace : isEnglish ? "LIVE WORKSPACE" : "실시간 작업 공간" ,
157- welcomeEyebrow : isEnglish ? "Welcome aboard" : "환영합니다" ,
158- welcomeTitle : isEnglish ? "Welcome to the joyful world of coding." : "즐거운 코딩의 세계로 오신 걸 환영합니다." ,
159- welcomeBody : isEnglish
160- ? "CodeDock keeps reviews, documents, and team conversations flowing while you build."
161- : "리뷰와 문서, 팀 대화는 CodeDock이 정리할게요.\n오늘도 편하게 개발을 시작하세요." ,
176+ welcomeTitle : "Hello CodeDock!" ,
177+ welcomeBody : loginWelcomeMessage ,
162178 } ;
163179 const activeDemo = loginTabDemos [ activeDemoIndex ] ;
164180 const demoCount = loginTabDemos . length ;
@@ -248,11 +264,8 @@ export function LoginPage() {
248264 exit = { { opacity : 0 , y : - 8 , height : 0 , filter : "blur(8px)" } }
249265 transition = { { duration : 0.42 , ease : "easeOut" } }
250266 >
251- < p className = "m-0 text-sm font-black tracking-tight" style = { { color : colors . primaryHex } } >
252- { loginCopy . welcomeEyebrow }
253- </ p >
254267 < h2
255- className = "m-0 mt-2 leading-tight tracking-tight"
268+ className = "m-0 leading-tight tracking-tight"
256269 style = { {
257270 color : "var(--white)" ,
258271 fontSize : "clamp(30px, 4.4vw, 54px)" ,
@@ -727,6 +740,10 @@ export function LoginPage() {
727740
728741 < button
729742 type = "submit"
743+ onMouseEnter = { ( ) => setIsLoginButtonHovering ( true ) }
744+ onMouseLeave = { ( ) => setIsLoginButtonHovering ( false ) }
745+ onFocus = { ( ) => setIsLoginButtonHovering ( true ) }
746+ onBlur = { ( ) => setIsLoginButtonHovering ( false ) }
730747 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]"
731748 style = { {
732749 background : `linear-gradient(135deg, ${ colors . primaryHex } , ${ colors . secondary } )` ,
@@ -736,7 +753,7 @@ export function LoginPage() {
736753 fontWeight : 950 ,
737754 } }
738755 >
739- { loginCopy . submit }
756+ < LoginSubmitLabel isHovering = { isLoginButtonHovering } defaultLabel = { loginCopy . submit } />
740757 < ArrowRight size = { 19 } strokeWidth = { 2.5 } />
741758 </ button >
742759 </ motion . div >
@@ -805,6 +822,70 @@ interface LoginChatPromptProps {
805822 typingDelay ?: number ;
806823}
807824
825+ function LoginSubmitLabel ( { isHovering, defaultLabel } : { isHovering : boolean ; defaultLabel : string } ) {
826+ const targetText = "Hello CodeDock!" ;
827+ const [ visibleCount , setVisibleCount ] = useState ( 0 ) ;
828+ const visibleText = targetText . slice ( 0 , visibleCount ) ;
829+
830+ useEffect ( ( ) => {
831+ if ( ! isHovering ) {
832+ setVisibleCount ( 0 ) ;
833+ return ;
834+ }
835+
836+ setVisibleCount ( 0 ) ;
837+ } , [ isHovering ] ) ;
838+
839+ useEffect ( ( ) => {
840+ if ( ! isHovering || visibleCount >= targetText . length ) {
841+ return ;
842+ }
843+
844+ const typingTimer = window . setTimeout ( ( ) => {
845+ setVisibleCount ( ( count ) => Math . min ( targetText . length , count + 1 ) ) ;
846+ } , visibleCount === 0 ? 120 : 38 ) ;
847+
848+ return ( ) => window . clearTimeout ( typingTimer ) ;
849+ } , [ isHovering , targetText . length , visibleCount ] ) ;
850+
851+ return (
852+ < span className = "inline-flex min-w-[150px] items-center justify-center text-center" >
853+ < AnimatePresence mode = "wait" initial = { false } >
854+ { isHovering ? (
855+ < motion . span
856+ key = "login-code-typing"
857+ className = "inline-flex items-center justify-center font-black"
858+ initial = { { opacity : 0 , y : 5 , filter : "blur(4px)" } }
859+ animate = { { opacity : 1 , y : 0 , filter : "blur(0px)" } }
860+ exit = { { opacity : 0 , y : - 5 , filter : "blur(4px)" } }
861+ transition = { { duration : 0.16 , ease : "easeOut" } }
862+ style = { { fontFamily : "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" } }
863+ >
864+ < span > { visibleText } </ span >
865+ < motion . span
866+ className = "ml-1 inline-block h-5 w-1 rounded-full"
867+ style = { { background : "#021014" } }
868+ animate = { { opacity : [ 0 , 1 , 0 ] } }
869+ transition = { { duration : 0.62 , repeat : Infinity , ease : "easeInOut" } }
870+ />
871+ </ motion . span >
872+ ) : (
873+ < motion . span
874+ key = "login-default-label"
875+ className = "inline-block"
876+ initial = { { opacity : 0 , y : 5 } }
877+ animate = { { opacity : 1 , y : 0 } }
878+ exit = { { opacity : 0 , y : - 5 } }
879+ transition = { { duration : 0.16 , ease : "easeOut" } }
880+ >
881+ { defaultLabel }
882+ </ motion . span >
883+ ) }
884+ </ AnimatePresence >
885+ </ span >
886+ ) ;
887+ }
888+
808889function LoginChatPrompt ( { text, delay = 0 , typingDelay = 620 } : LoginChatPromptProps ) {
809890 const { colors } = useTheme ( ) ;
810891 const [ isTyping , setIsTyping ] = useState ( true ) ;
0 commit comments