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
15 changes: 10 additions & 5 deletions frontend/src/pages/Home/ui/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
// PR 2 (feature/home-layout) ๋‹จ๊ณ„์˜ HomePage.tsx
import { SiteNav } from '@/widgets/site-nav'
import { HomeHero } from '@/widgets/home-hero'
import { HomeServices } from '@/widgets/home-services'
import { HomeQuote } from '@/widgets/home-quote'
import { HomeFaq } from '@/widgets/home-faq'
import { HomeCta } from '@/widgets/home-cta'
import { SiteFooter } from '@/widgets/site-footer'
// ์•„์ง PR 3์—์„œ ๋งŒ๋“ค ์˜ˆ์ •์ด๋ฏ€๋กœ ์ฃผ์„ ์ฒ˜๋ฆฌ!
// import { HomeHero } from '@/widgets/home-hero'

export default function HomePage() {
return (
<div className="min-h-svh bg-bg text-fg">
{/* ๊ธ€๋กœ๋ฒŒ ๋ ˆ์ด์•„์›ƒ์€ ์ถ”ํ›„ ๋ถ„๋ฆฌ ์˜ˆ์ •*/}
<SiteNav />
<main>
{/* TODO: ๋‹ค์Œ PR์—์„œ ์œ„์ ฏ๋“ค ์ถ”๊ฐ€ ์˜ˆ์ • */}
<HomeHero />
<HomeServices />
<HomeQuote />
<HomeFaq />
<HomeCta />
</main>
<SiteFooter />
</div>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/shared/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { useTypewriter } from './useTypewriter'
export type { UseTypewriterOptions } from './useTypewriter'
61 changes: 61 additions & 0 deletions frontend/src/shared/hooks/useTypewriter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useEffect, useState } from 'react'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ํ…์ŠคํŠธ ํ•œ๊ธ€์ž์”ฉ ๋‚˜ํƒ€๋‚˜๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋งŒ๋“ค๋•Œ ์“ฐ๋Š” ์ฝ”๋“œ์ธ๊ฐ€์š” ์ข‹๋„ค์š”


export type UseTypewriterOptions = {
startDelayMs?: number
stepMs?: number
respectReducedMotion?: boolean
}

export function useTypewriter(
text: string,
options: UseTypewriterOptions = {},
) {
const {
startDelayMs = 0,
stepMs = 100,
respectReducedMotion = true,
} = options

const [typed, setTyped] = useState('')

useEffect(() => {
const reduce =
respectReducedMotion &&
typeof window !== 'undefined' &&
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches

if (reduce) {
setTyped(text)
return
}

setTyped('')
let rafId = 0
let startTime: number | null = null

const tick = (now: number) => {
if (startTime === null) startTime = now
const elapsed = now - startTime - startDelayMs
if (elapsed < 0) {
rafId = window.requestAnimationFrame(tick)
return
}
const next = Math.min(
Math.floor(elapsed / stepMs) + 1,
text.length,
)
setTyped(text.slice(0, next))
if (next < text.length) {
rafId = window.requestAnimationFrame(tick)
}
}

rafId = window.requestAnimationFrame(tick)

return () => {
window.cancelAnimationFrame(rafId)
}
}, [text, startDelayMs, stepMs, respectReducedMotion])

return { typed, done: typed.length >= text.length }
}
1 change: 1 addition & 0 deletions frontend/src/widgets/home-cta/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { HomeCta } from './ui/HomeCta'
51 changes: 51 additions & 0 deletions frontend/src/widgets/home-cta/ui/HomeCta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export function HomeCta() {
return (
<section id="cta" className="bg-bg">
<div className="mx-auto max-w-content px-6 lg:px-12 pb-24 lg:pb-32">
<div className="relative overflow-hidden rounded-2xl">
<img
src="/last-section-get-started.png"
alt=""
aria-hidden
loading="lazy"
decoding="async"
className="absolute inset-0 w-full h-full object-cover"
/>
<div
aria-hidden
className="absolute inset-0"
style={{
background:
'linear-gradient(180deg, rgba(20,26,17,0.55) 0%, rgba(20,26,17,0.35) 50%, rgba(20,26,17,0.65) 100%)',
}}
/>

<div className="relative min-h-[360px] lg:min-h-[440px] flex flex-col items-center justify-center text-center px-6 py-20 lg:py-28">
<h2
className="font-heading font-extrabold uppercase text-white leading-[0.95] tracking-tight"
style={{ fontSize: 'clamp(40px, 5.5vw, 72px)' }}
>
Ready to level up?
</h2>
<p className="mt-5 text-white/80 text-rich max-w-xl">
์ง€๊ธˆ GitHub ๊ณ„์ •๋งŒ ์—ฐ๊ฒฐํ•˜๋ฉด, 30์ดˆ ์•ˆ์— ์ฒซ ๋ชจ์˜๋ฉด์ ‘์ด ์‹œ์ž‘๋ฉ๋‹ˆ๋‹ค.
</p>

<a
href="#login"
className="mt-10 inline-flex items-center gap-2 pl-5 pr-2 py-2.5 rounded-pill bg-[#e6dfd4] text-sage-900 text-button hover:bg-white transition-colors duration-fast"
>
Get Started
<span
aria-hidden
className="inline-flex items-center justify-center w-6 h-6 rounded-pill bg-sage-900 text-white text-[11px]"
>
โ†’
</span>
</a>
</div>
</div>
</div>
</section>
)
}
1 change: 1 addition & 0 deletions frontend/src/widgets/home-faq/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { HomeFaq } from './ui/HomeFaq'
86 changes: 86 additions & 0 deletions frontend/src/widgets/home-faq/ui/HomeFaq.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const faqs = [
{
q: 'Stack Up์€ ๋ฌด๋ฃŒ ํฌ๋ ˆ๋”ง์„ ์ œ๊ณตํ•˜๋‚˜์š”?',
a: '์›” 2ํšŒ์˜ ๋ฌด๋ฃŒ ๋ฉด์ ‘ ์„ธ์…˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ํ•™๊ต ์ข…ํ•ฉ์„ค๊ณ„ ํ”„๋กœ์ ํŠธ ๊ธฐ์ค€์ด๋ผ ๊ฒฐ์ œยท๊ตฌ๋… ๊ธฐ๋Šฅ์€ ํฌํ•จ๋˜์ง€ ์•Š์•„์š”.',
},
{
q: '์–ด๋–ค ํ˜•์‹์˜ ์ด๋ ฅ์„œ๋ฅผ ์ง€์›ํ•˜๋‚˜์š”?',
a: 'Phase 1 ๊ธฐ์ค€ PDF๋งŒ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. HWPยทDOCX๋Š” PDF๋กœ ๋ณ€ํ™˜ ํ›„ ์—…๋กœ๋“œํ•ด ์ฃผ์„ธ์š”.',
},
{
q: 'GitHub ์™ธ ๋‹ค๋ฅธ ๋กœ๊ทธ์ธ์€ ๊ฐ€๋Šฅํ•œ๊ฐ€์š”?',
a: '๋ ˆํฌ ๋ถ„์„์ด ํ•ต์‹ฌ ๊ธฐ๋Šฅ์ด๋ผ, Phase 1์—์„œ๋Š” GitHub OAuth๋งŒ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.',
},
{
q: '๊ผฌ๋ฆฌ์งˆ๋ฌธ์€ ์–ผ๋งˆ๋‚˜ ๋นจ๋ฆฌ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‚˜์š”?',
a: 'ํ‰๊ท  3์ดˆ ์ด๋‚ด ์‘๋‹ต์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. Flash ๋ชจ๋ธ๊ณผ ์‚ฌ์ „ ๊ตฌ์ถ• RAG ์ธ๋ฑ์Šค๋กœ ์ง€์—ฐ์„ ์ตœ์†Œํ™”ํ•ด์š”.',
},
{
q: '์Œ์„ฑยท๋น„์–ธ์–ด ๋ถ„์„์€ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋‚˜์š”?',
a: 'WebRTC๋กœ ๋งˆ์ดํฌยท์›น์บ  ์ŠคํŠธ๋ฆผ์„ ๋ฐ›์•„ ๋ง ์†๋„(WPM)ยท๊ฐ„ํˆฌ์–ดยท์‹œ์„ ยท์ž์„ธ๋ฅผ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. ๊ถŒํ•œ์„ ๊ฑฐ๋ถ€ํ•˜๋ฉด ํ…์ŠคํŠธ ์ž…๋ ฅ์œผ๋กœ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ์–ด์š”.',
},
]

export function HomeFaq() {
return (
<section id="faq" className="bg-bg">
<div className="mx-auto max-w-content px-6 lg:px-12 pt-8 pb-24 lg:pt-12 lg:pb-32">
<p className="text-button font-mono uppercase tracking-[0.22em] text-fg-muted">
FAQ
</p>

<h2
className="mt-4 font-heading font-extrabold uppercase text-sage-900 leading-[0.95] tracking-tight"
style={{ fontSize: 'clamp(40px, 5vw, 64px)' }}
>
Questions, answered.
</h2>

<div className="mt-12 h-px bg-border" />

<div className="mt-10 grid gap-12 lg:grid-cols-12">
<div className="lg:col-span-4">
<p className="text-rich text-fg-strong/85 leading-relaxed max-w-sm">
์—ฌ๊ธฐ์— ์—†๋Š” ๊ถ๊ธˆ์ฆ์€ Contact ํŽ˜์ด์ง€์—์„œ ์ด์–ด์„œ ๋‹ตํ•ด๋“œ๋ ค์š”.
</p>
<a
href="#cta"
className="mt-6 inline-flex items-center gap-2 pl-5 pr-2 py-2.5 rounded-pill bg-sage-800 text-white text-button hover:bg-sage-900 transition-colors duration-fast"
>
Contact
<span
aria-hidden
className="inline-flex items-center justify-center w-6 h-6 rounded-pill bg-white/15"
>
โ†’
</span>
</a>
</div>

<ul className="lg:col-span-8">
{faqs.map((f) => (
<li key={f.q} className="border-b border-border first:border-t">
<details className="group">
<summary className="flex items-center justify-between gap-6 py-6 cursor-pointer list-none select-none">
<span className="text-fg-strong text-[20px] lg:text-[22px] font-medium leading-snug">
{f.q}
</span>
<span
aria-hidden
className="shrink-0 w-7 h-7 grid place-items-center text-fg-strong text-xl transition-transform duration-normal ease-standard group-open:rotate-45"
>
+
</span>
</summary>
<div className="pb-6 pr-12 text-body text-fg-strong/70 leading-relaxed max-w-3xl">
{f.a}
</div>
</details>
</li>
))}
</ul>
</div>
</div>
</section>
)
}
1 change: 1 addition & 0 deletions frontend/src/widgets/home-hero/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { HomeHero } from './ui/HomeHero'
38 changes: 38 additions & 0 deletions frontend/src/widgets/home-hero/ui/HomeHero.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Laptop } from './Laptop'
import { ScreenContent } from './ScreenContent'

