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
102 changes: 41 additions & 61 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useEffect, useState } from 'react'
import { Toaster } from 'react-hot-toast'
import { ArrowLeft } from 'lucide-react'
import type { Session } from '@supabase/supabase-js'
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { supabase } from './lib/supabase'
Expand All @@ -15,8 +14,7 @@ import { LayoutProvider } from './context/LayoutProvider'

export default function App() {
const [session, setSession] = useState<Session | null>(null)
const [showAuth, setShowAuth] = useState(false)
const [authMode, setAuthMode] = useState<'signin' | 'signup'>('signin')
const [loading, setLoading] = useState(true) // Add loading state
const [showUpdatePassword, setShowUpdatePassword] = useState(() => {
if (typeof window !== 'undefined') {
const hash = window.location.hash
Expand All @@ -26,82 +24,64 @@ export default function App() {
})

useEffect(() => {
// Check active session
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session)
setLoading(false)
})

const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session)
if (session) {
setShowAuth(false)
}
setLoading(false)
})

return () => subscription.unsubscribe()
}, [])

// If not signed in and showAuth is true, show AuthPage overlay
if (!session && showAuth) {
return (
<div className="min-h-screen bg-secondary-50 flex items-center justify-center p-4">
<div className="relative">
<button
onClick={() => setShowAuth(false)}
className="absolute top-6 left-6 flex items-center gap-2 text-gray-500 hover:text-primary-600 z-10 font-medium transition-colors cursor-pointer bg-white/80 backdrop-blur-sm px-4 py-2 rounded-full shadow-sm hover:shadow-md"
>
<ArrowLeft size={20} />
<span>Back to Home</span>
</button>
<AuthPage initialIsSignUp={authMode === 'signup'} />
</div>
</div>
)
}

return (
<BrowserRouter>
<LayoutProvider>
<Layout
session={session}
onSignOut={() => supabase.auth.signOut()}
onSignIn={() => {
setAuthMode('signin')
setShowAuth(true)
}}
>
<Routes>
<Route
path="/"
element={
!session ? (
<LandingPage
onSignIn={() => {
setAuthMode('signin')
setShowAuth(true)
}}
onSignUp={() => {
setAuthMode('signup')
setShowAuth(true)
}}
/>
) : (
<Routes>
<Route
path="/"
element={
!session ? (
<Layout session={session} onSignOut={() => supabase.auth.signOut()}>
<LandingPage />
</Layout>
) : (
<Layout session={session} onSignOut={() => supabase.auth.signOut()}>
<DashboardPage />
)
}
/>
<Route
path="/tree/:treeId"
element={
<ProtectedRoute session={session}>
</Layout>
)
}
/>
<Route
path="/login"
element={!session ? <AuthPage /> : <Navigate to="/" replace />}
/>
<Route
path="/signup"
element={!session ? <AuthPage /> : <Navigate to="/" replace />}
/>
<Route
path="/forgot-password"
element={!session ? <AuthPage /> : <Navigate to="/" replace />}
/>
<Route
path="/tree/:treeId"
element={
<ProtectedRoute session={session} loading={loading}>
<Layout session={session} onSignOut={() => supabase.auth.signOut()}>
<TreePage />
</ProtectedRoute>
}
/>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Layout>
</Layout>
</ProtectedRoute>
}
/>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>

<UpdatePasswordModal
isOpen={showUpdatePassword}
Expand Down
76 changes: 70 additions & 6 deletions src/components/AddMemberModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,31 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
const [relativeId, setRelativeId] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);

const [emailError, setEmailError] = useState('');

if (!isOpen) return null;

const validateEmail = (email: string) => {
if (!email) return true;
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
};

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
setEmailError('');

let isValid = true;
if (!validateEmail(email)) {
setEmailError('Invalid email address');
isValid = false;
}

if (!isValid) {
setIsSubmitting(false);
return;
}

try {
const newMember: Omit<FamilyMember, 'id'> & { tree_id: string } = {
Expand Down Expand Up @@ -123,7 +143,7 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
<div className="bg-white rounded-2xl shadow-xl w-full max-w-md m-0 max-h-[90vh] overflow-y-auto p-6">
<h2 className="text-xl font-bold text-primary-900 mb-4">Add Family Member</h2>

<form onSubmit={handleSubmit} className="space-y-4">
<form onSubmit={handleSubmit} className="space-y-4" noValidate>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Name <span className="text-red-500">*</span>
Expand Down Expand Up @@ -213,10 +233,19 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 outline-none"
onChange={e => {
setEmail(e.target.value);
if (emailError) setEmailError('');
}}
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500 outline-none ${emailError ? 'border-red-500 bg-red-50' : 'border-gray-300'}`}
placeholder="john@example.com"
/>
{emailError && (
<div className="flex items-center gap-1 mt-1 text-red-600 text-xs">
<span>⚠️</span>
<span>{emailError}</span>
</div>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Expand Down Expand Up @@ -295,9 +324,44 @@ export const AddMemberModal: React.FC<AddMemberModalProps> = ({ isOpen, onClose,
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 outline-none"
>
<option value="">Select Relative</option>
{existingMembers.map(m => (
<option key={m.id} value={m.id}>{m.name} (b. {m.birth_year})</option>
))}
{relationType === 'child' ? (
// Group spouses for "Child of"
(() => {
const processedIds = new Set<string>();
const options = [];

for (const m of existingMembers) {
if (processedIds.has(m.id)) continue;

if (m.spouse) {
const spouse = existingMembers.find(s => s.id === m.spouse);
if (spouse) {
options.push(
<option key={m.id} value={m.id}>
{m.name} & {spouse.name}
</option>
);
processedIds.add(m.id);
processedIds.add(spouse.id);
continue;
}
}

options.push(
<option key={m.id} value={m.id}>
{m.name} (b. {m.birth_year})
</option>
);
processedIds.add(m.id);
}
return options;
})()
) : (
// Standard list for "Spouse of"
existingMembers.map(m => (
<option key={m.id} value={m.id}>{m.name} (b. {m.birth_year})</option>
))
)}
</select>
)}
</div>
Expand Down
Loading