Skip to content

Commit 10de357

Browse files
author
Alex Patterson
authored
Merge pull request CodingCatDev#156 from CodingCatDev/feature/authors
Feature/authors
2 parents 6b196f3 + cc16b71 commit 10de357

10 files changed

Lines changed: 376 additions & 54 deletions

File tree

backend/firebase/firestore.indexes.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,24 @@
1414
}
1515
]
1616
},
17+
{
18+
"collectionGroup": "posts",
19+
"queryScope": "COLLECTION",
20+
"fields": [
21+
{
22+
"fieldPath": "authorIds",
23+
"arrayConfig": "CONTAINS"
24+
},
25+
{
26+
"fieldPath": "type",
27+
"order": "ASCENDING"
28+
},
29+
{
30+
"fieldPath": "publishedAt",
31+
"order": "DESCENDING"
32+
}
33+
]
34+
},
1735
{
1836
"collectionGroup": "posts",
1937
"queryScope": "COLLECTION",

backend/firebase/functions/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export { newUserSetup } from './user/user';
33
export { onPostWriteSearch, onPostDeleteSearch } from './search/algolia';
44
export {
55
onPostCreate,
6-
onPostWrite,
6+
onPostUpdateAuthors,
77
onPostDelete,
88
onHistoryWrite,
99
} from './posts/posts';

backend/firebase/functions/src/posts/posts.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ export const onPostCreate = functions.firestore
1616
const author = await getPostCreatorProfile(post);
1717

1818
const authors = [];
19-
19+
const authorIds = [];
2020
if (author) {
2121
authors.push(author);
22+
authorIds.push(author);
2223
}
2324

2425
// Rules should require that all posts have a status of
@@ -29,6 +30,7 @@ export const onPostCreate = functions.firestore
2930
id,
3031
postId: context.params.postId,
3132
authors,
33+
authorIds,
3234
accessSettings: {
3335
accessMode: 'closed',
3436
},
@@ -40,11 +42,46 @@ export const onPostCreate = functions.firestore
4042
.set({ historyId: id }, { merge: true });
4143
});
4244

