Skip to content
Open
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
49 changes: 48 additions & 1 deletion recipe-demo/package-lock.json

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

3 changes: 2 additions & 1 deletion recipe-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"firebase": "^12.17.1",
"next": "^16.3.1",
"react": "^19.2.8",
"react-dom": "^19.2.8"
"react-dom": "^19.2.8",
"reactfire": "4.2.6-exp.ac3ccf9"
},
"devDependencies": {
"@types/node": "^24.3.0",
Expand Down
6 changes: 3 additions & 3 deletions recipe-demo/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import '@picocss/pico/css/pico.min.css';
import { SessionProvider } from '@/lib/session-context';
import { Providers } from '@/lib/providers';
import { SessionNav } from '@/components/SessionNav';

export const metadata: Metadata = {
Expand All @@ -13,7 +13,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
return (
<html lang="en">
<body>
<SessionProvider>
<Providers>
<header className="container">
<nav>
<ul>
Expand All @@ -32,7 +32,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</nav>
</header>
<main className="container">{children}</main>
</SessionProvider>
</Providers>
</body>
</html>
);
Expand Down
18 changes: 11 additions & 7 deletions recipe-demo/src/components/RecipeBrowser.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
'use client';

import { useState } from 'react';
import { useFirestoreCollectionData } from 'reactfire';
import { RecipeList } from './RecipeList';
import { useRecipes } from '@/lib/use-recipes';
import { recipeQuery } from '@/lib/recipes';
import { CUISINES, type Cuisine, type Recipe } from '@/lib/types';

export function RecipeBrowser({ initialRecipes }: { initialRecipes: Recipe[] }) {
const [cuisine, setCuisine] = useState<Cuisine | 'all'>('all');
const { recipes, status, error } = useRecipes(cuisine, initialRecipes);
// The server list is unfiltered, so it is only a valid seed for the unfiltered query.
// The key's presence is what counts: useObservable tests hasOwnProperty('initialData'),
// so passing it as undefined would report success with no data rather than loading.
const { data, status } = useFirestoreCollectionData(
recipeQuery(cuisine),
cuisine === 'all' ? { idField: 'id', initialData: initialRecipes } : { idField: 'id' },
);
const recipes = data as Recipe[];

return (
<>
Expand All @@ -23,11 +31,7 @@ export function RecipeBrowser({ initialRecipes }: { initialRecipes: Recipe[] })
</select>
</label>

{error ? (
<article aria-invalid="true">Could not load recipes: {error.message}</article>
) : (
<RecipeList recipes={recipes} loading={status === 'loading'} />
)}
<RecipeList recipes={recipes} loading={status === 'loading'} />
</>
);
}
4 changes: 2 additions & 2 deletions recipe-demo/src/components/RecipeCard.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
'use client';

import { useState } from 'react';
import { useUser } from 'reactfire';
import { toggleLike } from '@/lib/recipes';
import { useSession } from '@/lib/session-context';
import type { Recipe } from '@/lib/types';

export function RecipeCard({ recipe }: { recipe: Recipe }) {
const { user } = useSession();
const { data: user } = useUser();
const [pending, setPending] = useState(false);
const liked = user ? recipe.likedBy.includes(user.uid) : false;

Expand Down
15 changes: 8 additions & 7 deletions recipe-demo/src/components/RequireAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,27 @@

import { usePathname, useRouter } from 'next/navigation';
import { useEffect, type ReactNode } from 'react';
import { useSession } from '@/lib/session-context';
import { useSigninCheck } from 'reactfire';

export function RequireAuth({ children }: { children: ReactNode }) {
const { user, status } = useSession();
const { status, data: signinResult } = useSigninCheck();
const signedIn = signinResult?.signedIn ?? false;
const router = useRouter();
const pathname = usePathname();

useEffect(() => {
// Waiting for 'ready' is what stops a signed-in user being bounced to
// /signin on every hard reload, before onAuthStateChanged has fired.
if (status === 'ready' && !user) {
// Waiting for 'success' is what stops a signed-in user being bounced to
// /signin on every hard reload, before the auth state has resolved.
if (status === 'success' && !signedIn) {
router.replace(`/signin?next=${encodeURIComponent(pathname)}`);
}
}, [status, user, router, pathname]);
}, [status, signedIn, router, pathname]);

if (status === 'loading') {
return <article aria-busy="true">Checking your session</article>;
}

if (!user) {
if (!signedIn) {
return null;
}

Expand Down
4 changes: 2 additions & 2 deletions recipe-demo/src/components/SessionNav.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
'use client';

import Link from 'next/link';
import { useUser } from 'reactfire';
import { logOut } from '@/lib/session';
import { useSession } from '@/lib/session-context';

export function SessionNav() {
const { user, status } = useSession();
const { data: user, status } = useUser();

if (status === 'loading') {
return (
Expand Down
16 changes: 16 additions & 0 deletions recipe-demo/src/lib/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use client';

import { type ReactNode } from 'react';
import { AuthProvider, FirebaseAppProvider, FirestoreProvider } from 'reactfire';
import { app, firestore } from './firebase';
import { auth } from './session';

export function Providers({ children }: { children: ReactNode }) {
return (
<FirebaseAppProvider firebaseApp={app}>
<FirestoreProvider sdk={firestore}>
<AuthProvider sdk={auth}>{children}</AuthProvider>
</FirestoreProvider>
</FirebaseAppProvider>
);
}
18 changes: 0 additions & 18 deletions recipe-demo/src/lib/session-context.tsx

This file was deleted.

11 changes: 1 addition & 10 deletions recipe-demo/src/lib/session.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { getAuth, connectAuthEmulator, onAuthStateChanged, signInWithEmailAndPassword, signOut, type User } from 'firebase/auth';
import { getAuth, connectAuthEmulator, signInWithEmailAndPassword, signOut } from 'firebase/auth';
import { app, useEmulators } from './firebase';

export const auth = getAuth(app);
Expand All @@ -12,15 +12,6 @@ if (useEmulators && !(EMULATOR_SENTINEL in globalThis)) {
connectAuthEmulator(auth, 'http://127.0.0.1:9099', { disableWarnings: true });
}

export interface Session {
user: User | null;
status: 'loading' | 'ready';
}

export function subscribeToSession(onChange: (session: Session) => void) {
return onAuthStateChanged(auth, (user) => onChange({ user, status: 'ready' }));
}

export function signIn(email: string, password: string) {
return signInWithEmailAndPassword(auth, email, password);
}
Expand Down
44 changes: 0 additions & 44 deletions recipe-demo/src/lib/use-recipes.ts

This file was deleted.

Loading