diff --git a/apps/web/app/(auth)/login/LoginForm.tsx b/apps/web/app/(auth)/login/LoginForm.tsx index 5cd30d1..44ddb5b 100644 --- a/apps/web/app/(auth)/login/LoginForm.tsx +++ b/apps/web/app/(auth)/login/LoginForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import Link from 'next/link'; import { signIn, getSession } from 'next-auth/react'; @@ -11,6 +11,16 @@ import { toast } from 'sonner'; import SocialLoginButtons from '@/app/components/SocialLoginButtons'; import type { OAuthProvider } from '@/lib/auth-providers'; +const OAUTH_ERROR_MESSAGES: Record = { + OAuthAccountNotLinked: 'This email is already registered. Sign in with email & password, or use the same social login you originally used.', + OAuthSignin: 'Could not start social login. Please try again.', + OAuthCallback: 'Social login failed during callback. Please try again.', + OAuthCreateAccount: 'Could not create account via social login. Please try again.', + Configuration: 'A server configuration error occurred. Please try again later.', + AccessDenied: 'Access was denied. Please try again.', + Verification: 'The sign-in link has expired or already been used.', +}; + const loginSchema = z.object({ email: z.string().email('Please enter a valid email address'), password: z.string().min(1, 'Password is required'), @@ -21,8 +31,17 @@ export default function LoginForm({ enabledProviders }: { enabledProviders: OAut const router = useRouter(); const searchParams = useSearchParams(); const callbackUrl = searchParams.get('callbackUrl'); + const oauthError = searchParams.get('error'); const [isLoading, setIsLoading] = useState(false); + useEffect(() => { + if (oauthError) { + const message = OAUTH_ERROR_MESSAGES[oauthError] ?? `Sign-in error: ${oauthError}`; + toast.error(message, { duration: 6000 }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const { register, handleSubmit, @@ -130,17 +149,6 @@ export default function LoginForm({ enabledProviders }: { enabledProviders: OAut Don't have an account?{' '} Create one — it's free

- - {/* Demo credentials (dev only) */} - {process.env.NODE_ENV === 'development' && ( - <> -
Demo credentials
-
-

Customer: sarah.miller@gmail.com / Customer2026!

-

Admin: admin@ecowoods.ca / Admin2026!

-
- - )} ); diff --git a/apps/web/app/components/Header.tsx b/apps/web/app/components/Header.tsx index 7cbb099..71dd923 100644 --- a/apps/web/app/components/Header.tsx +++ b/apps/web/app/components/Header.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; +import { useSession } from 'next-auth/react'; /* ---------------------- Hooks ---------------------- */ function useScrollState() { @@ -42,6 +43,24 @@ const navigation = [ { label: 'FAQ', href: '#faq' }, ]; +const MYPAGE_NAV = [ + { href: '/mypage', label: 'Dashboard' }, + { href: '/mypage/quotes', label: 'My Quotes' }, + { href: '/mypage/projects', label: 'My Projects' }, + { href: '/mypage/invoices', label: 'Invoices & Payments' }, + { href: '/mypage/inquiries', label: 'Inquiries' }, +]; + +const ADMIN_NAV = [ + { href: '/admin', label: 'Dashboard' }, + { href: '/admin/quotes', label: 'Quotes & Leads' }, + { href: '/admin/projects', label: 'Projects' }, + { href: '/admin/invoices', label: 'Invoices' }, + { href: '/admin/users', label: 'Customers' }, + { href: '/admin/inquiries', label: 'Inquiries' }, + { href: '/admin/settings', label: 'Settings' }, +]; + const PHONE_DISPLAY = '(416) 249-1276'; const PHONE_HREF = 'tel:+14162491276'; @@ -49,8 +68,10 @@ const PHONE_HREF = 'tel:+14162491276'; export default function Header() { const { direction, scrolled } = useScrollState(); const [mobileOpen, setMobileOpen] = useState(false); + const [portalExpanded, setPortalExpanded] = useState(false); const [activeSection, setActiveSection] = useState(''); const [baseUrl, setBaseUrl] = useState('/'); + const { data: session, status } = useSession(); useEffect(() => { setBaseUrl(window.location.origin + '/'); @@ -70,6 +91,11 @@ export default function Header() { }; }, [mobileOpen]); + // Reset portal sub-menu when mobile sheet closes + useEffect(() => { + if (!mobileOpen) setPortalExpanded(false); + }, [mobileOpen]); + // Track active section on scroll useEffect(() => { const onScroll = () => { @@ -99,6 +125,12 @@ export default function Header() { return () => window.removeEventListener('keydown', onKey); }, []); + const isAdmin = session?.user?.role === 'ADMIN'; + const isLoggedIn = status === 'authenticated'; + const portalLabel = isAdmin ? 'Admin' : 'My Page'; + const portalNav = isAdmin ? ADMIN_NAV : MYPAGE_NAV; + const portalIndex = navigation.length + 2; + return ( <>
- + - Login + {isLoggedIn ? portalLabel : 'Login'} @@ -214,10 +246,48 @@ export default function Header() { Free Quote 0{navigation.length + 1} - setMobileOpen(false)}> - Login - 0{navigation.length + 2} - + + {/* Portal section — Login or My Page / Admin */} + {!isLoggedIn && status !== 'loading' ? ( + setMobileOpen(false)}> + Login + 0{portalIndex} + + ) : isLoggedIn ? ( +
+ + {portalExpanded && ( +
+ {portalNav.map((item) => ( + setMobileOpen(false)} + > + {item.label} + + ))} +
+ )} +
+ ) : null}
diff --git a/apps/web/app/components/SocialLoginButtons.tsx b/apps/web/app/components/SocialLoginButtons.tsx index bc57446..04b3c91 100644 --- a/apps/web/app/components/SocialLoginButtons.tsx +++ b/apps/web/app/components/SocialLoginButtons.tsx @@ -1,7 +1,7 @@ 'use client'; /** - * SocialLoginButtons — Google, Facebook, Apple OAuth 로그인 버튼 + * SocialLoginButtons — Google, Facebook, X(Twitter) OAuth 로그인 버튼 * * 어떤 provider가 환경변수에 설정되어 있는지 서버에서 prop으로 받아 * 활성화된 버튼만 렌더링합니다. @@ -9,7 +9,7 @@ * 사용 예시: * */ @@ -17,7 +17,7 @@ import { useState } from 'react'; import { signIn } from 'next-auth/react'; import { toast } from 'sonner'; -type Provider = 'google' | 'facebook' | 'apple'; +type Provider = 'google' | 'facebook' | 'twitter'; const PROVIDER_CONFIG: Record = { google: { @@ -44,22 +44,22 @@ const PROVIDER_CONFIG: Record ), style: { - background: '#1877F2', - color: '#ffffff', - border: '1px solid #1877F2', + background: '#ffffff', + color: '#3c4043', + border: '1px solid #dadce0', }, }, - apple: { - label: 'Continue with Apple', + twitter: { + label: 'Continue with X', icon: ( -