import { FirebaseApp, FirebaseOptions, initializeApp } from "firebase/app"; import { getAuth, signInWithPopup, GithubAuthProvider, onAuthStateChanged, signOut, Auth, User, } from "firebase/auth"; import { FirebaseFirestore, getFirestore, collection, getDocs, query, orderBy, CollectionReference, Timestamp, addDoc, } from "firebase/firestore"; import React, { useCallback, useContext, useEffect, useState } from "react"; import { AuthContext } from "./auth-context"; import { GalleryComposition, GalleryContext, GallerySubmitStatus, } from "./gallery-context"; const GALLERY_COLLECTION = "compositions"; const DEFAULT_APP_CONFIG: FirebaseOptions = { apiKey: "AIzaSyAV1kkVvSKEicEa8rLke9o_BxYBu1rb8kw", authDomain: "mystic-addaf.firebaseapp.com", projectId: "mystic-addaf", storageBucket: "mystic-addaf.appspot.com", messagingSenderId: "26787182745", appId: "1:26787182745:web:e4fbd9439b9279fe966008", measurementId: "G-JHKRSK1PR6", }; type FirebaseAppContext = { app: FirebaseApp; auth: Auth; provider: GithubAuthProvider; db: FirebaseFirestore; }; export const FirebaseAppContext = React.createContext(null); /** * A Firebase app provider. Any other components that use Firebase must * be a child of this. * * Note this component is assumed to never be unmounted, nor * for its non-children props to change. */ export const FirebaseAppProvider: React.FC<{ config?: FirebaseOptions }> = ({ config, children, }) => { const [value, setValue] = useState(null); useEffect(() => { const app = initializeApp(config || DEFAULT_APP_CONFIG); const auth = getAuth(app); const provider = new GithubAuthProvider(); const db = getFirestore(app); setValue({ app, auth, provider, db }); }, [config]); return ; }; /** * A Firebase GitHub authentication provider. Must be a child of a * `FirebaseAppProvider`. * * Note this component is assumed to never be unmounted. */ export const FirebaseGithubAuthProvider: React.FC<{}> = ({ children }) => { const appCtx = useContext(FirebaseAppContext); const [user, setUser] = useState(null); const [error, setError] = useState(undefined); const handleError = (e: Error) => setError(e.message); useEffect(() => { if (!appCtx) return; onAuthStateChanged(appCtx.auth, setUser); }, [appCtx]); const context: AuthContext = { loggedInUser: user && { id: user.uid, name: user.displayName || `GitHub user ${user.uid}`, }, providerName: appCtx && "GitHub", error, login: useCallback(() => { setError(undefined); appCtx && signInWithPopup(appCtx.auth, appCtx.provider).catch(handleError); }, [appCtx]), logout: useCallback(() => { setError(undefined); appCtx && signOut(appCtx.auth).catch(handleError); }, [appCtx]), }; return ; }; type FirebaseCompositionDocument = Omit< GalleryComposition, "id" | "createdAt" > & { createdAt: Timestamp; }; function getGalleryCollection(appCtx: FirebaseAppContext) { return collection( appCtx.db, GALLERY_COLLECTION ) as CollectionReference; } function docToComp( doc: FirebaseCompositionDocument, id: string ): GalleryComposition { const { createdAt, ...data } = doc; return { ...data, id, createdAt: createdAt.toDate(), }; } export const FirebaseGalleryProvider: React.FC<{}> = ({ children }) => { const appCtx = useContext(FirebaseAppContext); const [compositions, setCompositions] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(undefined); const [lastRefresh, setLastRefresh] = useState(0); const [submitStatus, setSubmitStatus] = useState("idle"); const [lastSubmission, setLastSubmission] = useState< GalleryComposition | undefined >(undefined); const context: GalleryContext = { compositions, isLoading, error, lastRefresh, lastSubmission, submitStatus, submit(props, onSuccess) { if (!(appCtx && submitStatus === "idle")) return; const doc: FirebaseCompositionDocument = { ...props, createdAt: Timestamp.now(), }; setSubmitStatus("submitting"); setLastSubmission(undefined); addDoc(getGalleryCollection(appCtx), doc) .then((docRef) => { const comp = docToComp(doc, docRef.id); setSubmitStatus("idle"); setCompositions([comp, ...compositions]); setLastSubmission(comp); onSuccess(docRef.id); }) .catch((e) => { setSubmitStatus("error"); console.log(e); }); }, refresh: useCallback(() => { if (!(appCtx && !isLoading)) return false; setError(undefined); setIsLoading(true); getDocs(query(getGalleryCollection(appCtx), orderBy("createdAt", "desc"))) .then((snapshot) => { setLastRefresh(Date.now()); setIsLoading(false); setCompositions( snapshot.docs.map((doc) => docToComp(doc.data(), doc.id)) ); }) .catch((e) => { setIsLoading(false); setError(e.message); }); return true; }, [appCtx, isLoading]), }; return ; };