UNPKG

@gftdcojp/gftd-orm

Version:

Enterprise-grade real-time data platform with ksqlDB, inspired by Supabase architecture

64 lines 2.39 kB
'use client'; import React, { useState, useEffect } from 'react'; import { useUser } from '../nextjs-auth0-hooks'; /** * Hydration安全な認証コンポーネント * React BuildError / Hydration Error を防ぐ実装 */ export function SafeAuthComponent() { const { user, isLoading, error } = useUser(); const [isMounted, setIsMounted] = useState(false); useEffect(() => { setIsMounted(true); }, []); // 🔧 Hydration対策: マウント前は統一された表示 if (!isMounted) { return <div className="text-gray-500">読み込み中...</div>; } if (isLoading) { return (<div className="flex items-center gap-2"> <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600"></div> <span>認証確認中...</span> </div>); } if (error) { return (<div className="text-red-600"> <p>認証エラーが発生しました</p> <button onClick={() => window.location.reload()} className="text-blue-600 underline"> 再試行 </button> </div>); } if (user) { // UserPayloadから表示用データを取得 const displayName = user.user_metadata?.name || user.email || user.sub; const profilePicture = user.user_metadata?.picture || user.app_metadata?.picture; return (<div className="flex items-center gap-4"> {profilePicture && (<img src={profilePicture} alt="Profile" className="w-8 h-8 rounded-full"/>)} <span>Welcome, {displayName}!</span> <a href="/auth/logout" className="btn btn-ghost"> Logout </a> </div>); } return (<div className="flex gap-2"> <a href="/auth/login" className="btn btn-primary">Login</a> <a href="/auth/login?screen_hint=signup" className="btn btn-secondary">Sign Up</a> </div>); } /** * HOC版: 任意のコンポーネントをHydration安全にする */ export function withHydrationSafety(Component) { return function HydrationSafeComponent(props) { const [isMounted, setIsMounted] = useState(false); useEffect(() => { setIsMounted(true); }, []); if (!isMounted) { return <div className="text-gray-500">読み込み中...</div>; } return <Component {...props}/>; }; } //# sourceMappingURL=SafeAuthComponent.jsx.map