export function HomeHero() {
return (
<section
id="top"
className="relative overflow-hidden"
style={{
background:
'radial-gradient(120% 80% at 50% 0%, #f1ece4 0%, #e9e8e7 60%, #e9e8e7 100%)',
}}
>
<div
aria-hidden
className="pointer-events-none absolute inset-0 opacity-[0.18] mix-blend-multiply"
style={{
background:
'radial-gradient(60% 50% at 18% 30%, rgba(31,39,27,0.5) 0%, transparent 60%),' +
'radial-gradient(40% 35% at 82% 20%, rgba(31,39,27,0.35) 0%, transparent 60%)',
}}
/>

<div className="relative mx-auto max-w-content px-6 lg:px-12 pt-10 pb-12 lg:pt-14 lg:pb-16 flex flex-col items-center">
<Laptop>
<ScreenContent />
</Laptop>

<p className="anim-hero-rise mt-6 lg:mt-8 text-button font-mono uppercase tracking-[0.22em] text-fg-muted [animation-delay:2.2s]">
<span className="inline-flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-success" aria-hidden />
Phase 1 ยท MVP ยท 2026
</span>
</p>
</div>
</section>
)
}
59 changes: 59 additions & 0 deletions frontend/src/widgets/home-hero/ui/Laptop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { ReactNode } from 'react'

