Congratulations! You've got to the final part of this tutorial.
So far, you've generated a Strapi back-end, added the necessary Quora content types, and made modifications to the back-end by adding new routes to fill the front-end requirements.
All that's left is to build the front-end with Next.js.
This tutorial is divided into two:
The front-end has 5 pages:
In each of these pages, users can upvote or downvote questions and answers in addition to leaving comments.
Cloudflare Workers AI-generated answers are displayed on the individual question page and on the home page if no users have left answers for a question.
Throughout this final part of the tutorial, you will add pages, components, actions, assets, utilities, and dependencies to build a complete app that mimics Quora. So let's get started.
At the end of this tutorial, this is what the app will look like.
Run this command to generate the front-end:
cd apps && \
npx create-next-app@latest quora-frontend --no-src-dir --no-import-alias --no-turbopack --ts --tailwind --eslint --app --use-yarn && \
cd ..
These are a couple of additional dependencies needed for this project:
Dependency | Purpose |
---|---|
Headless UI | To provide components like its dialog, tabs, and textarea |
Heroicons | For icons |
MDX Editor | To edit markdown/rich text |
Formik | For various forms used throughout the application |
Jose | For session encryption and decryption |
Marked | To display markdown/rich text as HTML |
React paginate | For pagination |
Yup | For schema validation |
Install them with this command:
yarn workspace quora-frontend add @headlessui/react @heroicons/react @mdxeditor/editor formik jose marked react-paginate && yarn workspace quora-frontend add yup -D
Lastly, include the shared strapi-types package to the apps/quora-frontend/package.json:
1 "devDependencies": {
2 ...
3 "strapi-types": "*"
4 }
To truly mimic the Quora front-end, these are a couple of imageassets you will need.
Asset | Purpose |
---|---|
login.webp | The background image used on the login page. |
bot.webp | The profile picture that represents the AI bot. |
You can copy them to the apps/quora-frontend/public
folder from this Download Directory link.
Copy the Quora favicon as well to apps/quora-frontend/app
from this link.
Create a .env
file:
touch apps/quora-frontend/.env
Add these two env vars to the new apps/quora-frontend/.env
file:
1SESSION_SECRET={THE SESSION_SECRET}
2NEXT_PUBLIC_STRAPI_URL=http://localhost:1337
The NEXT_PUBLIC_STRAPI_URL
environment variable is used by the app's actions to make requests to Strapi and the SESSION_SECRET
to encrypt the session.
You can create a session secret by running:
openssl rand -hex 32
Modify the metadata
exported constant in apps/quora-frontend/layout.ts
to this to better represent the site:
1export const metadata: Metadata = {
2 title: "Quora Clone",
3 description: "A Quora clone built with Strapi",
4}
Next.js sets it as a boilerplate title and description which don't match what Quora has.
lib
FolderSeveral utilities, schemas, and models are used throughout the front-end.x
They are all placed in the apps/quora-frontend/app/lib
.
If all these are covered in detail here, it may make the tutorial unnecessarily long.
So let's just cover what they do and you can copy them from Download Directory here or you can view the source here on the Github repo.
File | Description |
---|---|
lib/dal.ts | Data access layer for authorization-related logic and requests. |
lib/client-request.ts | Utilities for requests made on the client. |
lib/request.ts | Utilities for server requests. |
lib/sessions.ts | Session logic and utilities. |
lib/definitions/content-types.ts | Strapi content type models. |
lib/definitions/request.ts | Request related models. |
lib/definitions/schemas/account.ts | Form validation schemas for account creation and modification. |
lib/definitions/schemas/auth.ts | Form validation schemas for authentication. |
lib/definitions/schemas/content-types.ts | Form validation schemas for Strapi content creation and modification. |
Server actions handle submissions to Strapi and data mutations.
Similar to the above section, adding these here would lengthen the tutorial.
So here's a breakdown of what each of the files under apps/quora-frontend/app/actions
does. Download the content of this directory at this link or view it on Github here.
File | Request utilities for the |
---|---|
actions/answers.ts | answers content type API |
actions/auth.ts | authentication API |
actions/bot-answers.ts | bot answers content type API |
actions/comments.ts | comments content type API |
actions/questions.ts | questions content type API |
actions/users.ts | users and permission plugin API |
actions/votes.ts | votes content type API |
The apps/quora-frontend/app/ui
folder holds all the components used throughout this app.
We will also not cover their actual contents here, but rather just what they do for brevity.
Same as above, you can download this entire folder at this link through Download Directory or view its contents on Github here.
Component File | Use |
---|---|
Profile components | |
account/profile/credential-form.tsx | Modifies the user's credential |
account/profile/delete-dialog.tsx | Account deletion confirmation dialog |
account/profile/email-form.tsx | User email modification form |
account/profile/name-form.tsx | Modification form for User's actual name |
account/profile/password-form.tsx | Password reset form |
account/profile/username-form.tsx | Username modification form |
Account page tab components | |
account/tabs/answers.tsx | The answers tab displayed on the account page that shows a user's answers |
account/tabs/comments.tsx | User's comments tab for the account page |
account/tabs/profile.tsx | User's profile tab for the account page |
account/tabs/questions.tsx | User's questions tab for the account page |
account/tabs/votes.tsx | User's votes tab for the account page |
General account components | |
account/modify-actions-card.tsx | Card used to modify or delete user-generated content (questions, answers, comments, etc.) |
account/subject-card.tsx | Card used to show subjects related to a user's content (e.g. question under a user's answer) |
Component File | Use |
---|---|
Answer components | |
shared/answers/card.tsx | Displays an answer or a bot answer |
shared/answers/input-form.tsx | Input form for an answer |
Comment components | |
shared/comments/comment-card.tsx | Displays a single comment |
shared/comments/comment-group.tsx | Shows a group of comments |
shared/comments/comments-button.tsx | Shows the comment count and reveals a comment section when clicked |
shared/comments/create-form.tsx | For creating a comment |
Editor components | |
shared/editor/ForwardRefEditor.tsx | Reference to the MDX editor with SSR disabled |
shared/editor/InitializedMDXEditor.tsx | To initialize the MDX editor used for answers |
shared/editor/index.tsx | Cleaned up MDX editor export |
Header components | |
shared/header/account-button.tsx | Link button to account page |
shared/header/index.tsx | The complete page header |
shared/header/login-button.tsx | Link to login page |
shared/header/logo.tsx | Shows app text logo |
shared/header/logout-button.tsx | Logout button |
shared/header/menu-button-container.tsx | Container for header menu items |
shared/header/menu-buttons.tsx | Links to various app pages |
shared/header/mobile-menu.tsx | Mobile version of the header menu |
shared/header/question-button.tsx | Launches question input form |
Question components | |
shared/questions/card.tsx | Displays a question |
shared/questions/input-form.tsx | Question input form |
Vote components | |
shared/votes/downvote-button.tsx | Downvote button |
shared/votes/vote-button.tsx | Combined upvote and downvote button |
shared/votes/vote-mod-button.tsx | Button for vote modification and deletion |
General shared components | |
shared/dialog.tsx | General dialog |
shared/error-message.tsx | General error message |
shared/header-container.tsx | Container with header on top |
shared/hr.tsx | General horizontal line |
shared/input-form.tsx | General input form |
shared/pagination.tsx | Pagination component |
The Quora clone contains five pages:
Let's dive in and see how to create each.
On the Quora login page, the user can sign up for a new account or login to their existing account. Start by creating the page:
mkdir -p apps/quora-frontend/app/login && touch apps/quora-frontend/app/login/page.tsx apps/quora-frontend/app/login/styles.module.css
This is what apps/quora-frontend/app/login/styles.module.css
contains:
1.background {
2 background-image: url('../../public/login.webp');
3 background-size: cover;
4}
This sets the background image copied earlier from Github for the login page.
Here's the apps/quora-frontend/app/login/page.tsx
file:
1'use client'
2
3import { ErrorMessage, Field, Form, Formik } from "formik"
4import styles from './styles.module.css'
5import { LoginSchema, SignupSchema } from "@/app/lib/definitions/schemas/auth"
6import { login, signup } from "@/app/actions/auth"
7import { useEffect, useState } from "react"
8import { getAllSearchParams } from "../lib/client-request"
9
10function Login() {
11 const [signupError, setSignupError] = useState('')
12 const [loginError, setLoginError] = useState('')
13
14 const fromLink = getAllSearchParams()['from']
15
16 const fieldClasses = 'bg-neutral-900 border border-neutral-700 hover:border-blue-700 rounded px-1 py-1.5 my-1'
17
18 useEffect(() => {
19 document.title = 'Login - Quora Clone'
20 }, [])
21
22 return <div className={`w-100 min-h-dvh flex flex-col items-center justify-center border-red-950 ${styles.background}`}>
23 <div className="bg-neutral-800 flex flex-col items-center justify-center p-6 rounded-lg">
24 <h1 className="text-red-600 text-6xl font-semibold font-serif tracking-tight mb-2 text-center">Quora Clone</h1>
25 <h6 className="font-bold text-center">A place to share knowledge and better understand the world</h6>
26 <div className="flex p-6 pt-10 flex-col lg:flex-row">
27 <Formik
28 initialValues={{ username: '', password: '', name: '', email: '' }}
29 validationSchema={SignupSchema}
30 onSubmit={async (userInfo) => {
31 setSignupError('')
32 const res = await signup(userInfo)
33 if (res && res.error) {
34 setSignupError(res?.error?.message || 'There was a problem signing you up')
35 }
36 }}
37 >
38 {({ errors, touched }) => (
39 <Form
40 className="flex flex-col min-w-64 max-lg:mb-8 max-lg:pb-10 max-lg:border-b max-lg:border-neutral-700"
41 >
42 <h4 className="text-base font-bold border-b border-neutral-700 pb-1.5 mb-1.5">Signup</h4>
43 <label className="text-sm font-bold" htmlFor="name">Name</label>
44 <Field id="name" name="name" placeholder="Your first name" className={fieldClasses} />
45 <ErrorMessage name="name" />
46 <label className="text-sm font-bold" htmlFor="username">Username</label>
47 <Field id="username" name="username" placeholder="Your username" className={fieldClasses} />
48 <ErrorMessage name="username" />
49 <label className="text-sm font-bold" htmlFor="email">Email</label>
50 <Field id="email" name="email" type="email" placeholder="Your email" className={fieldClasses} />
51 <ErrorMessage name="email" />
52 <label className="text-sm font-bold" htmlFor="password">Password</label>
53 <Field id="password" name="password" type="password" placeholder="Your password" className={fieldClasses} />
54 <ErrorMessage name="password" />
55 <button className="rounded bg-blue-600 text-white rounded-xl px-3 py-2 mt-1.5 self-end" type="submit">Signup</button>
56 {signupError && <div className="text-white bg-red-500 font-semibold p-2 mt-3 rounded-md">{signupError}</div>}
57 </Form>
58 )}
59 </Formik>
60 <Formik
61 initialValues={{ password: '', identifier: '' }}
62 validationSchema={LoginSchema}
63 onSubmit={async (credentials: { identifier: string, password: string }) => {
64 setLoginError('')
65
66 const res = await login(credentials, fromLink)
67
68 if (res) {
69 setLoginError(res?.error?.message || 'There was a problem logging you in')
70 }
71 }}
72 >
73 {({ errors, touched }) => (
74 <Form
75 className="flex flex-col lg:ml-8 lg:pl-8 lg:border-s lg:border-neutral-700 min-w-64"
76 >
77 <h4 className="text-base font-bold border-b border-neutral-700 pb-1.5 mb-1.5">Login</h4>
78 <label className="text-sm font-bold" htmlFor="login-identifier">Email</label>
79 <Field id="login-identifier" name="identifier" type="email" placeholder="Your email" className={fieldClasses} />
80 <ErrorMessage name="identifier" />
81 <label className="text-sm font-bold" htmlFor="login-password">Password</label>
82 <Field id="login-password" name="password" type="password" placeholder="Your password" className={fieldClasses} />
83 <ErrorMessage name="password" />
84 <button className="rounded bg-blue-600 text-white rounded-xl px-3 py-2 mt-1.5 self-end" type="submit">Login</button>
85 {loginError && <div className="text-white bg-red-500 font-semibold p-2 mt-3 rounded-md">{loginError}</div>}
86 </Form>
87 )}
88 </Formik>
89 </div>
90 </div>
91 </div>
92}
93
94export default Login
The completed login page.
On the home page, a paginated list of questions and their top-voted answers are displayed.
If a question lacks a user-written answer, an AI-generated answer is shown instead.
A user can comment on the question or its selected answer and upvote or downvote either.
Replace the contents of apps/quora-frontend/app/page.tsx
with:
1'use client'
2
3import HeaderContainer from "@/app/ui/shared/header-container"
4import { useEffect, useRef, useState } from "react"
5import { Answer } from "@/app/lib/definitions/content-types"
6import { getHomeAnswers, getHomeAnswersCount } from "@/app/actions/answers"
7import AnswerCard from "@/app/ui/shared/answers/card"
8import Pagination, { SelectedItem } from "./ui/shared/pagination"
9import { isLoggedIn } from "./actions/auth"
10import ErrorMessage from "./ui/shared/error-message"
11
12export default function Home() {
13 const pageSize = 5
14
15 const [ready, setReady] = useState(false)
16 const [answerCount, setAnswerCount] = useState(1)
17 const [isUserLoggedIn, setIsUserLoggedIn] = useState(false)
18 const [answers, setAnswers] = useState([] as Answer[])
19 const topRef = useRef<any>(null)
20
21 useEffect(() => {
22 isLoggedIn().then(loggedIn => {
23 setIsUserLoggedIn(loggedIn)
24 })
25
26 getHomeAnswers(1, pageSize).then(answ => {
27 if (answ?.error) {
28 setAnswers([])
29 } else {
30 setAnswers(answ)
31 setReady(true)
32 }
33 })
34
35 getHomeAnswersCount().then(cnt => {
36 setAnswerCount(cnt.count)
37 })
38 }, [])
39
40 const fetchAnswers = (selectedItem: SelectedItem) => {
41 const selectedPage = selectedItem.selected + 1
42
43 getHomeAnswers(selectedPage, pageSize).then(answ => {
44 if (answ?.error) {
45 setAnswers([])
46 } else {
47 setAnswers(answ)
48
49 const top = topRef.current.offsetTop - 110
50 window.scrollTo({
51 top,
52 behavior: "smooth"
53 })
54 }
55 })
56 }
57
58 return (
59 <HeaderContainer>
60 <div
61 ref={topRef}
62 className="flex flex-col mt-5 w-full items-center max-w-[700px] self-center"
63 >
64 {!!answers.length ?
65 <>
66 {
67 answers.map((answ, index) => (
68 <AnswerCard
69 answer={answ}
70 key={`${answ.id}-${answ.documentId}-${index}`}
71 minimal={false}
72 isUserLoggedIn={isUserLoggedIn}
73 />
74 ))
75 }
76 {
77 (ready && answerCount > pageSize)
78 &&
79 <Pagination fetchNext={fetchAnswers} noItems={answerCount} pageSize={pageSize} />
80 }
81 </> : <ErrorMessage message="No questions posted yet" />}
82 </div>
83 </HeaderContainer >
84 )
85}
The home page.
The Ask-a-question dialog.
The Answer-a-question dialog.
On this page, a list of questions together with their upvote count and total number of answers are displayed. A user can pick one and answer it from here. No answers are displayed.
The purpose of this page is to encourage users to answer questions. Downvotes and comments are enabled on these questions.
To create this page, use:
mkdir -p apps/quora-frontend/app/answer && touch apps/quora-frontend/app/answer/page.tsx
The file apps/quora-frontend/app/answer/page.tsx
contains:
1'use client'
2
3import { useEffect, useRef, useState } from "react"
4import HeaderContainer from "../ui/shared/header-container"
5import { getHomeQuestion, getQuestionCount } from "../actions/questions"
6import { Question } from "../lib/definitions/content-types"
7import QuestionCard from "../ui/shared/questions/card"
8import Pagination, { SelectedItem } from "../ui/shared/pagination"
9import { useRouter } from "next/navigation"
10import { isLoggedIn } from "../actions/auth"
11import ErrorMessage from "../ui/shared/error-message"
12
13export default function AnswerPage() {
14 const pageSize = 5
15
16 const [ready, setReady] = useState(false)
17 const [questions, setQuestions] = useState([])
18 const [questionCount, setQuestionCount] = useState(0)
19 const [page, setPage] = useState(1)
20 const topRef = useRef<any>(null)
21 const router = useRouter()
22
23 const [isUserLoggedIn, setIsUserLoggedIn] = useState(false)
24
25
26 useEffect(() => {
27 isLoggedIn().then(loggedIn => {
28 setIsUserLoggedIn(loggedIn)
29 })
30
31 getHomeQuestion(page, pageSize).then(qsts => {
32 if (!qsts.error) {
33 setQuestions(qsts)
34 setReady(true)
35 }
36 })
37
38 getQuestionCount().then(qc => {
39 if (!qc.error) {
40 setQuestionCount(qc.count)
41 }
42 })
43 }, [])
44
45 const fetchQuestions = (selectedItem: SelectedItem) => {
46 const sp = selectedItem.selected + 1
47
48 getHomeQuestion(sp, pageSize).then(qsts => {
49 if (!qsts.error) {
50 setQuestions(qsts)
51 setPage(sp)
52
53 const top = topRef.current.offsetTop - 110
54 window.scrollTo({
55 top,
56 behavior: "smooth"
57 })
58 }
59 })
60 }
61
62 return <HeaderContainer>
63 {
64 questions.length > 0 ?
65 (ready &&
66 <div
67 ref={topRef}
68 className="flex flex-col mt-5 w-full items-center max-w-[700px] self-center"
69 >
70
71 {(questions as Question[]).map((question, index) => (
72 <QuestionCard
73 question={question}
74 key={`${question.id}-${question.documentId}-${index}`}
75 commentsAvailable={true}
76 answerCountAvailable={true}
77 refreshAnswers={() => {
78 router.push(`/question/${question.id}`)
79 router.refresh()
80 }}
81 isUserLoggedIn={isUserLoggedIn}
82 />
83 ))}
84 {
85 questionCount > pageSize
86 &&
87 <Pagination fetchNext={fetchQuestions} pageSize={pageSize} noItems={questionCount} />
88 }
89
90 </div>
91 )
92 :
93 <ErrorMessage message="No questions to answer posted yet" />
94 }
95 </HeaderContainer>
96}
The answer page.
This page is reserved for individual questions.
The question and its related paginated answers are displayed as well as the bot-generated answer.
You can comment on the question and its answers and upvote and downvote them as well.
You can also view comments others have left on the question or its answers.
Make this page with:
mkdir -p apps/quora-frontend/app/question/[id] && touch apps/quora-frontend/app/question/[id]/page.tsx
Add this to the apps/quora-frontend/app/question/[id]/page.tsx
file:
1'use client'
2
3import { useEffect, useRef, useState } from "react"
4import { Answer, BotAnswer, Question as QuestionCT } from "@/app/lib/definitions/content-types"
5import { getQuestionBotAnswer, getQuestionWithAnswers } from "@/app/actions/questions"
6import HeaderContainer from "@/app/ui/shared/header-container"
7import AnswerCard from "@/app/ui/shared/answers/card"
8import Pagination, { SelectedItem } from "@/app/ui/shared/pagination"
9import QuestionCard from "@/app/ui/shared/questions/card"
10import { isLoggedIn } from "@/app/actions/auth"
11import ErrorMessage from "@/app/ui/shared/error-message"
12
13export default function Question({ params: { id } }: { params: { id: string } }) {
14 const pageSize = 4
15
16 const [question, setQuestion] = useState(null)
17 const [ready, setReady] = useState(false)
18 const [page, setPage] = useState(1)
19 const [isUserLoggedIn, setIsUserLoggedIn] = useState(false)
20 const [botAnswer, setBotAnswer] = useState(null)
21 const topRef = useRef<any>(null)
22
23 useEffect(() => {
24 isLoggedIn().then(loggedIn => {
25 setIsUserLoggedIn(loggedIn)
26 })
27
28 getQuestionWithAnswers(Number(id), page, pageSize)
29 .then((res) => {
30 if (!res.error) {
31 setQuestion(res)
32 setReady(true)
33 }
34 return getQuestionBotAnswer(id)
35 })
36 .then((res) => {
37 if (!res.error) {
38 setBotAnswer(res[0])
39 }
40 })
41 }, [])
42
43 const fetchAnswers = (selectedItem: SelectedItem) => {
44 const selectedPage = selectedItem.selected + 1
45
46 getQuestionWithAnswers(Number(id), selectedPage, pageSize).then(res => {
47 if (!res.error) {
48 setQuestion(res)
49 setPage(selectedPage)
50
51 const top = topRef.current.offsetTop - 110
52 window.scrollTo({
53 top,
54 behavior: "smooth"
55 })
56 }
57 })
58 }
59
60 const refreshAnswers = () => {
61 getQuestionWithAnswers(Number(id), 1, pageSize).then(res => {
62 if (!res.error) {
63 setQuestion(res)
64 setPage(1)
65
66 const top = topRef.current.offsetTop - 110
67 window.scrollTo({
68 top,
69 behavior: "smooth"
70 })
71 }
72 })
73 }
74
75 return <HeaderContainer>
76 {question ?
77 (ready &&
78 <div
79 ref={topRef}
80 className="flex flex-col mt-5 w-full items-center max-w-[700px] self-center"
81 >
82 <QuestionCard
83 question={question}
84 refreshAnswers={refreshAnswers}
85 isUserLoggedIn={isUserLoggedIn}
86 commentsAvailable={true}
87 />
88 {botAnswer && <AnswerCard
89 answer={botAnswer}
90 key={`${(botAnswer as BotAnswer)?.id}-${(botAnswer as BotAnswer)?.documentId}`}
91 minimal={false}
92 isUserLoggedIn={isUserLoggedIn}
93 isBot={true}
94 />}
95 {((question as QuestionCT)?.answers as Answer[]).length ? ((question as QuestionCT)?.answers as Answer[]).map((answ, index) => (
96 <AnswerCard
97 answer={answ}
98 key={`${answ.id}-${answ.documentId}-${index}`}
99 minimal={false}
100 isUserLoggedIn={isUserLoggedIn}
101 />
102 )) :
103 <div className="text-center h-36 text-2xl font-thin self-stretch rounded-2xl p-3 flex justify-center items-center bg-white/5">
104 No answer has been posted by a human yet
105 </div>
106 }
107 {
108 (question as QuestionCT).answerCount as number > pageSize
109 &&
110 <Pagination fetchNext={fetchAnswers} pageSize={pageSize} noItems={(question as QuestionCT).answerCount as number} />
111 }
112 </div>
113 ) :
114 <ErrorMessage
115 message="Question not found"
116 linkText="Go home"
117 linkHref="/"
118 />
119 }
120 </HeaderContainer>
121}
The question page
On the account page, a user can: 1. Modify their account details 2. Reset their password 3. Modify or delete their answers 3. Modify or delete their questions 3. Modify or delete their comments 3. Modify or delete their votes
You can check out what each of the tabs do at this link.
Create this page by running:
mkdir -p apps/quora-frontend/app/account && touch apps/quora-frontend/app/account/page.tsx
Cope this to the apps/quora-frontend/app/account/page.tsx
file:
1'use client'
2
3import { TabGroup, TabList, Tab, TabPanels, TabPanel } from "@headlessui/react"
4import HeaderContainer from "@/app/ui/shared/header-container"
5import ProfileTab from "@/app/ui/account/tabs/profile"
6import AnswersTab from "@/app/ui/account/tabs/answers"
7import QuestionsTab from "@/app/ui/account/tabs/questions"
8import CommentsTab from "@/app/ui/account/tabs/comments"
9import VotesTab from "@/app/ui/account/tabs/votes"
10
11function Account() {
12 const tabs = [
13 { name: 'Profile', component: <ProfileTab /> },
14 { name: 'Questions', component: <QuestionsTab /> },
15 { name: 'Answers', component: <AnswersTab /> },
16 { name: 'Comments', component: <CommentsTab /> },
17 { name: 'Votes', component: <VotesTab /> }
18 ]
19
20 return (
21 <HeaderContainer>
22 <div className="flex flex-col max-w-[700px] w-full self-center">
23 <TabGroup className="pt-8 w-full">
24 <TabList className="flex gap-4 overflow-x-auto">
25 {tabs.map(({ name }) => (
26 <Tab
27 key={name}
28 className="mb-3 rounded-full py-1 px-3 text-sm/6 font-semibold text-white focus:outline-none data-[selected]:bg-white/10 data-[hover]:bg-white/5 data-[selected]:data-[hover]:bg-white/10 data-[focus]:outline-1 data-[focus]:outline-white"
29 >
30 {name}
31 </Tab>
32 ))}
33 </TabList>
34 <TabPanels className="mt-3 w-full">
35 {tabs.map(({ name, component }) => (
36 <TabPanel key={name} className="rounded-xl bg-white/5 flex flex-col p-6">
37 {name != tabs[0].name && <p className="text-2xl font-light mb-3">Your {name}</p>}
38 {component}
39 </TabPanel>
40 ))}
41 </TabPanels>
42 </TabGroup>
43 </div>
44 </HeaderContainer>
45 )
46}
47
48export default Account
The profile tab of the account page.
The questions tab of the account page.
The answers tab of the account page
The comments tab of the account page.
The votes tab of the account page.
On Quora, only the question and login pages are accessible if a user is logged out. So add a middleware to protect against unauthenticated access to the rest of the pages.
Create the middleware file:
touch apps/quora-frontend/middleware.ts
Add this to the apps/quora-frontend/middleware.ts
file:
1import { NextResponse } from 'next/server'
2import type { NextRequest } from 'next/server'
3import { decrypt } from './app/lib/sessions'
4import { SessionPayload } from './app/lib/definitions/request'
5
6export async function middleware(request: NextRequest) {
7 const loginUrl = new URL('/login', request.url)
8 loginUrl.searchParams.set('from', request.nextUrl.pathname)
9
10 try {
11 const cookie = request.cookies.get('session')?.value
12 const session = (await decrypt(cookie)) as SessionPayload
13
14 if (!session) {
15 return NextResponse.redirect(loginUrl)
16 }
17
18 if (session?.expiresAt) {
19 if (session?.expiresAt < (new Date())) {
20 return NextResponse.redirect(loginUrl)
21 }
22 }
23 } catch {
24 return NextResponse.redirect(loginUrl)
25 }
26}
27
28export const config = {
29 matcher: [
30 '/',
31 '/account',
32 '/answer',
33 ]
34}
To run the whole project at once, use the turbo dev
command at the project root.
You can check out the source code for this project here.
🎉 You've made it to the end of this tutorial. A big congratulations if you followed each step and have a working project at the end.
You were able to create a monorepo, set up a Strapi back-end, add necessary content types, make customizations to the content type and plugin REST APIs, and build a whole front-end that mimics Quora.
Strapi is a headless content management system that takes a lot of the pain out of creating and handling your online content. It provides a ready-to-use admin panel to add and customize multi-format content, plugins that manage users and their roles, and automatically generates an API for each type of content to make consuming it extremely smooth.
Interested in learning more about Strapi? Check out their quick start guide at this link or join the Strapi Discord community.
I am a developer and writer passionate about performance optimization, user-centric designs, and scalability.