Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jdgamble555 committed Feb 5, 2022
1 parent 0131eb6 commit d8edf0f
Show file tree
Hide file tree
Showing 30 changed files with 3,046 additions and 113 deletions.
9 changes: 9 additions & 0 deletions components/AuthCheck.tsx
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>;
}
40 changes: 40 additions & 0 deletions components/HeartButton.tsx
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>
);
}
58 changes: 58 additions & 0 deletions components/ImageUploader.tsx
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">{`![alt](${downloadURL})`}</code>}
</div>
);
}
3 changes: 3 additions & 0 deletions components/Loader.tsx
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;
}
22 changes: 22 additions & 0 deletions components/Metatags.tsx
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>
);
}
51 changes: 51 additions & 0 deletions components/Navbar.tsx
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>
);
}
21 changes: 21 additions & 0 deletions components/PostContent.tsx
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>
);
}
47 changes: 47 additions & 0 deletions components/PostFeed.tsx
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>
);
}
13 changes: 13 additions & 0 deletions components/UserProfile.tsx
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>
);
}
3 changes: 3 additions & 0 deletions lib/context.ts
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 });
86 changes: 86 additions & 0 deletions lib/firebase.ts
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,
};
}
Loading

0 comments on commit d8edf0f

Please sign in to comment.