Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 55 additions & 42 deletions bot/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,57 @@ const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base(
process.env.AIRTABLE_BASE_ID,
);

async function processStatusUpdate(baseAirtable, formula, singularTerm){
const records = await base(baseAirtable)
.select({
filterByFormula: formula,
})
.all();
for (const record of records) {
let name, status, email, reason;
if (singularTerm === "project") {
name = record.get("project_name_override") || record.get("Project Name");
status = record.get("status");
email = record.get("Email");
reason = record.get("status_change_reason");
} else if (singularTerm == "team") {
status = record.get("team");
reason = `*You hear drumbeats in the distance...*\nSomewhere, ${status} is calling your name. \n_*Welcome to the <https://en.wikipedia.org/wiki/Panathenaic_Games|Panathenaic Games>.*_\n\nFrom June 25th to July 9th, ship projects to earn points for your deity and outcompete rival deities.\nAll members of the winning team who contribute will earn an *exclusive Athena Award t-shirt!*\nYour deity is <https://https://en.wikipedia.org/wiki/${status}|${status}> - view your team's progress in the <https://award.athena.hackclub.com/gallery|Gallery>.`
email = record.get("email")
}
if (!email) continue;
try {
const channelId = await openConversationWithEmail(email);
await app.client.chat.postMessage({
token: process.env.SLACK_BOT_TOKEN,
channel: channelId,
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `\n*Hey <@${await getSlackIdByEmail(email)}>!*\n\nYour ${singularTerm} ${name ? name : ""} has had a status update! It's now *${status}*.\nThe notes given were:\n\n>${reason}\n\nIf you have questions, send a message in #athena-award.\n `,
},
},
],
username: "Athena Award",
});
await base(baseAirtable).update([
{
id: record.id,
fields: { status_change_dm_sent: true },
},
]);
app.logger.info(
`Sent status change update to ${email} and marked as sent.`,
);
} catch (error) {
app.logger.info(`Failed to message ${email} - ${error}`);
}
}

}

setInterval(async () => {
try {
const records = await base("Email Slack Invites")
Expand Down Expand Up @@ -156,48 +207,10 @@ Here's where you are right now:
}

try {
const records = await base("YSWS Project Submission")
.select({
filterByFormula: `AND(NOT({status_change_dm_sent}), {status_change_reason}, OR({status} = "approved", {status} = "rejected"))`,
})
.all();
for (const record of records) {
const email = record.get("Email");
const project = record.get("Project Name");
const project_name_override = record.get("project_name_override");
const status = record.get("status");
const reason = record.get("status_change_reason");
app.logger.info(email, project, status, reason);
if (!email) continue;
try {
const channelId = await openConversationWithEmail(email);
await app.client.chat.postMessage({
token: process.env.SLACK_BOT_TOKEN,
channel: channelId,
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `\n*Hey <@${await getSlackIdByEmail(email)}>!*\n\nYour project '${project_name_override || project}' has had a status update! It's now *${status}*.\nThe reason given was:\n\n>${reason}\n\nIf you have questions, send a message in #athena-award.\n `,
},
},
],
username: "Athena Award",
});
await base("YSWS Project Submission").update([
{
id: record.id,
fields: { status_change_dm_sent: true },
},
]);
app.logger.info(
`Sent status change update to ${email} and marked as sent.`,
);
} catch (error) {
app.logger.info(`Failed to message ${email}`);
}
}
processStatusUpdate("YSWS Project Submission", `AND(NOT({status_change_dm_sent}), {status_change_reason}, OR({status} = "approved", {status} = "rejected"))`, "project")
//processStatusUpdate("Orders", `AND(NOT({status_change_dm_sent}), {status_change_reason}, OR({status} = "fulfilled", {status} = "rejected"))`, "project")
processStatusUpdate("Registered Users", `NOT({status_change_dm_sent})`, "team")

} catch (err) {
app.logger.info("Airtable fetch error:", err);
}
Expand Down
43 changes: 43 additions & 0 deletions site/app/api/podium/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Airtable from "airtable";
import { NextResponse } from "next/server";

