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
25 changes: 25 additions & 0 deletions app/[locale]/(auth)/forgot-password/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use server'

import { createClient } from '@/lib/supabase/server'
import { headers } from 'next/headers'
import { getBaseUrl } from '@/lib/utils/url'

async function getOrigin(): Promise<string> {
const headerStore = await headers()
return headerStore.get('origin') || getBaseUrl()
}

export async function sendResetEmail({ email }: { email: string }) {
const supabase = await createClient()
const origin = await getOrigin()

const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${origin}/auth/callback?next=/projects`,
})

if (error) {
return { error: error.message }
}

return { error: null }
}
108 changes: 108 additions & 0 deletions app/[locale]/(auth)/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
'use client'

import { useState, type FormEvent } from 'react'
import { Link } from '@/i18n/navigation'
import { useTranslations } from 'next-intl'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Card, CardContent, CardDescription, CardFooter, CardHeader } from '@/components/ui/card'
import { sendResetEmail } from './actions'

export default function ForgotPasswordPage() {
const t = useTranslations('auth.forgotPassword')
const tCommon = useTranslations('common')

const [email, setEmail] = useState('')
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState(false)
const [isLoading, setIsLoading] = useState(false)

async function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault()
if (isLoading) return

setError(null)
setSuccess(false)
setIsLoading(true)

const result = await sendResetEmail({ email: email.trim() })
if (result?.error) {
setError(result.error)
} else {
setSuccess(true)
}

setIsLoading(false)
}

return (
<div className="flex min-h-screen">
{/* Left: Branding panel */}
<div className="relative hidden w-1/2 overflow-hidden bg-brand lg:block">
<div className="absolute inset-0 grid-pattern" />
<div className="absolute inset-0 gradient-mesh-dark" />
<div className="noise absolute inset-0" />
<div className="relative z-10 flex h-full flex-col justify-between p-12">
<Link href="/" className="animate-fade-in font-display text-xl font-bold tracking-tight text-white">
{tCommon('brandName')}
</Link>
<div className="animate-fade-in-up delay-1">
<h2 className="font-display text-4xl font-bold leading-[0.95] tracking-headline text-white xl:text-5xl">
{t('title')}
</h2>
<p className="mt-6 max-w-sm text-sm leading-relaxed text-white/50">
{t('description')}
</p>
</div>
</div>
</div>

{/* Right: Form */}
<div className="flex flex-1 items-center justify-center px-6 py-12">
<Card className="w-full max-w-md border-0 shadow-none lg:border lg:shadow-sm">
<CardHeader className="lg:hidden">
<h2 className="font-display text-2xl text-text-primary">{t('title')}</h2>
<CardDescription>{t('description')}</CardDescription>
</CardHeader>
<CardContent>
{success ? (
<div className="space-y-4 rounded-md border border-success/20 bg-success/5 p-4">
<p className="text-sm font-medium text-success">{t('successTitle')}</p>
<p className="text-sm text-text-secondary">{t('successDescription', { email })}</p>
<Link href="/login" className="inline-flex text-sm font-medium text-accent hover:underline">
{t('backToLogin')}
</Link>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email" required>{t('email')}</Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
placeholder="[email protected]"
/>
</div>
{error && (
<p className="text-sm text-error" role="alert" aria-live="polite">{error}</p>
)}
<Button type="submit" className="w-full" isLoading={isLoading}>
{t('submit')}
</Button>
</form>
)}
</CardContent>
<CardFooter className="justify-center">
<Link href="/login" className="text-sm text-text-secondary hover:underline">
{t('backToLogin')}
</Link>
</CardFooter>
</Card>
</div>
</div>
)
}
57 changes: 30 additions & 27 deletions app/[locale]/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
import { useState } from 'react'
import { Eye, EyeOff } from 'lucide-react'
import { Link } from '@/i18n/navigation'
import { useTranslations } from 'next-intl'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { signIn, signInWithOAuth } from './actions'

export default function LoginPage() {
const t = useTranslations('auth.login')
const tAuth = useTranslations('auth')
const tCommon = useTranslations('common')

const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [showPassword, setShowPassword] = useState(false)
Expand Down Expand Up @@ -49,27 +54,27 @@ export default function LoginPage() {
<div className="absolute inset-0 gradient-mesh-dark" />
<div className="noise absolute inset-0" />
<div className="relative z-10 flex h-full flex-col justify-between p-12">
<Link href="/" className="animate-fade-in font-display text-xl font-bold text-white tracking-tight">
BrandKit
<Link href="/" className="animate-fade-in font-display text-xl font-bold tracking-tight text-white">
{tCommon('brandName')}
</Link>
<div className="animate-fade-in-up delay-1">
<h2 className="font-display text-4xl font-bold leading-[0.95] tracking-headline text-white xl:text-5xl">
브랜드 에셋을
{t('branding.headline')}
<br />
<span className="text-white/40">자동으로.</span>
<span className="text-white/40">{t('branding.headlineSub')}</span>
</h2>
<p className="mt-6 max-w-sm text-sm leading-relaxed text-white/50">
Brand Profile에 스타일을 저장하면, AI가 모든 플랫폼의 에셋을 생성합니다.
{t('branding.description')}
</p>
</div>
<div className="animate-fade-in-up delay-3 flex gap-10">
<div>
<div className="font-display text-2xl font-bold text-white tracking-tight">12+</div>
<div className="mt-1 font-mono text-[10px] tracking-[0.2em] text-white/30 uppercase">Assets</div>
<div className="mt-1 font-mono text-[10px] tracking-[0.2em] text-white/30 uppercase">{t('branding.stats.assets')}</div>
</div>
<div>
<div className="font-display text-2xl font-bold text-white tracking-tight">&lt;60s</div>
<div className="mt-1 font-mono text-[10px] tracking-[0.2em] text-white/30 uppercase">Speed</div>
<div className="mt-1 font-mono text-[10px] tracking-[0.2em] text-white/30 uppercase">{t('branding.stats.speed')}</div>
</div>
</div>
</div>
Expand All @@ -80,42 +85,40 @@ export default function LoginPage() {
<Card className="w-full max-w-md border-0 shadow-none lg:border lg:shadow-sm">
<CardHeader>
<div className="mb-2 lg:hidden">
<Link href="/" className="font-display text-lg font-bold text-text-primary">
BrandKit
</Link>
<Link href="/" className="font-display text-lg font-bold text-text-primary">{tCommon('brandName')}</Link>
</div>
<CardTitle className="font-display text-2xl">Log in</CardTitle>
<CardDescription>계정에 로그인하여 계속하세요</CardDescription>
<CardTitle className="font-display text-2xl">{t('title')}</CardTitle>
<CardDescription>{t('subtitle')}</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email" required>Email</Label>
<Label htmlFor="email" required>{t('email')}</Label>
<Input
id="email"
type="email"
placeholder="[email protected]"
placeholder={t('emailPlaceholder')}
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="password" required>Password</Label>
<Label htmlFor="password" required>{t('password')}</Label>
<Link
href="/forgot-password"
className="text-xs text-text-tertiary transition-colors hover:text-accent"
tabIndex={-1}
>
Forgot password?
{t('forgotPassword')}
</Link>
</div>
<div className="relative">
<Input
id="password"
type={showPassword ? 'text' : 'password'}
placeholder="••••••••"
placeholder={t('passwordPlaceholder')}
value={password}
onChange={(e) => setPassword(e.target.value)}
required
Expand All @@ -126,7 +129,7 @@ export default function LoginPage() {
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-text-tertiary transition-colors hover:text-text-secondary"
tabIndex={-1}
aria-label={showPassword ? '비밀번호 숨기기' : '비밀번호 표시'}
aria-label={showPassword ? t('hidePassword') : t('showPassword')}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
Expand All @@ -136,7 +139,7 @@ export default function LoginPage() {
<p className="text-sm text-error" role="alert" aria-live="polite">{error}</p>
)}
<Button type="submit" className="w-full" isLoading={isLoading}>
Log in
{t('submit')}
</Button>
</form>

Expand All @@ -145,7 +148,7 @@ export default function LoginPage() {
<span className="w-full border-t border-border" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-surface px-2 font-mono text-text-tertiary">or</span>
<span className="bg-surface px-2 font-mono text-text-tertiary">{tCommon('or')}</span>
</div>
</div>

Expand All @@ -159,10 +162,10 @@ export default function LoginPage() {
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="currentColor" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path fill="currentColor" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path fill="currentColor" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.84.81-.62z"/>
<path fill="currentColor" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
Google
{tAuth('oauth.google')}
</Button>
<Button
variant="outline"
Expand All @@ -171,17 +174,17 @@ export default function LoginPage() {
disabled={!!oauthLoading || isLoading}
>
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
<path fill="currentColor" d="M12 .297c6.63 0 12 5.373 12 12 0 5.303-3.438 9.8-8.205 11.385-.6.113-.82-.258-.82-.577 0-.285-.01-1.04-.015-2.04 0 0 3.338.726 4.042-1.417 0 0 .546-1.387 1.333-1.756-4.148-.47-8.516-2.074-8.516-9.225 0-1.31.468-2.381 1.236-3.221-.124-.303-.536-1.524.117-3.176 0 0 .985-.315 3.225 1.23a11.2 11.2 0 0 1 2.98-.4c.997 0 1.998.132 2.923.39 2.239-1.545 3.223-1.23 3.223-1.23.653 1.652.241 2.873.118 3.176.77.84 1.235 1.911 1.235 3.221 0 7.164-4.373 8.75-8.532 9.21.548.472 1.037 1.403 1.037 2.825 0 2.04-.02 3.684-.02 4.184 0 .321-.218.695-.826.577C4.42 23.796 1 19.301 1 14C1 7.373 5.373 1 12 5.373z"/>
</svg>
GitHub
{tAuth('oauth.github')}
</Button>
</div>
</CardContent>
<CardFooter className="justify-center">
<p className="text-sm text-text-secondary">
Don&apos;t have an account?{' '}
<Link href="/signup" className="font-medium text-accent hover:underline">
Sign up
{t('noAccount')}
<Link href="/signup" className="ml-1 font-medium text-accent hover:underline">
{t('signupLink')}
</Link>
</p>
</CardFooter>
Expand Down
Loading