Skip to content

Commit f02a6d5

Browse files
author
Alex Patterson
authored
Merge pull request CodingCatDev#158 from CodingCatDev/feature/tag-pages
add tags
2 parents 10de357 + aa978ff commit f02a6d5

File tree

10 files changed

+355
-6
lines changed

10 files changed

+355
-6
lines changed

backend/firebase/firestore.indexes.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,24 @@
3232
}
3333
]
3434
},
35+
{
36+
"collectionGroup": "posts",
37+
"queryScope": "COLLECTION",
38+
"fields": [
39+
{
40+
"fieldPath": "tag",
41+
"arrayConfig": "CONTAINS"
42+
},
43+
{
44+
"fieldPath": "type",
45+
"order": "ASCENDING"
46+
},
47+
{
48+
"fieldPath": "publishedAt",
49+
"order": "DESCENDING"
50+
}
51+
]
52+
},
3553
{
3654
"collectionGroup": "posts",
3755
"queryScope": "COLLECTION",

backend/firebase/functions/package-lock.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/firebase/functions/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"cloudinary": "^1.23.0",
2020
"firebase-admin": "^9.2.0",
2121
"firebase-functions": "^3.11.0",
22+
"slugify": "^1.5.0",
2223
"uuid": "^8.3.2"
2324
},
2425
"devDependencies": {

backend/firebase/functions/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export {
1313
onSubscriptionCreate,
1414
onSubscriptionCancel,
1515
} from './stripe/subscriptions';
16+
export { onPostWriteTags, scheduledTagsUpdate } from './tags/tag';
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import * as functions from 'firebase-functions';
2+
import { firestore } from './../config/config';
3+
import * as admin from 'firebase-admin';
4+
import slugify from 'slugify';
5+
6+
export const scheduledTagsUpdate = functions.pubsub
7+
.schedule('every 1 hours')
8+
.onRun(async () => {
9+
await updateTags();
10+
});
11+
12+
export const onPostWriteTags = functions.firestore
13+
.document('posts/{postId}')
14+
.onUpdate((change) => {
15+
// Retrieve the current and previous value
16+
const data = change.after.data();
17+
18+
// If there are no tags just return
19+
if (!data.tags || data.tags.length === 0) {
20+
return null;
21+
}
22+
23+
return updateTags();
24+
});
25+
26+
export async function updateTags(): Promise<void> {
27+
const postDocs = await firestore
28+
.collection('posts')
29+
.where('tag', '!=', [])
30+
.orderBy('tag', 'asc')
31+
.get();
32+
33+
const tags = new Map();
34+
postDocs.forEach((postDoc) => {
35+
const post = postDoc.data();
36+
if (post.tag) {
37+
post.tag.forEach((tag: string) => {
38+
if (tags.has(tag)) {
39+
tags.set(tag, [...tags.get(tag), post.id]);
40+
} else {
41+
tags.set(tag, [post.id]);
42+
}
43+
});
44+
}
45+
});
46+
for (const [tag, posts] of tags.entries()) {
47+
console.log('Tag: ', tag);
48+
console.log(JSON.stringify(posts));
49+
50+
// Update all tags
51+
const slug = slugify(tag, { lower: true });
52+
await firestore.collection('tags').doc(slug).set(
53+
{
54+
posts,
55+
count: posts.length,
56+
tag: tag,
57+
slug: slug,
58+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
59+
},
60+
{ merge: true }
61+
);
62+
}
63+
}

frontend/main/src/components/home/Skills.tsx

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,43 @@ import {
66
CssLogo,
77
HtmlLogo,
88
} from '@/components/global/icons/VendorLogos';
9+
import Link from 'next/link';
910

1011
export default function Skills() {
1112
return (
1213
<>
1314
{/* These 3 are bigger in size, so they are h-8/16 */}
14-
<ReactLogo className="h-14 md:h-16 " />
15-
<AngularLogo className="h-14 md:h-16 " />
16-
<VueLogo className="h-14 md:h-16 " />
15+
<Link href="/tags/reactjs">
16+
<a>
17+
<ReactLogo className="h-14 md:h-16 " />
18+
</a>
19+
</Link>
20+
<Link href="/tags/angular">
21+
<a>
22+
<AngularLogo className="h-14 md:h-16 " />
23+
</a>
24+
</Link>
25+
<Link href="/tags/vue">
26+
<a>
27+
<VueLogo className="h-14 md:h-16 " />
28+
</a>
29+
</Link>
1730
{/* These 3 are smaller in size, so they are h-10/20 */}
18-
<SvelteLogo className="h-16 md:h-20 " />
19-
<CssLogo className="h-16 md:h-20 " />
20-
<HtmlLogo className="h-16 md:h-20 " />
31+
<Link href="/tags/svelte">
32+
<a>
33+
<SvelteLogo className="h-16 md:h-20 " />
34+
</a>
35+
</Link>
36+
<Link href="/tags/css">
37+
<a>
38+
<CssLogo className="h-16 md:h-20 " />
39+
</a>
40+
</Link>
41+
<Link href="/tags/html">
42+
<a>
43+
<HtmlLogo className="h-16 md:h-20 " />
44+
</a>
45+
</Link>
2146
</>
2247
);
2348
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import firebase from 'firebase/app';
2+
3+
export interface Tag {
4+
count: number;
5+
posts: string[];
6+
slug: string;
7+
tag: string;
8+
updatedAt?: firebase.firestore.Timestamp;
9+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import http from 'http';
2+
import { getSite, getTagBySlug, postsByTag } from '@/services/serversideApi';
3+
import { NextSeo } from 'next-seo';
4+
import Layout from '@/layout/Layout';
5+
import { Site } from '@/models/site.model';
6+
import { UserInfoExtended } from '@/models/user.model';
7+
import { Post, PostType } from '@/models/post.model';
8+
import PostsCards from '@/components/PostsCards';
9+
import AuthorCard from '@/components/authors/AuthorCard';
10+
import { Tag } from '@/models/tag.model';
11+
12+
export default function AuthorPage({
13+
site,
14+
tag,
15+
courses,
16+
tutorials,
17+
posts,
18+
podcasts,
19+
}: {
20+
site: Site | null;
21+
tag: Tag;
22+
courses: Post[] | null;
23+
tutorials: Post[] | null;
24+
posts: Post[] | null;
25+
podcasts: Post[] | null;
26+
}): JSX.Element {
27+
return (
28+
<Layout site={site}>
29+
<NextSeo
30+
title={`${tag.tag ? tag.tag : ''} | CodingCatDev`}
31+
canonical={`https://codingcat.dev/tag/${tag.slug}`}
32+
></NextSeo>
33+
<section className="grid grid-cols-1 gap-20 p-4 sm:p-10 place-items-center">
34+
<h1>{tag.tag}</h1>
35+
</section>
36+
{courses && courses.length > 0 && (
37+
<section className="grid w-full gap-10 px-4 mx-auto xl:px-10">
38+
<h2 className="mt-4 text-4xl text-primary-900 lg:text-5xl">
39+
Courses
40+
</h2>
41+
{courses && <PostsCards posts={courses} />}
42+
</section>
43+
)}
44+
{tutorials && tutorials.length > 0 && (
45+
<section className="grid w-full gap-10 px-4 mx-auto xl:px-10">
46+
<h2 className="mt-4 text-4xl text-primary-900 lg:text-5xl">
47+
Tutorials
48+
</h2>
49+
{tutorials && <PostsCards posts={tutorials} />}
50+
</section>
51+
)}
52+
{posts && posts.length > 0 && (
53+
<section className="grid w-full gap-10 px-4 mx-auto xl:px-10">
54+
<h2 className="mt-4 text-4xl text-primary-900 lg:text-5xl">
55+
Blog Posts
56+
</h2>
57+
{posts && <PostsCards posts={posts} />}
58+
</section>
59+
)}
60+
{podcasts && podcasts.length > 0 && (
61+
<section className="grid w-full gap-10 px-4 mx-auto xl:px-10">
62+
<h2 className="mt-4 text-4xl text-primary-900 lg:text-5xl">
63+
Podcasts
64+
</h2>
65+
{podcasts && <PostsCards posts={podcasts} />}
66+
</section>
67+
)}
68+
</Layout>
69+
);
70+
}
71+
72+
export async function getServerSideProps({
73+
params,
74+
req,
75+
}: {
76+
params: { tagPath: string };
77+
req: http.IncomingMessage;
78+
}): Promise<
79+
| {
80+
props: {
81+
site: Site | null;
82+
tag: Tag;
83+
courses: Post[] | null;
84+
tutorials: Post[] | null;
85+
posts: Post[] | null;
86+
podcasts: Post[] | null;
87+
};
88+
}
89+
| { redirect: { destination: string; permanent: boolean } }
90+
| { notFound: boolean }
91+
> {
92+
const { tagPath } = params;
93+
94+
if (!tagPath) {
95+
return {
96+
notFound: true,
97+
};
98+
}
99+
const site = await getSite();
100+
const tag = await getTagBySlug(tagPath);
101+
if (!tag) {
102+
return {
103+
notFound: true,
104+
};
105+
}
106+
107+
const courses = await postsByTag(PostType.course, tag.tag);
108+
const tutorials = await postsByTag(PostType.tutorial, tag.tag);
109+
const posts = await postsByTag(PostType.post, tag.tag);
110+
const podcasts = await postsByTag(PostType.podcast, tag.tag);
111+
112+
return {
113+
props: {
114+
site,
115+
tag,
116+
courses,
117+
tutorials,
118+
posts,
119+
podcasts,
120+
},
121+
};
122+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { NextSeo } from 'next-seo';
2+
import Layout from '@/layout/Layout';
3+
4+
import { getTags, getSite } from '@/services/serversideApi';
5+
import { Site } from '@/models/site.model';
6+
import { Tag } from '@/models/tag.model';
7+
import Link from 'next/link';
8+
9+
export default function AuthorsPage({
10+
site,
11+
tags,
12+
}: {
13+
site: Site | null;
14+
tags: Tag[];
15+
}): JSX.Element {
16+
return (
17+
<Layout site={site}>
18+
<NextSeo
19+
title="Tags | CodingCatDev"
20+
canonical={`https://codingcat.dev/tags/`}
21+
></NextSeo>
22+
<section className="grid gap-10 p-4 sm:p-10 place-items-center">
23+
<h1 className="text-5xl lg:text-7xl">Tags</h1>
24+
<section className="grid grid-cols-12 gap-2">
25+
{tags.map((tag, i) => (
26+
<Link href={`/tags/${tag.slug}`} key={i}>
27+
<a className="flex flex-col items-center p-2 bg-primary-900 text-primary-50 rounded-xl">
28+
<p>{tag.tag}</p>
29+
<p className="flex-initial p-2 rounded-full text-basics-900 bg-secondary-500">
30+
{tag.count}
31+
</p>
32+
</a>
33+
</Link>
34+
))}
35+
</section>
36+
</section>
37+
</Layout>
38+
);
39+
}
40+
41+
export async function getStaticProps(): Promise<{
42+
props: {
43+
site: Site | null;
44+
tags: Tag[];
45+
};
46+
revalidate: number;
47+
}> {
48+
const site = await getSite();
49+
const tags = await getTags();
50+
51+
return {
52+
props: {
53+
site,
54+
tags,
55+
},
56+
revalidate: 60,
57+
};
58+
}

0 commit comments

Comments
 (0)