const airtable = new Airtable({
apiKey: process.env.AIRTABLE_API_KEY,
}).base(process.env.AIRTABLE_BASE_ID!);


export async function GET(){
const deities: { [key: string]: { hours: number; members: string[] }} = {
"Neith": {
hours: 0,
members: []
},
"Durga": {
hours: 0,
members: []
},
"Minerva": {
hours: 0,
members: []
}
}

for (var i = 0; i < Object.keys(deities).length; i ++){
var deityTotal = 0
var members: string[] = [];
console.log(Object.keys(deities)[i])
const records = await airtable("Registered Users").select({
filterByFormula: `AND({team}="${Object.keys(deities)[i]}", NOT({role}="admin"))`,
fields: ["hours_spent_panathenaic_games", "display_name"],
sort: [{ field: "hours_spent_unified_db", direction: "desc" }]
}).all()
for (const record of records){
deityTotal += Number(record.get("hours_spent_panathenaic_games"))
members.push(String(record.get("display_name")))
}
deities[Object.keys(deities)[i] as "Neith" | "Durga" | "Minerva"]["hours"] = deityTotal
deities[Object.keys(deities)[i] as "Neith" | "Durga" | "Minerva"]["members"] = members

}
return NextResponse.json(deities)
}
3 changes: 2 additions & 1 deletion site/app/api/user/my/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import { auth } from "@/auth";
import { getValue } from "@/services/fetchData";
import { verifyAuth } from "@/services/verifyAuth";

const validData = ["track", "current_stage", "slack_id", "total_time_approved_projects", "referred_users_count"]; // this is really stupid
const validData = ["track", "current_stage", "slack_id", "total_time_approved_projects", "referred_users_count", "team"]; // this is really stupid
interface validData {
track: string,
current_stage: string;
slack_id: string;
total_time_approved_projects: number;
referred_users_count: number;
team: string;
}

