This documentation provides a step-by-step guide to building a simplified Facebook clone using the provided boilerplate. The project is structured to include user authentication, post creation, and displaying user posts.
Table of Contents
- Setup Authentication
- Create the Post Model
- Create the Post Service
- Create the Post Router
- Create the CreatePost Component
- Create the YourPosts Component
- Integrate Components in the App Page
1. Setup Authentication
To set up authentication, NextAuth is integrated into the project. Update the environment variables with your credentials to enable sign up and sign in using Email, Google, and more.
MONGODB_URI=mongodb-connection-string
NEXTAUTH_SECRET=random-character
SENDGRID_API_KEY=api-key-from-sendgrid
GOOGLE_ID=google-crediential-id
GOOGLE_SECRET=google-credential-secret
2. Create the Post Model
Define the post model using Mongoose to manage post data in MongoDB.
import mongoose, { Schema, Document } from "mongoose";
export interface IPost extends Document {
userId: mongoose.Schema.Types.ObjectId;
text: string;
image?: string;
likes?: number;
}
const PostSchema: Schema = new Schema(
{
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "users",
required: true,
},
text: { type: String, required: true },
image: { type: String },
likes: { type: Number, default: 0 },
},
{
timestamps: { createdAt: "created_at", updatedAt: "updated_at" },
}
);
export const Post =
mongoose.models.posts || mongoose.model<IPost>("posts", PostSchema);
3. Create the Post Service
Implement the post service to handle post creation and fetching user posts.
import { IPost, Post } from "../models/post";
import { User } from "../models/user";
import { generateTemporaryPublicUrl } from "./aws-service";
export interface ICreatePostInput {
userId: string;
text: string;
image?: string;
}
export const getUserPosts = async (
userId: string
): Promise<
{
_id: string;
text: string;
image?: string;
likes?: number;
author?: {
name: string;
avatar: string;
};
created_at: Date;
}[]
> => {
const posts = await Post.find({ userId }).sort("-created_at");
const postsWithUrls = await Promise.all(
posts.map(async (post) => {
const user = await User.findById(post.userId);
const postObject = {
...post.toObject(),
author: { name: user.name, avatar: user.image },
};
if (post.image) {
const imageUrl = await generateTemporaryPublicUrl({
key: post.image,
expiresIn: 86400,
});
console.log({ imageUrl });
return {
...postObject,
image: imageUrl,
};
}
return postObject;
})
);
return postsWithUrls;
};
export const createPost = async ({
userId,
text,
image,
}: ICreatePostInput): Promise<IPost> => {
try {
const newPost = new Post({
userId,
text,
image,
});
return await newPost.save();
} catch (error: any) {
throw new Error("Error creating post: " + error.message);
}
};
4. Create the Post Router
Define the router to handle API requests related to posts.
import { z } from "zod";
import { createPost, getUserPosts } from "../service/post-service";
import { protectedProcedure, router } from "../trpc";
export const createPostRouter = () => {
return router({
fetchUserPosts: protectedProcedure
.input(z.object({}))
.query(async (opts) => {
const userId = opts.ctx.session.user.id;
const posts = await getUserPosts(userId);
console.log(posts);
return {
posts: posts.map((post) => {
return {
id: post._id as string,
text: post.text,
image: post.image,
author: post.author,
createdAt: post.created_at,
};
}),
};
}),
createPost: protectedProcedure
.input(
z.object({
text: z.string(),
image: z.string().optional(),
})
)
.mutation(async (opts) => {
const userId = opts.ctx.session.user.id;
const { image, text } = opts.input;
try {
const newPost = await createPost({
text: text,
image: image,
userId: userId,
});
return newPost;
} catch (error: any) {
throw new Error("Error creating post: " + error.message);
}
}),
});
};
5. Create the CreatePost Component
Implement the CreatePost component for users to create new posts.
import React, { useState } from "react";
import { trpc } from "../shared/utils/trpc";
import AppButton from "../shared/components/AppButton";
export default function CreatePost() {
const { mutateAsync: createPost } = trpc.posts.createPost.useMutation();
const { mutateAsync: generateUploadUrl } =
trpc.image.generateUploadUrl.useMutation();
const [description, setDescription] = useState("");
const [selectedImage, setSelectedImage] = useState<File | null>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && files.length > 0) {
setSelectedImage(files[0]);
}
};
const createPostClicked = async () => {
if (!description || !selectedImage) {
alert("Please fill in all required fields");
return;
}
try {
// Generate upload URL for the selected image and upload the image
const { uploadUrl, fileName } = await generateUploadUrl({
fileExtension: getFileExtension(selectedImage.name),
});
await fetch(uploadUrl, { method: "PUT", body: selectedImage });
// Create post with description and uploaded image URL
const newPost = await createPost({
text: description,
image: fileName,
});
console.log("Post created successfully!", newPost);
// Optionally reset form fields or show success message
} catch (error) {
console.error("Error uploading image or creating post:", error);
// Handle error (e.g., show error message to user)
}
};
const getFileExtension = (fileName: string) => {
return fileName.split(".").pop()?.toLowerCase() ?? "";
};
return (
<div>
<div className="space-y-2 w-full bg-base-200 rounded-box p-8 hover:shadow-lg duration-200">
<h2 className="font-bold text-lg">Create a Post</h2>
<label className="form-control w-full">
<div className="label">
<span className="label-text">Post Description</span>
</div>
<textarea
className="textarea textarea-bordered"
placeholder="Write your post description here"
onChange={(e) => setDescription(e.target.value)}
></textarea>
</label>
<label className="form-control w-full">
<div className="label">
<span className="label-text">Upload Image</span>
</div>
<input
type="file"
className="file-input file-input-bordered w-full"
accept="image/*"
onChange={handleFileChange}
/>
</label>
<div className="pt-4">
<AppButton onClick={createPostClicked}>Create Post</AppButton>
</div>
</div>
</div>
);
}
6. Create the YourPosts Component
Create the YourPosts component to display a list of posts by the user.
import React from "react";
import { trpc } from "../shared/utils/trpc";
import dayjs from "dayjs";
const getInitials = (name: string) => {
const names = name.split(" ");
const initials = names.map((n) => n[0]).join("");
return initials;
};
const YourPosts = () => {
const { data: postResponse } = trpc.posts.fetchUserPosts.useQuery({});
return (
<div className="container mx-auto p-4">
<h2 className="text-2xl font-bold mb-4">Your Posts</h2>
<div className="grid gap-4">
{postResponse?.posts ? (
postResponse.posts.map((post) => (
<div
key={post.id}
className=" bg-base-200 rounded-box hover:shadow-lg duration-200 w-full p-6"
>
<div>
<div className="flex items-center mb-2">
{post.author?.avatar ? (
<img
src={post.author.avatar}
alt="Author Avatar"
className="w-10 h-10 rounded-full mr-
3"
/>
) : (
<div className="w-10 h-10 rounded-full mr-3 bg-gray-500 text-white flex items-center justify-center">
{getInitials(post.author?.name || "User")}
</div>
)}
<div>
<p className="font-semibold text-sm">{post.author?.name}</p>
<p className="text-xs text-gray-500">
{dayjs(post.createdAt).format("MMMM D, YYYY h:mm A")}
</p>
</div>
</div>
<p className="text-xl font-medium text-base-content">
{post.text}
</p>
{post.image && (
<figure className="w-full mt-3">
<img
src={post.image}
alt="Post Image"
className="rounded-lg w-full max-h-80 object-cover"
/>
</figure>
)}
</div>
</div>
))
) : (
<></>
)}
</div>
</div>
);
};
export default YourPosts;
7. Integrate Components in the App Page
Finally, integrate all components in the main app page.
import CreatePost from "../features/app/CreatePost";
import YourPosts from "../features/app/YourPosts";
import Nav from "../features/shared/components/Nav";
export default function App() {
return (
<div>
<Nav />
<div className="flex flex-col lg:flex-row max-w-7xl mx-auto p-4 min-h-screen">
<div className="w-full lg:w-[450px] lg:mr-8 flex-shrink-0">
<CreatePost />
</div>
<div className="flex-grow">
<YourPosts />
</div>
</div>
</div>
);
}
By following these steps, you will have a functioning simplified Facebook clone that includes user authentication, the ability to create posts, and display posts by the user.