-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0131eb6
commit d8edf0f
Showing
30 changed files
with
3,046 additions
and
113 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import Link from 'next/link'; | ||
import { useContext } from 'react'; | ||
import { UserContext } from '@lib/context'; | ||
|
||
// Component's children only shown to logged-in users | ||
export default function AuthCheck(props: any) { | ||
const { username } = useContext(UserContext); | ||
return username ? props.children : props.fallback || <Link href="/enter">You must be signed in</Link>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { auth } from '@lib/firebase'; | ||
import { useDocument } from 'react-firebase-hooks/firestore'; | ||
import { increment, writeBatch, doc, getFirestore } from "firebase/firestore"; | ||
|
||
|
||
// Allows user to heart or like a post | ||
export default function Heart({ postRef }: any) { | ||
|
||
const uid: any = auth?.currentUser?.uid; | ||
|
||
// Listen to heart document for currently logged in user | ||
const heartRef = doc(getFirestore(), postRef.path, 'hearts', uid); | ||
const [heartDoc] = useDocument(heartRef); | ||
|
||
// Create a user-to-post relationship | ||
const addHeart = async () => { | ||
const batch = writeBatch(getFirestore()); | ||
|
||
batch.update(postRef, { heartCount: increment(1) }); | ||
batch.set(heartRef, { uid }); | ||
|
||
await batch.commit(); | ||
}; | ||
|
||
// Remove a user-to-post relationship | ||
const removeHeart = async () => { | ||
const batch = writeBatch(getFirestore()); | ||
|
||
batch.update(postRef, { heartCount: increment(-1) }); | ||
batch.delete(heartRef); | ||
|
||
await batch.commit(); | ||
}; | ||
|
||
return heartDoc?.exists() ? ( | ||
<button onClick={removeHeart}>💔 Unheart</button> | ||
) : ( | ||
<button onClick={addHeart}>💗 Heart</button> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { useState } from 'react'; | ||
import { auth, storage, STATE_CHANGED } from '@lib/firebase'; | ||
import Loader from './Loader'; | ||
import { ref, uploadBytesResumable, getDownloadURL } from 'firebase/storage'; | ||
|
||
// Uploads images to Firebase Storage | ||
export default function ImageUploader() { | ||
const [uploading, setUploading] = useState(false); | ||
const [progress, setProgress] = useState(0); | ||
const [downloadURL, setDownloadURL] = useState(null); | ||
|
||
// Creates a Firebase Upload Task | ||
const uploadFile = async (e: any) => { | ||
// Get the file | ||
const file = Array.from(e.target.files)[0] as Blob; | ||
const extension = file.type.split('/')[1]; | ||
|
||
// Makes reference to the storage bucket location | ||
const uid: any = auth?.currentUser?.uid; | ||
const fileRef = ref(storage, `uploads/${uid}/${Date.now()}.${extension}`); | ||
setUploading(true); | ||
|
||
// Starts the upload | ||
const task = uploadBytesResumable(fileRef, file) | ||
|
||
// Listen to updates to upload task | ||
task.on(STATE_CHANGED, (snapshot) => { | ||
const pct: any = ((snapshot.bytesTransferred / snapshot.totalBytes) * 100).toFixed(0); | ||
setProgress(pct); | ||
}); | ||
|
||
// Get downloadURL AFTER task resolves (Note: this is not a native Promise) | ||
task | ||
.then(() => getDownloadURL(fileRef)) | ||
.then((url: any) => { | ||
setDownloadURL(url); | ||
setUploading(false); | ||
}); | ||
}; | ||
|
||
return ( | ||
<div className="box"> | ||
<Loader show={uploading} /> | ||
{uploading && <h3>{progress}%</h3>} | ||
|
||
{!uploading && ( | ||
<> | ||
<label className="btn"> | ||
📸 Upload Img | ||
<input type="file" onChange={uploadFile} accept="image/x-png,image/gif,image/jpeg" /> | ||
</label> | ||
</> | ||
)} | ||
|
||
{downloadURL && <code className="upload-snippet">{``}</code>} | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function Loader({ show }: { show: boolean }) { | ||
return show ? <div className="loader" ></div> : null; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import Head from 'next/head'; | ||
|
||
export default function Metatags({ | ||
title = 'The Full Next.js + Firebase Course', | ||
description = 'A complete Next.js + Firebase course by Fireship.io', | ||
image = 'https://fireship.io/courses/react-next-firebase/img/featured.png', | ||
}) { | ||
return ( | ||
<Head> | ||
<title>{title}</title> | ||
<meta name="twitter:card" content="summary" /> | ||
<meta name="twitter:site" content="@fireship_dev" /> | ||
<meta name="twitter:title" content={title} /> | ||
<meta name="twitter:description" content={description} /> | ||
<meta name="twitter:image" content={image} /> | ||
|
||
<meta property="og:title" content={title} /> | ||
<meta property="og:description" content={description} /> | ||
<meta property="og:image" content={image} /> | ||
</Head> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import Link from 'next/link'; | ||
import Img from 'next/image'; | ||
import { useContext } from 'react'; | ||
import { UserContext } from '@lib/context'; | ||
|
||
// Top navbar | ||
export default function Navbar() { | ||
|
||
const { user, username } = useContext(UserContext); | ||
|
||
return ( | ||
<nav className="navbar"> | ||
<ul> | ||
<li> | ||
<Link passHref={true} href="/"> | ||
<button className="btn-logo">FEED</button> | ||
</Link> | ||
</li> | ||
|
||
{/* user is signed-in and has username */} | ||
{username && ( | ||
<> | ||
<li className="push-left"> | ||
<Link passHref href="/admin"> | ||
<button className="btn-blue">Write Posts</button> | ||
</Link> | ||
</li> | ||
<li> | ||
{user?.photoURL && ( | ||
<Link passHref href={`/${username}`}> | ||
<a> | ||
<Img src={user?.photoURL} width="50px" height="50px" /> | ||
</a> | ||
</Link> | ||
)} | ||
</li> | ||
</> | ||
)} | ||
|
||
{/* user is not signed OR has not created username */} | ||
{!username && ( | ||
<li> | ||
<Link passHref href="/enter"> | ||
<button className="btn-blue">Log in</button> | ||
</Link> | ||
</li> | ||
)} | ||
</ul> | ||
</nav> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import Link from 'next/link'; | ||
import ReactMarkdown from 'react-markdown'; | ||
|
||
// UI component for main post content | ||
export default function PostContent({ post }: { post: any }) { | ||
const createdAt = typeof post?.createdAt === 'number' ? new Date(post.createdAt) : post.createdAt.toDate(); | ||
|
||
return ( | ||
<div className="card"> | ||
<h1>{post?.title}</h1> | ||
<span className="text-sm"> | ||
Written by{' '} | ||
<Link href={`/${post.username}/`}> | ||
<a className="text-info">@{post.username}</a> | ||
</Link>{' '} | ||
on {createdAt.toISOString()} | ||
</span> | ||
<ReactMarkdown>{post?.content}</ReactMarkdown> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import Link from 'next/link'; | ||
|
||
export default function PostFeed({ posts, admin = false }: { posts: any, admin?: boolean }) { | ||
return posts ? posts.map((post: any) => <PostItem post={post} key={post.slug} admin={admin} />) : null; | ||
} | ||
|
||
function PostItem({ post, admin }: { post: any, admin: boolean }) { | ||
// Naive method to calc word count and read time | ||
const wordCount = post?.content.trim().split(/\s+/g).length; | ||
const minutesToRead = (wordCount / 100 + 1).toFixed(0); | ||
|
||
return ( | ||
<div className="card"> | ||
<Link href={`/${post.username}`}> | ||
<a> | ||
<strong>By @{post.username}</strong> | ||
</a> | ||
</Link> | ||
|
||
<Link passHref href={`/${post.username}/${post.slug}`}> | ||
<h2> | ||
<a>{post.title}</a> | ||
</h2> | ||
</Link> | ||
|
||
<footer> | ||
<span> | ||
{wordCount} words. {minutesToRead} min read | ||
</span> | ||
<span className="push-left">💗 {post.heartCount || 0} Hearts</span> | ||
</footer> | ||
|
||
{/* If admin view, show extra controls for user */} | ||
{admin && ( | ||
<> | ||
<Link passHref href={`/admin/${post.slug}`}> | ||
<h3> | ||
<button className="btn-blue">Edit</button> | ||
</h3> | ||
</Link> | ||
|
||
{post.published ? <p className="text-success">Live</p> : <p className="text-danger">Unpublished</p>} | ||
</> | ||
)} | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import Img from 'next/image'; | ||
|
||
export default function UserProfile({ user }: { user: any}) { | ||
return ( | ||
<div className="box-center"> | ||
<Img src={user.photoURL || '/hacker.png'} width={150} height={150} objectFit="cover" className="card-img-center" /> | ||
<p> | ||
<i>@{user.username}</i> | ||
</p> | ||
<h1>{user.displayName || 'Anonymous User'}</h1> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { createContext } from 'react'; | ||
|
||
export const UserContext = createContext<any>({ user: null, username: null }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import { initializeApp, getApp, FirebaseOptions } from "firebase/app"; | ||
import { getAuth, GoogleAuthProvider } from "firebase/auth"; | ||
import { getFirestore, collection, where, getDocs, query, limit } from "firebase/firestore"; | ||
import { getStorage } from "firebase/storage"; | ||
|
||
const firebaseConfig = { | ||
apiKey: 'AIzaSyBX5gkKsbOr1V0zxBuSqHWFct12dFOsQHA', | ||
authDomain: 'nextfire-demo.firebaseapp.com', | ||
projectId: 'nextfire-demo', | ||
storageBucket: 'nextfire-demo.appspot.com', | ||
messagingSenderId: '827402452263', | ||
appId: '1:827402452263:web:c9a4bea701665ddf15fd02', | ||
}; | ||
|
||
// Initialize firebase | ||
// let firebaseApp; | ||
// let firestore; | ||
// if (!getApps().length) { | ||
// // firebase.initializeApp(firebaseConfig); | ||
// initializeApp(firebaseConfig); | ||
// firestore = getFirestore(); | ||
// } | ||
|
||
function createFirebaseApp(config: FirebaseOptions) { | ||
try { | ||
return getApp(); | ||
} catch { | ||
return initializeApp(config); | ||
} | ||
} | ||
|
||
// const firebaseApp = initializeApp(firebaseConfig); | ||
const firebaseApp = createFirebaseApp(firebaseConfig); | ||
|
||
|
||
|
||
// Auth exports | ||
// export const auth = firebase.auth(); | ||
export const auth = getAuth(firebaseApp); | ||
export const googleAuthProvider = new GoogleAuthProvider(); | ||
|
||
// Firestore exports | ||
export const firestore = getFirestore(firebaseApp); | ||
// export const firestore = firebase.firestore(); | ||
// export { firestore }; | ||
// export const serverTimestamp = serverTimestamp; | ||
// export const fromMillis = fromMillis; | ||
// export const increment = increment; | ||
|
||
// Storage exports | ||
export const storage = getStorage(firebaseApp); | ||
export const STATE_CHANGED = 'state_changed'; | ||
|
||
/// Helper functions | ||
|
||
|
||
/**` | ||
* Gets a users/{uid} document with username | ||
* @param {string} username | ||
*/ | ||
export async function getUserWithUsername(username: string) { | ||
// const usersRef = collection(firestore, 'users'); | ||
// const query = usersRef.where('username', '==', username).limit(1); | ||
|
||
const q = query( | ||
collection(firestore, 'users'), | ||
where('username', '==', username), | ||
limit(1) | ||
) | ||
const userDoc = (await getDocs(q)).docs[0]; | ||
return userDoc; | ||
} | ||
|
||
/**` | ||
* Converts a firestore document to JSON | ||
* @param {DocumentSnapshot} doc | ||
*/ | ||
export function postToJSON(doc: any) { | ||
const data = doc.data(); | ||
return { | ||
...data, | ||
// Gotcha! firestore timestamp NOT serializable to JSON. Must convert to milliseconds | ||
createdAt: data?.createdAt.toMillis() || 0, | ||
updatedAt: data?.updatedAt.toMillis() || 0, | ||
}; | ||
} |
Oops, something went wrong.