export function Laptop({ children }: { children: ReactNode }) {
return (
<div
className="anim-laptop-rise relative w-full [animation-delay:0.05s]"
style={{
maxWidth: 'min(820px, calc((100svh - 240px) * 1.6))',
filter: 'drop-shadow(0 30px 50px rgba(31,39,27,0.22))',
}}
>
{/* Lid */}
<div
className="relative rounded-[20px] p-[9px]"
style={{
background: 'linear-gradient(180deg, #d8d6d2 0%, #c9c6c2 100%)',
boxShadow:
'inset 0 1px 0 rgba(255,255,255,0.6), 0 1px 2px rgba(31,39,27,0.15)',
}}
>
{/* Bezel */}
<div
className="relative rounded-[12px] p-[12px]"
style={{ background: 'linear-gradient(180deg, #1a1a1a 0%, #0c0c0c 100%)' }}
>
{/* Camera dot */}
<div className="absolute top-[5px] left-1/2 -translate-x-1/2 w-1.5 h-1.5 rounded-full bg-zinc-700" />

{/* Screen */}
<div
className="relative aspect-[16/10] rounded-[5px] overflow-hidden flex items-center justify-center px-6"
style={{
background:
'radial-gradient(120% 100% at 50% 0%, #f5f1ea 0%, #e8e3da 60%, #ddd6cc 100%)',
}}
>
{children}
</div>
</div>
</div>

{/* Base / hinge */}
<div className="relative mx-auto" style={{ width: '108%' }}>
<div
className="h-[12px] -mt-[2px] mx-auto rounded-b-[10px]"
style={{
background: 'linear-gradient(180deg, #c2bfba 0%, #a8a5a0 100%)',
boxShadow:
'inset 0 1px 0 rgba(255,255,255,0.4), 0 8px 12px -6px rgba(31,39,27,0.25)',
}}
/>
<div
className="absolute top-0 left-1/2 -translate-x-1/2 h-[4px] w-[18%] rounded-b-md"
style={{ background: '#8e8b86' }}
/>
</div>
</div>
)
}
60 changes: 60 additions & 0 deletions frontend/src/widgets/home-hero/ui/ScreenContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useTypewriter } from '@/shared/hooks'

