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
2 changes: 1 addition & 1 deletion src/app/mypage/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import ClientMyPage from '@/features/mypage/ClientMyPage';
export default function MyPage() {
return (
<div className="min-h-screen to-blue-50 md:p-6">
<div className="max-w-6xl mx-auto space-y-4 md:space-y-6">
<div className="max-w-[1400px] mx-auto space-y-4 md:space-y-6">
<div className="flex justify-between items-end">
<h1 className="title mb-3">마이페이지</h1>
{/* TODO: 계정 정보 뭘 수정할지 확인 */}
Expand Down
21 changes: 4 additions & 17 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,13 @@
import CTASection from '@/features/home/CTASection';
import { HeroSection } from '@/features/home/HeroSection';
import PrimaryContentSection from '@/features/home/PrimaryContentSection';

export default function Home() {
return (
<section className="text-center w-full mx-auto">
<HeroSection />
{/* <h1 className="text-3xl font-bold mb-4 text-left">
도전할 HSK 급수를 선택하세요
</h1>
<div className="text-lg sm:text-xl text-gray-600 dark:text-gray-300 mb-6 grid grid-cols-3 gap-4">
{levels.map((item, index) => (
<Button
asChild
className="rounded-lg p-6 text-lg"
variant="outline"
key={index}
>
<Link href={`/word/${item.link}`} className="text-center">
{item.name}
</Link>
</Button>
))}
</div> */}
<PrimaryContentSection />
<CTASection />
</section>
);
}
2 changes: 1 addition & 1 deletion src/app/word/[level]/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default async function WordDetailPage({ params }: Props) {
};

return (
<div className="w-full max-w-5xl mx-auto py-10 lg:py-15">
<div className="w-full max-w-[1400px] mx-auto py-10 lg:py-15">
<ClientWordDetail wordId={id} initialData={initialData} />
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/app/word/[level]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default async function WordPage({ params }: Props) {
}

return (
<div className="w-full max-w-5xl px-4 sm:px-6 lg:px-8 mx-auto py-10 lg:py-15">
<div className="w-full max-w-[1400px] px-4 sm:px-6 lg:px-8 mx-auto py-10 lg:py-15">
<h1 className="title">{level}급 단어</h1>
<div className="flex justify-end mb-10 animate-wiggle">
{/* 학습중 으로 상태 변경하려면... usestate 필요. */}
Expand Down
5 changes: 3 additions & 2 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ export default function Header() {
];

return (
<div className="px-4 md:px-10 py-5 bg-stone-700">
// <div className="px-4 md:px-10 py-5 bg-white/80 backdrop-blur-md border-b border-white/50">
<div className="px-4 md:px-10 py-5 bg-white border-b border-slate-200">
<div className="flex items-center justify-between">
<h1 className="font-bold text-2xl md:text-3xl text-white">
<h1 className="font-bold text-2xl md:text-3xl text-slate-800">
<Link href="/">HSKPass</Link>
</h1>
{/* desktop layout */}
Expand Down
41 changes: 5 additions & 36 deletions src/components/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { Button } from '@/components/ui/button';
import { supabase } from '@/lib/supabase/client';

import { toast } from 'sonner';
import {
Dialog,
Expand All @@ -10,57 +10,26 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { logout } from '@/lib/supabase/userApi';
import { logout, loginWithGoogle } from '@/lib/supabase/userApi';
import { LogInIcon } from 'lucide-react';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { useRouter } from 'next/navigation';
import { useUser } from '@/hooks/useUser';
import { useQueryClient } from '@tanstack/react-query';
import { useModal } from '@/hooks/useMoal';

// Google Icon
function GoogleIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
className="w-5 h-5"
>
<path
fill="#EA4335"
d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"
/>
<path
fill="#4285F4"
d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"
/>
<path
fill="#FBBC05"
d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"
/>
<path
fill="#34A853"
d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"
/>
</svg>
);
}
import { GoogleIcon } from '@/components/icons/GoogleIcon';

export default function Login() {
const { data: user } = useUser();
const queryClient = useQueryClient();
const router = useRouter();
const { loginOpen, openLoginModal, closeLoginModal } = useModal();

const handleLogin = () => {
const handleLogin = async () => {
try {
// 브라우저에서 팝업 또는 리다이렉트를 통해 동작하므로 client component
supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: process.env.NEXT_PUBLIC_BASE_URL,
},
});
await loginWithGoogle();
} catch (error) {
console.error(`[ERROR] Failed login: ${error}`);
toast.error('로그인 실패. 다시 시도해주세요.');
Expand Down
26 changes: 26 additions & 0 deletions src/components/icons/GoogleIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export function GoogleIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
className="w-5 h-5"
>
<path
fill="#EA4335"
d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"
/>
<path
fill="#4285F4"
d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"
/>
<path
fill="#FBBC05"
d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"
/>
<path
fill="#34A853"
d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"
/>
</svg>
);
}
2 changes: 1 addition & 1 deletion src/components/ui/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
Expand Down
3 changes: 3 additions & 0 deletions src/features/home/CTASection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function CTASection() {
return <div></div>;
}
67 changes: 60 additions & 7 deletions src/features/home/HeroSection.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
'use client';

import { ArrowRight, BookOpen, Sparkles, Trophy } from 'lucide-react';
import Link from 'next/link';
import { GoogleIcon } from '@/components/icons/GoogleIcon';
import { loginWithGoogle } from '@/lib/supabase/userApi';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { useModal } from '@/hooks/useMoal';
import { toast } from 'sonner';
import { ArrowRight, BookOpen, Sparkles, Trophy } from 'lucide-react';

export function HeroSection() {
const { loginOpen, openLoginModal, closeLoginModal } = useModal();

const hskLevels = [
{ level: 1, words: 150, color: 'from-green-400 to-emerald-500' },
{ level: 2, words: 300, color: 'from-blue-400 to-cyan-500' },
Expand All @@ -28,8 +42,18 @@ export function HeroSection() {
{ char: '文', left: '65%', top: '80%' },
];

const handleLogin = async () => {
try {
// 브라우저에서 팝업 또는 리다이렉트를 통해 동작하므로 client component
await loginWithGoogle();
} catch (error) {
console.error(`[ERROR] Failed login: ${error}`);
toast.error('로그인 실패. 다시 시도해주세요.');
}
};

return (
<section className="relative min-h-screen overflow-hidden bg-gradient-to-br from-blue-50 via-purple-50 to-pink-50">
<section className="relative h-[850px] overflow-hidden bg-gradient-to-br from-slate-50 via-indigo-50 to-sky-50">
{/* 백그라운드에 한자 플로팅 */}
<div className="absolute inset-0 overflow-hidden pointer-events-none opacity-5">
{floatingChars.map((item, i) => (
Expand All @@ -53,7 +77,7 @@ export function HeroSection() {
<div className="absolute top-0 right-0 w-96 h-96 bg-blue-300 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob animation-delay-2000"></div>
<div className="absolute bottom-0 left-1/2 w-96 h-96 bg-pink-300 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob animation-delay-4000"></div>

<div className="relative container mx-auto px-4 py-20 lg:py-32">
<div className="relative container max-w-[1400px] mx-auto px-4 py-20 lg:py-32">
<div className="grid lg:grid-cols-5 gap-12 items-center">
<div className="lg:col-span-3 space-y-8">
<div className="space-y-4 text-left">
Expand All @@ -65,9 +89,9 @@ export function HeroSection() {
<span className="text-gray-900">똑똑한 HSK 학습</span>
</h1>
<p className="text-xl lg:text-2xl text-gray-600 max-w-2xl">
10,000개 이상의 단어, AI 생성 예문, 실시간 퀴즈로
10,000개 이상의 HSK 단어를
<br />
획순 애니메이션과 함께 정확하게 배우세요
AI가 생성한 예문과 퀴즈로 학습하세요
</p>
</div>

Expand All @@ -88,7 +112,10 @@ export function HeroSection() {

{/* CTA Buttons */}
<div className="flex flex-col sm:flex-row gap-4 pt-4">
<button className="group bg-gradient-to-r from-blue-600 to-purple-600 text-white px-8 py-4 rounded-xl font-semibold text-lg shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200 flex items-center justify-center gap-2">
<button
className="group bg-indigo-600 text-white px-8 py-4 rounded-xl font-semibold text-lg shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200 flex items-center justify-center gap-2 cursor-pointer"
onClick={openLoginModal}
>
시작하기
<ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
</button>
Expand Down Expand Up @@ -144,7 +171,33 @@ export function HeroSection() {
</div>
</div>
</div>

{/* 로그인 모달 */}
<Dialog
open={loginOpen}
onOpenChange={(open) => {
if (!open) closeLoginModal();
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>로그인</DialogTitle>

<DialogDescription className="py-10 flex justify-center">
<Button
onClick={handleLogin}
variant="outline"
className="w-full p-6 gap-3 flex items-center justify-center"
>
<GoogleIcon />
<span className="text-sm text-gray-700">
구글 계정으로 로그인
</span>
</Button>
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</section>
// </section>
);
}
63 changes: 63 additions & 0 deletions src/features/home/PrimaryContentSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';

import { Dot } from 'lucide-react';

export default function PrimaryContentSection() {
const contents = [
{
title: 'AI가 만든 예문·동의어로 맥락 학습',
features: [
'자연스러운 예문으로 단어 이해',
'유의어·반의어·동의어 한눈에 파악',
'맥락 속에서 실제 사용법 학습',
],
},
{
title: 'AI 퀴즈로 바로 복습',
features: [
'다양한 유형의 퀴즈 자동 생성',
'실제 시험처럼 반복 학습',
'즉시 피드백으로 완벽 이해',
],
},
{
title: '학습 결과를 한눈에',
features: [
'잘 외워지지 않는 단어 자동 체크',
'퀴즈 결과와 오답 내역 확인',
'마이페이지에서 한눈에 확인',
],
},
];

return (
<div className="w-full max-w-[1400px] mx-auto py-20 grid grid-cols-1 md:grid-cols-3 gap-10">
{contents.map((content, index) => (
<Card className="w-full min-h-[300px] py-10" key={`card-${index}`}>
<CardHeader className="px-10">
<CardTitle className="text-2xl font-bold text-left">
<Badge
variant="secondary"
className="mr-3 bg-indigo-500 text-white text-base"
>
Step {index + 1}
</Badge>
<p className="mt-4">{content.title}</p>
</CardTitle>
</CardHeader>
<CardContent className="text-left whitespace-pre-line px-10">
<ul>
{content.features.map((item, index) => (
<li className="flex mb-1" key={index}>
<Dot />
{item}
</li>
))}
</ul>
</CardContent>
</Card>
))}
</div>
);
}
14 changes: 14 additions & 0 deletions src/lib/supabase/userApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,17 @@ export const logout = async () => {
throw error;
}
};

export const loginWithGoogle = async () => {
const { error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: process.env.NEXT_PUBLIC_BASE_URL,
},
});

if (error) {
console.error(`[ERROR] Failed login: ${error}`);
throw error;
}
};