@gftdcojp/gftd-orm
Version:
Enterprise-grade real-time data platform with ksqlDB, inspired by Supabase architecture
64 lines • 2.39 kB
JSX
'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