43-
export const onPostWrite = functions.firestore
45+
export const onPostUpdateAuthors = functions.firestore
4446
.document('posts/{postId}')
45-
.onWrite(async (snap, context) => {
46-
//Keep function
47-
return true;
47+
.onUpdate(async (change, context) => {
48+
// Retrieve the current and previous value
49+
const data = change.after.data();
50+
51+
// If there are no authors just return
52+
if (data.authors.length === 0) {
53+
return null;
54+
}
55+
56+
const authorIds: string[] = [];
57+
let noUpdate = true;
58+
59+
// Force Update if this doesn't exist.
60+
if (!data.authorIds || data.authorIds.length === 0) {
61+
noUpdate = false;
62+
}
63+
64+
data.authors.forEach((author: any) => {
65+
authorIds.push(author.uid);
66+
if (noUpdate) {
67+
noUpdate = data.authorIds.includes(author.uid);
68+
console.log('Author ID not found needs updated', author.uid);
69+
}
70+
});
71+
72+
// Nothing needs update exit function
73+
if (noUpdate) {
74+
console.log('Nothing needed for update');
75+
return null;
76+
}
77+
78+
// Make update
79+
return change.after.ref.set(
80+
{
81+
authorIds,
82+
},
83+
{ merge: true }
84+
);
4885
});
4986

5087
export const onPostDelete = functions.firestore
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { UserInfoExtended } from '@/models/user.model';
2+
3+
export default function AuthorCard({
4+
author,
5+
}: {
6+
author: UserInfoExtended;
7+
}): JSX.Element {
8+
return (
9+
<article className="grid items-start max-w-md grid-cols-1 gap-4 p-4 shadow-lg justify-items-center justify-self-center bg-basics-50 text-basics-900 hover:text-basics-900 hover:shadow-sm">
10+
{author?.displayName && author?.photoURL ? (
11+
<img
12+
className="rounded-full"
13+
src={author.photoURL}
14+
alt={author.displayName}
15+
/>
16+
) : (
17+
<img
18+
className="w-24 rounded-full"
19+
src="/static/images/avatar.png"
20+
alt="Avatar Image Placeholder"
21+
/>
22+
)}
23+
<>
24+
<h3 className="font-sans text-3xl lg:text-4xl">{author.displayName}</h3>
25+
<p className="text-base lg:text-lg">{author.basicInfo?.about}</p>
26+
</>
27+
</article>
28+
);
29+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { UserInfoExtended } from '@/models/user.model';
2+
import Link from 'next/link';
3+
import AuthorCard from './AuthorCard';
4+
5+
export default function Authors({
6+
authors,
7+
}: {
8+
authors: UserInfoExtended[];
9+
}): JSX.Element {
10+
return (
11+
<section className="grid gap-4 justify-items-center">
12+
<section className="max-w-2xl text-center">
13+
<h2 className="mb-2 text-4xl lg:text-5xl">Authors & Instructors</h2>
14+
<h3 className="font-sans text-2xl">
15+
Our wonderful team of humans who are here to help you become a{' '}
16+
<span className="font-heading text-secondary-600 dark:text-secondary-600">
17+
Purrfect
18+
</span>{' '}
19+
developer.
20+
</h3>
21+
</section>
22+
<section className="flex flex-wrap items-start justify-center w-full gap-10">
23+
{authors.map((author, i) => (
24+
<Link href={`/authors/${author.uid}`} key={i}>
25+
<a>
26+
<AuthorCard author={author} />
27+
</a>
28+
</Link>
29+
))}
30+
</section>
31+
</section>
32+
);
33+
}

frontend/main/src/models/post.model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface Post {
2929
coverVideo?: CoverMedia;
3030
sections?: Section[];
3131
authors?: UserInfoExtended[];
32+
authorIds?: string[]; //uid list of authors
3233

3334
// Payment Fields
3435
accessSettings?: AccessSettings;
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import http from 'http';
2+
import {
3+
getSite,
4+
getAuthorProfile,
5+
postsByUser,
6+
} from '@/services/serversideApi';
7+
import { NextSeo } from 'next-seo';
8+
import Layout from '@/layout/Layout';
9+
import { Site } from '@/models/site.model';
10+
import { UserInfoExtended } from '@/models/user.model';
11+
import { Post, PostType } from '@/models/post.model';
12+
import PostsCards from '@/components/PostsCards';
13+
import AuthorCard from '@/components/authors/AuthorCard';
14+
15+
export default function AuthorPage({
16+
site,
17+
author,
18+
courses,
19+
tutorials,
20+
posts,
21+
}: {
22+
site: Site | null;
23+
author: UserInfoExtended;
24+
courses: Post[] | null;
25+
tutorials: Post[] | null;
26+
posts: Post[] | null;
27+
}): JSX.Element {
28+
return (
29+
<Layout site={site}>
30+
<NextSeo
31+
title={`${author.displayName ? author.displayName : ''} | CodingCatDev`}
32+
canonical={`https://codingcat.dev/authors/`}
33+
></NextSeo>
34+
<section className="grid grid-cols-1 gap-20 p-4 sm:p-10 place-items-center">
35+
<AuthorCard author={author} />
36+
</section>
37+
{courses && courses.length > 0 && (
38+
<section className="grid w-full gap-10 px-4 mx-auto xl:px-10">
39+
<h2 className="mt-4 text-4xl text-primary-900 lg:text-5xl">
40+
Courses
41+
</h2>
42+
{courses && <PostsCards posts={courses} />}
43+
</section>
44+
)}
45+
{tutorials && tutorials.length > 0 && (
46+
<section className="grid w-full gap-10 px-4 mx-auto xl:px-10">
47+
<h2 className="mt-4 text-4xl text-primary-900 lg:text-5xl">
48+
Tutorials
49+
</h2>
50+
{tutorials && <PostsCards posts={tutorials} />}
51+
</section>
52+
)}
53+
{posts && posts.length > 0 && (
54+
<section className="grid w-full gap-10 px-4 mx-auto xl:px-10">
55+
<h2 className="mt-4 text-4xl text-primary-900 lg:text-5xl">
56+
Blog Posts
57+
</h2>
58+
{posts && <PostsCards posts={posts} />}
59+
</section>
60+
)}
61+
</Layout>
62+
);
63+
}
64+
65+
export async function getServerSideProps({
66+
params,
67+
req,
68+
}: {
69+
params: { authorPath: string };
70+
req: http.IncomingMessage;
71+
}): Promise<
72+
| {
73+
props: {
74+
site: Site | null;
75+
author: UserInfoExtended | null;
76+
courses: Post[] | null;
77+
tutorials: Post[] | null;
78+
posts: Post[] | null;
79+
};
80+
}
81+
| { redirect: { destination: string; permanent: boolean } }
82+
| { notFound: boolean }
83+
> {
84+
const { authorPath } = params;
85+
86+
if (!authorPath) {
87+
return {
88+
notFound: true,
89+
};
90+
}
91+
const site = await getSite();
92+
const author = (await getAuthorProfile(authorPath)) as UserInfoExtended;
93+
const courses = await postsByUser(PostType.course, authorPath);
94+
const tutorials = await postsByUser(PostType.tutorial, authorPath);
95+
const posts = await postsByUser(PostType.post, authorPath);
96+
97+
if (!author) {
98+
console.log('Author not found');
99+
return {
100+
notFound: true,
101+
};
102+
}
103+
104+
return {
105+
props: {
106+
site,
107+
author,
108+
courses,
109+
tutorials,
110+
posts,
111+
},
112+
};
113+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { NextSeo } from 'next-seo';
2+
import Layout from '@/layout/Layout';
3+
import Authors from '@/components/authors/Authors';
4+
5+
import { getAuthorProfiles, getSite } from '@/services/serversideApi';
6+
import { Site } from '@/models/site.model';
7+
import { UserInfoExtended } from '@/models/user.model';
8+
9+
export default function AuthorsPage({
10+
site,
11+
authors,
12+
}: {
13+
site: Site | null;
14+
authors: UserInfoExtended[];
15+
}): JSX.Element {
16+
return (
17+
<Layout site={site}>
18+
<NextSeo
19+
title="Authors | CodingCatDev"
20+
canonical={`https://codingcat.dev/authors/`}
21+
></NextSeo>
22+
<section className="grid grid-cols-1 gap-10 p-4 sm:p-10 place-items-center">
23+
<h1 className="text-5xl lg:text-7xl">Authors</h1>
24+
<Authors authors={authors} />
25+
</section>
26+
</Layout>
27+
);
28+
}
29+
30+
export async function getStaticProps(): Promise<{
31+
props: {
32+
site: Site | null;
33+
authors: UserInfoExtended[];
34+
};
35+
revalidate: number;
36+
}> {
37+
const site = await getSite();
38+
const allAuthors = await getAuthorProfiles();
39+
40+
// Return only authors with Profile Data
41+
const authors = allAuthors.filter((a) =>
42+
Object.keys(a).includes('basicInfo')
43+
);
44+
45+
return {
46+
props: {
47+
site,
48+
authors,
49+
},
50+
// Next.js will attempt to re-generate the page:
51+
// - When a request comes in
52+
// - At most once every second
53+
revalidate: 1, // In seconds
54+
};
55+
}

0 commit comments

Comments
 (0)