Skip to content

Commit

Permalink
feat(backend): create post leaderboard + disable rate-limited buttons…
Browse files Browse the repository at this point in the history
… + custom 404 page
  • Loading branch information
EddyVinck committed Nov 3, 2023
1 parent 266ba2d commit 173fffc
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 17 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ Change any lines or add more in the `<head>` tags in `src/components/BaseHead.as
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
```

You can also update the colors in `tailwind.config.css`:

```js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
theme: {
extend: {
colors: {
header: "#f86423",
"header-foreground": "#000",
link: "#f86423",
primary: {
500: "#f86423",
600: "#db5215",
},
"primary-foreground": "#000",
},
```
#### Optional: enable backend services
You can enable backend services to your project by adding an [Appwrite](https://appwrite.io/) API key:
Expand Down
2 changes: 1 addition & 1 deletion src/components/Header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const topics = await getCollection("topic");
class="absolute left-0 top-full hidden w-full bg-header px-5 md:static md:block md:p-0"
>
<ul
class="m-0 flex list-none flex-col items-start gap-2 px-2 py-4 pb-8 md:flex-row md:items-center md:justify-between md:p-0 md:py-2"
class="m-0 flex list-none flex-col items-start gap-4 px-2 py-4 pb-8 md:flex-row md:items-center md:justify-between md:p-0 md:py-2"
>
<HeaderLink href="/blog">Blog</HeaderLink>
<HeaderLink href="/authors">Authors</HeaderLink>
Expand Down
41 changes: 33 additions & 8 deletions src/components/backend-services/EmojiReactions.astro
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,13 @@ if (shouldUseBackendServices) {
<div>
{
shouldUseBackendServices && emojiReactions !== null && (
<>
{console.log(emojiReactions)}
<aside>
<p class="text-center text-lg italic">Rate this article</p>
<div class="grid grid-cols-2 gap-4 md:grid-cols-4">
<button
id="likes-btn"
data-article={Astro.props.id}
class="rounded-lg bg-primary-500 p-2 text-center text-lg transition hover:bg-primary-600 active:scale-105"
class="rounded-lg bg-primary-500 p-2 text-center text-lg transition hover:bg-primary-600 active:scale-105 disabled:bg-slate-400 disabled:hover:bg-slate-500"
>
<span class="not-sr-only">👍</span>
<span class="sr-only">likes: </span>
Expand All @@ -55,7 +54,7 @@ if (shouldUseBackendServices) {
<button
id="hearts-btn"
data-article={Astro.props.id}
class="rounded-lg bg-primary-500 p-2 text-center text-lg transition hover:bg-primary-600 active:scale-105"
class="rounded-lg bg-primary-500 p-2 text-center text-lg transition hover:bg-primary-600 active:scale-105 disabled:bg-slate-400 disabled:hover:bg-slate-500"
>
<span class="not-sr-only">❤️</span>
<span class="sr-only">hearts: </span>
Expand All @@ -67,7 +66,7 @@ if (shouldUseBackendServices) {
<button
id="parties-btn"
data-article={Astro.props.id}
class="rounded-lg bg-primary-500 p-2 text-center text-lg transition hover:bg-primary-600 active:scale-105"
class="rounded-lg bg-primary-500 p-2 text-center text-lg transition hover:bg-primary-600 active:scale-105 disabled:bg-slate-400 disabled:hover:bg-slate-500"
>
<span class="not-sr-only">🎉</span>
<span class="sr-only">parties: </span>
Expand All @@ -79,7 +78,7 @@ if (shouldUseBackendServices) {
<button
id="poops-btn"
data-article={Astro.props.id}
class="rounded-lg bg-primary-500 p-2 text-center text-lg transition hover:bg-primary-600 active:scale-105"
class="rounded-lg bg-primary-500 p-2 text-center text-lg transition hover:bg-primary-600 active:scale-105 disabled:bg-slate-400 disabled:hover:bg-slate-500"
>
<span class="not-sr-only">💩</span>
<span class="sr-only">poops: </span>
Expand All @@ -88,7 +87,13 @@ if (shouldUseBackendServices) {
</span>
</button>
</div>
</>
<p class="text-center text-xs italic">
Checkout the blog post{" "}
<a href="/blog-ranking" class="text-link">
ranking page
</a>
</p>
</aside>
)
}
</div>
Expand All @@ -115,6 +120,12 @@ if (shouldUseBackendServices) {
});
if (!response.ok) {
console.log(`could not update ${type}`);
if (response.status === 429) {
setButtonsDisabled(true);
setTimeout(() => {
setButtonsDisabled(false);
}, 10000);
}
return;
}
const data = (await response.json()) as PostReactions;
Expand All @@ -123,7 +134,21 @@ if (shouldUseBackendServices) {
btn.querySelector(`#${type}-counter`)!.textContent =
data[type].toString();
}
// incrementEmojiReactionCount(type, articleId)
}
}

function setButtonsDisabled(isDisabled: boolean) {
if (isDisabled) {
document.querySelector("#likes-btn")?.setAttribute("disabled", "true");

document.querySelector("#hearts-btn")?.setAttribute("disabled", "true");
document.querySelector("#parties-btn")?.setAttribute("disabled", "true");
document.querySelector("#poops-btn")?.setAttribute("disabled", "true");
} else {
document.querySelector("#likes-btn")?.removeAttribute("disabled");
document.querySelector("#hearts-btn")?.removeAttribute("disabled");
document.querySelector("#parties-btn")?.removeAttribute("disabled");
document.querySelector("#poops-btn")?.removeAttribute("disabled");
}
}

Expand Down
71 changes: 71 additions & 0 deletions src/components/backend-services/RankedPost.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
import type { PostReactions } from "../../lib/appwrite/appwrite.server";
export type PostDataWithReactions = PostReactions & {
href: string;
title: string;
};
export type Props = PostDataWithReactions & { rank: number };
const { title, href, likes, hearts, parties, poops, rank } = Astro.props;
---

<div
class={`not-prose flex gap-1 rounded-md border p-2 ${
rank === 1
? "shadow-lg dark:bg-white/5 shadow-primary-500/40 md:scale-105 mb-2"
: ""
}`}
>
<p class="mt-1">
<span
class="tex-tenter mr-2 inline-flex h-12 w-12 items-center justify-center rounded-full bg-primary-500 text-primary-foreground"
>
{
rank < 4 ? (
<span class="text-3xl">
{rank === 1
? "🥇"
: rank === 2
? "🥈"
: rank === 3
? "🥉"
: `#${rank}`}
</span>
) : (
<span>#{rank}</span>
)
}
</span>
</p>
<div class="flex w-full flex-col gap-2">
<h2 class="m-0 text-lg">
<a class="text-link no-underline hover:underline" href={href}>
{title}
</a>
</h2>
<ul class="flex list-none justify-start gap-6">
<li>
<span class="not-sr-only">👍</span>
<span class="sr-only">likes:&nbsp;</span>
<span class="ml-1">{likes}</span>
</li>
<li>
<span class="not-sr-only">❤️</span>
<span class="sr-only">hearts:&nbsp;</span>
<span class="ml-1">{hearts}</span>
</li>
<li>
<span class="not-sr-only">🎉</span>
<span class="sr-only">parties:&nbsp;</span>
<span class="ml-1">{parties}</span>
</li>
<li>
<span class="not-sr-only">💩</span>
<span class="sr-only">poops:&nbsp;</span>
<span class="ml-1">{poops}</span>
</li>
</ul>
</div>
</div>
15 changes: 15 additions & 0 deletions src/content/blog/engineering-blog.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ module.exports = {
},
```
#### Optional: enable backend services
You can enable backend services to your project by adding an [Appwrite](https://appwrite.io/) API key:
```
# in /.env
SECRET_APPWRITE_API_KEY=YOUR_APPWRITE_API_KEY
PUBLIC_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
PUBLIC_APPWRITE_PROJECT_ID=PUBLIC_APPWRITE_PROJECT_ID
PUBLIC_APPWRITE_DATABASE_ID=PUBLIC_APPWRITE_DATABASE_ID
PUBLIC_APPWRITE_EMOJI_REACTIONS_COLLECTION_ID=post-reactions
```
## Features
- ✅ Easy configuration
Expand All @@ -75,6 +88,8 @@ module.exports = {
- ✅ Topics
- ✅ Blog pagination
- ✅ Blog drafts
- ✅ Dark mode (system preference + toggle button)
- ✅ Backend services (optional, uses Appwrite's free tier)
## Technologies used
Expand Down
40 changes: 39 additions & 1 deletion src/lib/appwrite/appwrite.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,45 @@ export const tryInitNewBlogPostsReactionsInDatabaseCollection = async (
}
};

export const getPostReactionsRanked = async (): Promise<
PostReactions[] | null
> => {
try {
const list = await appwriteDatabases.listDocuments<PostReactionsDocument>(
import.meta.env.PUBLIC_APPWRITE_DATABASE_ID,
import.meta.env.PUBLIC_APPWRITE_EMOJI_REACTIONS_COLLECTION_ID,
[
Query.orderDesc("likes"),
Query.orderDesc("hearts"),
Query.orderDesc("parties"),
Query.orderDesc("poops"),
]
);

if (list.total === 0) {
throw new Error(
`Got ${list.total} results when querying post reaction rankings`
);
}

return list.documents.map((doc) => ({
id: doc.id,
likes: doc.likes,
hearts: doc.hearts,
parties: doc.parties,
poops: doc.poops,
}));
} catch (error) {
if (error instanceof Error) {
console.log(
`Could not get post reaction data for ranking`,
error.message
);
}
return null;
}
};

export const getPostReactionsById = async (id: string) => {
try {
const list = await appwriteDatabases.listDocuments<PostReactionsDocument>(
Expand Down Expand Up @@ -182,7 +221,6 @@ export const incrementEmojiReactionCount = async (
articleId: string,
emojiType: PostReactionOption
) => {
console.log({ emojiType, articleId });
const list = await appwriteDatabases.listDocuments<PostReactionsDocument>(
import.meta.env.PUBLIC_APPWRITE_DATABASE_ID,
import.meta.env.PUBLIC_APPWRITE_EMOJI_REACTIONS_COLLECTION_ID,
Expand Down
13 changes: 13 additions & 0 deletions src/pages/404.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
import Layout from "../layouts/Page.astro";
---

<Layout
title="Topics"
description="An overview of all the various topics on our blog."
>
<h1>404 - not found :(</h1>
<div class="not-prose">
<a class="btn" href="/blog">Go to the homepage</a>
</div>
</Layout>
28 changes: 28 additions & 0 deletions src/pages/api/post-reactions-ranked.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { type APIRoute } from "astro";
import { getPostReactionsRanked } from "../../lib/appwrite/appwrite.server";

export const prerender = false;

export const GET: APIRoute = async (): Promise<Response> => {
if (!import.meta.env.SECRET_APPWRITE_API_KEY) {
return new Response(JSON.stringify({ error: "internal server error" }), {
status: 500,
});
}

let postReactionsRanked = null;
postReactionsRanked = await getPostReactionsRanked();
if (!postReactionsRanked) {
return new Response(null, {
status: 404,
statusText: "Not found",
});
}

return new Response(JSON.stringify(postReactionsRanked), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
};
23 changes: 16 additions & 7 deletions src/pages/api/post-reactions/[id].ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import rateLimit, { type RateLimitHeaders } from "../../../lib/ratelimit";
export const prerender = false;

const Getlimiter = rateLimit({
interval: 60 * 1000, // 60 seconds
interval: 10 * 1000, // 10 seconds
uniqueTokenPerInterval: 500, // Max 500 users per second
});

const Postlimiter = rateLimit({
interval: 60 * 1000, // 60 seconds
interval: 10 * 1000, // 10 seconds
uniqueTokenPerInterval: 500, // Max 500 users per second
});

Expand All @@ -26,6 +26,12 @@ export const GET: APIRoute = async ({
const id = params.id;
const userIP = clientAddress;

if (!import.meta.env.SECRET_APPWRITE_API_KEY) {
return new Response(JSON.stringify({ error: "internal server error" }), {
status: 500,
});
}

let rateLimitHeaders: RateLimitHeaders | null = null;
try {
rateLimitHeaders = await Getlimiter.check(25, `GET-REACTIONS-${userIP}`);
Expand Down Expand Up @@ -85,10 +91,6 @@ export const GET: APIRoute = async ({
poops: postReactions.poops,
};

console.log(
`👍 Post "${postReactionData.id}" has ${postReactionData.likes} likes!`
);

headers.append("Content-Type", "application/json");

return new Response(JSON.stringify(postReactionData), {
Expand All @@ -101,9 +103,15 @@ export const POST: APIRoute = async ({ request, params, clientAddress }) => {
const id = params.id;
const userIP = clientAddress;

if (!import.meta.env.SECRET_APPWRITE_API_KEY) {
return new Response(JSON.stringify({ error: "internal server error" }), {
status: 500,
});
}

let rateLimitHeaders: RateLimitHeaders | null = null;
try {
rateLimitHeaders = await Postlimiter.check(16, `POST-REACTIONS-${userIP}`);
rateLimitHeaders = await Postlimiter.check(10, `POST-REACTIONS-${userIP}`);
} catch (error: any) {
if ("X-RateLimit-Limit" in error && "X-RateLimit-Remaining" in error) {
const err = error as RateLimitHeaders;
Expand All @@ -117,6 +125,7 @@ export const POST: APIRoute = async ({ request, params, clientAddress }) => {
});
}
}

if (
typeof rateLimitHeaders?.["X-RateLimit-Limit"] === "undefined" ||
typeof rateLimitHeaders?.["X-RateLimit-Remaining"] === "undefined"
Expand Down
Loading

0 comments on commit 173fffc

Please sign in to comment.