export async function GET(request: NextRequest) {
Expand Down
2 changes: 2 additions & 0 deletions site/app/gallery/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { STAGES } from "@/app/STAGES";
import { UXEvent, UXEventContext } from "@/components/context/UXStages";
import ShopModal from "@/components/modals/shop/Modal";
import LeaderboardModal from "@/components/modals/leaderboard/Modal";
import PodiumModal from "@/components/modals/podium/Modal";
import { useSession } from "next-auth/react";
import { useSearchParams } from "next/navigation";
import { useEffect } from "react";
Expand Down Expand Up @@ -42,6 +43,7 @@ export default function Gallery() {
<ProfileModal />
<InfoModal />
<LeaderboardModal />
<PodiumModal/>
<GalleryMenu
module={module}
setModule={
Expand Down
3 changes: 2 additions & 1 deletion site/components/context/UXStages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export type UXEvent =
| "resources"
| "info"
| "shop"
| "leaderboard";
| "leaderboard"
| "podium";

export type UXEventState = [UXEvent, (event: UXEvent) => void];

Expand Down
54 changes: 54 additions & 0 deletions site/components/modals/podium/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Tooltip } from "react-tooltip";
import Modal from "@/components/panels/layout/PopUpModal";
import useSWR from "swr";
import { multiFetcher } from "@/services/fetcher";
import { useContext } from "react";
import { UXEventContext } from "@/components/context/UXStages";
import { useSession } from "next-auth/react";

export default function PodiumModal() {
const session = useSession();
const [uxEvent, setUXEvent] = useContext(UXEventContext);
const { data, error, isLoading } = useSWR(
["/api/podium", "/api/user/my?query=team"],
multiFetcher,
);
let podium: {[key: string]: { hours: number; members: string[] }} = {}
let team: string = ""
if (data) {
podium = data[0]
team = data[1]["message"]

}

return (
<Modal
customHeader={{ icon: "bank-account", heading: "The Panathenaic Games" }}
uxEventName="podium"
uxEvent={uxEvent}
setUXEvent={setUXEvent}
className="overflow-auto">
<div>
<p>Compete against other Athena Award acolytes in the <a href = "https://en.wikipedia.org/wiki/Panathenaic_Games" target="_blank">Panathenaic Games</a> to honour your deity, <a href = {`https://en.wikipedia.org/wiki/${team}`}>{team}</a>.</p>
<p>Any projects shipped between June 24th and July 8th will count towards the Games. Winners earn an exclusive t-shirt!</p>
<div className = "flex flex-row *:col-span-1 *:w-full gap-5 h-96 m-3 p-3">
{Object.keys(podium).map((deity: any, index: number) =>
<div key = {index} className = "flex flex-col justify-end">
<Tooltip id = {String(index)} className = "max-w-72"/>
<div className = "text-center w-full">
<p className = "uppercase">{deity}</p>
<span className = "text-sm italic hidden sm:block">
{ deity == "Neith" && "Egyptian goddess of warfare & creator of the world"}
{ deity == "Durga" && "Hindu goddess of protection and strength"}
{ deity == "Minerva" && "Roman goddess of wisdom and justice"}
</span>
<span className = "text-sm">{podium[deity as 'Neith' | 'Durga' | 'Minerva']["hours"]} hours coded</span>
</div>
<div data-tooltip-id = {String(index)} data-tooltip-content = { podium[deity as 'Neith' | 'Durga' | 'Minerva']["members"].slice(0,40).join(", ") + ` + ${podium[deity as 'Neith' | 'Durga' | 'Minerva']["members"].length - 41} more`} className = "bg-gold mt-auto justify-self-end" style = {{ height: podium[deity as 'Neith' | 'Durga' | 'Minerva']["hours"] + "%"}}/>
</div>
)}
</div>
</div>
</Modal>
)
}
13 changes: 13 additions & 0 deletions site/components/panels/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,19 @@ export default function Icons() {
/>
</svg>
</button>

<button
onClick={() => {
setUXEvent("podium");
}}
id="podium"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" strokeWidth={1.5}
stroke="currentColor"
className="size-8 md:size-10 fill-white">
<path d="M353.8 54.1L330.2 6.3c-3.9-8.3-16.1-8.6-20.4 0L286.2 54.1l-52.3 7.5c-9.3 1.4-13.3 12.9-6.4 19.8l38 37-9 52.1c-1.4 9.3 8.2 16.5 16.8 12.2l46.9-24.8 46.6 24.4c8.6 4.3 18.3-2.9 16.8-12.2l-9-52.1 38-36.6c6.8-6.8 2.9-18.3-6.4-19.8l-52.3-7.5zM256 256c-17.7 0-32 14.3-32 32l0 192c0 17.7 14.3 32 32 32l128 0c17.7 0 32-14.3 32-32l0-192c0-17.7-14.3-32-32-32l-128 0zM32 320c-17.7 0-32 14.3-32 32L0 480c0 17.7 14.3 32 32 32l128 0c17.7 0 32-14.3 32-32l0-128c0-17.7-14.3-32-32-32L32 320zm416 96l0 64c0 17.7 14.3 32 32 32l128 0c17.7 0 32-14.3 32-32l0-64c0-17.7-14.3-32-32-32l-128 0c-17.7 0-32 14.3-32 32z"/></svg>
</button>

</div>
</>
);
Expand Down
5 changes: 4 additions & 1 deletion site/services/fetchData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface safeData {
total_time_approved_projects: number;
referred_users_count: number;
track: "beginner" | "advanced";
team: string;
}

// verify req
Expand Down Expand Up @@ -56,6 +57,7 @@ export async function getValue(emailAddress: string): Promise<safeData> {
prettyRecordID["total_time_approved_projects"];
const track = prettyRecordID["track"];
const referred_users_count = prettyRecordID["referred_users_count"]
const team = prettyRecordID["team"]

return {
slack_id,
Expand All @@ -68,6 +70,7 @@ export async function getValue(emailAddress: string): Promise<safeData> {
project_unique_names,
total_time_approved_projects,
track,
referred_users_count
referred_users_count,
team
};
}