const TYPED_TEXT = 'Stack Up'

export function ScreenContent() {
const { typed, done } = useTypewriter(TYPED_TEXT, {
startDelayMs: 850,
stepMs: 130,
})

return (
<div className="relative text-center w-full">
<p className="anim-hero-rise text-caption sm:text-button font-mono uppercase tracking-[0.28em] text-sage-700 [animation-delay:0.5s]">
IT Interview Solution
</p>

<h1
className="anim-hero-rise mt-3 sm:mt-4 text-sage-900 leading-[0.95] inline-block [animation-delay:0.8s]"
style={{
fontFamily: 'var(--font-script)',
fontSize: 'clamp(56px, 8.4vw, 116px)',
fontWeight: 700,
letterSpacing: '-0.01em',
transform: 'rotate(-2deg)',
minHeight: '1em',
}}
aria-label={TYPED_TEXT}
>
<span aria-hidden>{typed}</span>
<span
aria-hidden
className="inline-block align-baseline ml-1"
style={{
width: '3px',
height: '0.78em',
background: 'var(--color-sage-800)',
verticalAlign: '-0.06em',
animation: done ? 'caret-blink 1s steps(1, end) infinite' : 'none',
opacity: done ? undefined : 1,
}}
/>
</h1>

<div className="anim-hero-rise mt-4 sm:mt-6 [animation-delay:2.0s]">
<a
href="#cta"
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-pill bg-sage-700 text-white text-button hover:bg-sage-800 transition-colors duration-fast"
>
Get Started
<span
aria-hidden
className="inline-flex items-center justify-center w-5 h-5 rounded-pill bg-white/15"
>
โ†’
</span>
</a>
</div>
</div>
)
}
1 change: 1 addition & 0 deletions frontend/src/widgets/home-quote/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { HomeQuote } from './ui/HomeQuote'
Loading