Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feats): Initialize database, sign-in/sign-out, header #3

Merged
merged 3 commits into from
Oct 27, 2024
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 package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"@mui/icons-material": "^6.1.5",
"@mui/material": "^6.1.5",
"firebase": "^11.0.1",
"react": "^18.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.0",
"react-router-dom": "^6.27.0"
},
Expand Down
63 changes: 63 additions & 0 deletions src/components/common/Header.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { AppBar, Avatar, Box, Button, IconButton, Toolbar, Typography } from '@mui/material';
import { handleSignIn, handleSignOut } from '@utils/auth';
import { auth } from '@utils/firebaseConfig';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';

const Header = () => {
const [user, setUser] = useState(null);
const navigate = useNavigate();

useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setUser(user);
});
return () => unsubscribe();
}, []);

const handleAuthClick = async () => {
if (user) {
await handleSignOut(navigate);
} else {
await handleSignIn();
}
};

return (
<AppBar position="sticky" sx={{ backgroundColor: 'primary.light', color: '#000' }}>
<Toolbar sx={{ position: 'relative', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{/* Placeholder Box for back button if needed in the future */}
<Box sx={{ width: '48px' }} />
</Box>

<Typography
variant="h6"
sx={{
position: 'absolute',
left: '50%',
transform: 'translateX(-50%)',
fontWeight: '600',
fontSize: '1.4rem',
}}
>
Stepwise
</Typography>

<Box sx={{ display: 'flex', alignItems: 'center' }}>
{user ? (
<IconButton edge="end" color="inherit" onClick={handleAuthClick}>
<Avatar alt={user.displayName} src={user.photoURL} />
</IconButton>
) : (
<Button color="inherit" onClick={handleAuthClick}>
Sign In
</Button>
)}
</Box>
</Toolbar>
</AppBar>
);
};

export default Header;
9 changes: 8 additions & 1 deletion src/pages/Home.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import Header from '@components/common/Header';

const Home = () => {
return <h1>Home</h1>;
return (
<div>
<Header />
<h1>Home</h1>
</div>
);
};

export default Home;
47 changes: 47 additions & 0 deletions src/utils/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { createFirstUserProfile, fetchUserProfile } from '@utils/firebase/createUserProfile';
import { auth } from '@utils/firebaseConfig';
import { GoogleAuthProvider, signInWithPopup, signOut } from 'firebase/auth';

const provider = new GoogleAuthProvider();

// Google Sign-In function
const signInWithGoogle = async () => {
try {
const result = await signInWithPopup(auth, provider);
return result.user;
} catch (error) {
console.error('Error during sign-in:', error);
return null;
}
};

// Handle Sign-In
// Returns true if the user already exists in the database, false if a new profile was created
export const handleSignIn = async () => {
const user = await signInWithGoogle();

if (user) {
// Try to fetch the existing user profile
const { profile } = await fetchUserProfile(user.uid);

// If no profile found, create one with default data
if (!profile) {
await createFirstUserProfile(user);
return false; // New user profile created
}
return true; // User profile already exists
}
return false; // Sign-in failed or cancelled
};

// Handle Sign-Out
// Redirects to homepage after sign-out
export const handleSignOut = async (navigate) => {
try {
await signOut(auth);
console.log('Sign out successful');
navigate('/');
} catch (error) {
console.error('Error during sign-out:', error);
}
};
77 changes: 77 additions & 0 deletions src/utils/firebase/createUserProfile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { db } from '@utils/firebaseConfig';
import { doc, getDoc, setDoc, updateDoc } from 'firebase/firestore';

// Unified function to fetch and listen to user profile changes by UID
// (supports both regular and transaction-based fetches)
export const fetchUserProfile = async (uid, transaction = null) => {
try {
const userRef = doc(db, 'users', uid);

// If transaction is provided, use it to fetch the document
const userSnapshot = transaction ? await transaction.get(userRef) : await getDoc(userRef);

if (!userSnapshot.exists()) {
console.error(`User profile for ${uid} does not exist`);
return { ref: userRef, profile: null }; // Return consistent format with null profile
}

const profile = userSnapshot.data();
return { ref: userRef, profile };
} catch (error) {
console.error('Error fetching user profile:', error);
return { ref: null, profile: null };
}
};

// Check or create user profile in Firestore (uses fetchUserProfile to streamline code)
export const createFirstUserProfile = async (user) => {
try {
const { uid, photoURL, displayName } = user;
const defaultProfile = {
uid,
profilePic: photoURL || '',
name: displayName || '',
goals: [
{
name: '',
progress: 0,
microgoals: [
{
name: '',
progress: 0,
tasks: [{ name: '', completed: false }],
},
],
},
],
streak: [],
};

// If the profile does not exist, create it with the default data
await setDoc(doc(db, 'users', uid), defaultProfile);
console.warn('New user profile created with default data.');

return true;
} catch (error) {
console.error('Error checking or creating user profile:', error);
return false; // Return false if an error occurs
}
};

// Get user profile by UID
// (simple wrapper around fetchUserProfile for non-transaction use)
export const getUserProfile = async (uid) => {
const fetchedUser = await fetchUserProfile(uid);
return fetchedUser ? fetchedUser.profile : null; // Return only the profile data
};

// Update user profile by UID
export const updateUserProfile = async (uid, updates) => {
try {
const userDocRef = doc(db, 'users', uid);
await updateDoc(userDocRef, updates);
console.warn('User profile updated');
} catch (error) {
console.error('Error updating user profile:', error);
}
};
18 changes: 16 additions & 2 deletions src/utils/firebaseConfig.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
// Import the functions you need from the SDKs you need
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import {
CACHE_SIZE_UNLIMITED,
initializeFirestore,
persistentLocalCache,
persistentSingleTabManager,
} from 'firebase/firestore';
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

Expand All @@ -17,6 +24,13 @@ const firebaseConfig = {

// Initialize Firebase
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

export { db };
// Enable IndexedDB persistence with single-tab manager
export const db = initializeFirestore(app, {
localCache: persistentLocalCache({
tabManager: persistentSingleTabManager(),
cacheSizeBytes: CACHE_SIZE_UNLIMITED,
}),
});

export const auth = getAuth(app);
Loading