-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
create routing and ranking business logic
- Loading branch information
1 parent
5230e49
commit 0946ba7
Showing
15 changed files
with
535 additions
and
495 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const RANKING_INITIAL_DATA = new Date("2024-06-10"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { NextApiRequest, NextApiResponse } from "next"; | ||
import runRequestWithDIContainer from "../../../../../../middlewares/diContainerMiddleware"; | ||
import { PrismaClient } from "@prisma/client"; | ||
import { DIContainerNextApiRequest } from "../../../../../../dependency_injection/DIContainerNextApiRequest"; | ||
import { RANKING_INITIAL_DATA } from "../../../contants"; | ||
import { GetClassData } from "../../../types"; | ||
|
||
export default async (req: NextApiRequest, res: NextApiResponse) => { | ||
if (req.method === "GET") { | ||
await runRequestWithDIContainer(req, res, run); | ||
} else { | ||
res.statusCode = 405; | ||
res.send(""); | ||
} | ||
}; | ||
|
||
async function run(req: DIContainerNextApiRequest, res: NextApiResponse) { | ||
const prismaClient: PrismaClient = req.scope.resolve("dbClient"); | ||
try { | ||
const rankingData = await getData( | ||
prismaClient, | ||
req.query.degree as string, | ||
req.query.class as string | ||
); | ||
res.statusCode = 200; | ||
res.json(rankingData); | ||
} catch (error) { | ||
console.log(error); | ||
res.statusCode = 500; | ||
return; | ||
} | ||
} | ||
|
||
async function getData( | ||
prisma: PrismaClient, | ||
degree: string, | ||
year: string | ||
): Promise<GetClassData> { | ||
const minYear = Math.floor(Number(year) / 5) * 5; | ||
const maxYear = minYear + 5; | ||
|
||
const result: { | ||
period: number; | ||
degree: string; | ||
amount: number; | ||
donors: number; | ||
first_name: string; | ||
last_name: string; | ||
url: string; | ||
}[] = await prisma.$queryRaw` | ||
SELECT | ||
u."id", | ||
u."first_name", | ||
u."last_name", | ||
u."url", | ||
SUM(c."amount_in_cents")/100 AS "amount" | ||
FROM "users" u | ||
LEFT JOIN "contributions" c ON u."id" = c."userId" | ||
WHERE | ||
u."degree" = ${degree} | ||
AND u."admission_year" >= ${minYear} | ||
AND u."admission_year" < ${maxYear} | ||
AND c."state" = 'completed' | ||
AND c."createdAt" > ${RANKING_INITIAL_DATA} | ||
GROUP BY u."id" | ||
`; | ||
|
||
const amount = result.reduce((acc, row) => acc + row.amount, 0); | ||
const numberOfDonors = result.length; | ||
const donors: GetClassData["donors"] = result.map((row, index) => { | ||
return { | ||
name: `${row.first_name} ${row.last_name}`, | ||
url: row.url, | ||
}; | ||
}); | ||
|
||
return { | ||
amount, | ||
numberOfDonors, | ||
donors, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { NextApiRequest, NextApiResponse } from "next"; | ||
import runRequestWithDIContainer from "../../../../../middlewares/diContainerMiddleware"; | ||
import { PrismaClient } from "@prisma/client"; | ||
import { DIContainerNextApiRequest } from "../../../../../dependency_injection/DIContainerNextApiRequest"; | ||
import { RANKING_INITIAL_DATA } from "./../../contants"; | ||
import { GetRankingData } from "./../../types"; | ||
|
||
export default async (req: NextApiRequest, res: NextApiResponse) => { | ||
if (req.method === "GET") { | ||
await runRequestWithDIContainer(req, res, run); | ||
} else { | ||
res.statusCode = 405; | ||
res.send(""); | ||
} | ||
}; | ||
|
||
async function run(req: DIContainerNextApiRequest, res: NextApiResponse) { | ||
const prismaClient: PrismaClient = req.scope.resolve("dbClient"); | ||
try { | ||
console.log(req.query); | ||
const rankingData = await getRankingData( | ||
prismaClient, | ||
req.query.degree as string | undefined | ||
); | ||
res.statusCode = 200; | ||
res.json(rankingData); | ||
} catch (error) { | ||
console.log(error); | ||
res.statusCode = 500; | ||
return; | ||
} | ||
} | ||
|
||
async function getRankingData( | ||
prisma: PrismaClient, | ||
degree: string | undefined | ||
): Promise<GetRankingData> { | ||
const result: { | ||
period: number; | ||
degree: string; | ||
amount: number; | ||
donors: number; | ||
}[] = await prisma.$queryRaw` | ||
SELECT | ||
FLOOR((u."admission_year") / 5) * 5 AS "period", | ||
u."degree", | ||
SUM(c."amount_in_cents")/100 AS "amount", | ||
COUNT(DISTINCT c."userId") AS "donors" | ||
FROM "users" u | ||
JOIN "contributions" c ON u."id" = c."userId" | ||
WHERE | ||
c."state" = 'completed' | ||
AND c."createdAt" > ${RANKING_INITIAL_DATA} | ||
AND u."degree" = ${degree} | ||
GROUP BY "period", u."degree" | ||
ORDER BY "amount" DESC; | ||
`; | ||
|
||
const amount = result.reduce((acc, row) => acc + row.amount, 0); | ||
const numberOfDonors = result.reduce((acc, row) => acc + row.donors, 0); | ||
const ranking: GetRankingData["ranking"] = result.map((row, index) => { | ||
return { | ||
position: index + 1, | ||
degree: row.degree, | ||
initialYear: row.period, | ||
finalYear: row.period + 4, | ||
amount: row.amount, | ||
numberOfDonors: row.donors, | ||
}; | ||
}); | ||
|
||
return { | ||
amount, | ||
numberOfDonors, | ||
ranking, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { NextApiRequest, NextApiResponse } from "next"; | ||
import runRequestWithDIContainer from "../../../middlewares/diContainerMiddleware"; | ||
import { PrismaClient } from "@prisma/client"; | ||
import { DIContainerNextApiRequest } from "../../../dependency_injection/DIContainerNextApiRequest"; | ||
import { RANKING_INITIAL_DATA } from "./contants"; | ||
import { GetRankingData } from "./types"; | ||
|
||
export default async (req: NextApiRequest, res: NextApiResponse) => { | ||
if (req.method === "GET") { | ||
await runRequestWithDIContainer(req, res, run); | ||
} else { | ||
res.statusCode = 405; | ||
res.send(""); | ||
} | ||
}; | ||
|
||
async function run(req: DIContainerNextApiRequest, res: NextApiResponse) { | ||
const prismaClient: PrismaClient = req.scope.resolve("dbClient"); | ||
try { | ||
console.log(req.query); | ||
const rankingData = await getRankingData(prismaClient); | ||
res.statusCode = 200; | ||
res.json(rankingData); | ||
} catch (error) { | ||
console.log(error); | ||
res.statusCode = 500; | ||
return; | ||
} | ||
} | ||
|
||
const MOCK: GetRankingData = { | ||
amount: 100000, | ||
numberOfDonors: 100, | ||
ranking: [], | ||
}; | ||
|
||
async function getRankingData(prisma: PrismaClient): Promise<GetRankingData> { | ||
const result: { | ||
period: number; | ||
degree: string; | ||
amount: number; | ||
donors: number; | ||
}[] = await prisma.$queryRaw` | ||
SELECT | ||
FLOOR((u."admission_year") / 5) * 5 AS "period", | ||
u."degree", | ||
SUM(c."amount_in_cents")/100 AS "amount", | ||
COUNT(DISTINCT c."userId") AS "donors" | ||
FROM "users" u | ||
JOIN "contributions" c ON u."id" = c."userId" | ||
WHERE | ||
c."state" = 'completed' | ||
AND c."createdAt" > ${RANKING_INITIAL_DATA} | ||
GROUP BY "period", u."degree" | ||
ORDER BY "amount" DESC; | ||
`; | ||
|
||
const amount = result.reduce((acc, row) => acc + row.amount, 0); | ||
const numberOfDonors = result.reduce((acc, row) => acc + row.donors, 0); | ||
const ranking: GetRankingData["ranking"] = result.map((row, index) => { | ||
return { | ||
position: index + 1, | ||
degree: row.degree, | ||
initialYear: row.period, | ||
finalYear: row.period + 4, | ||
amount: row.amount, | ||
numberOfDonors: row.donors, | ||
}; | ||
}); | ||
|
||
return { | ||
amount, | ||
numberOfDonors, | ||
ranking, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
export type GetRankingData = { | ||
amount: number; | ||
numberOfDonors: number; | ||
ranking: { | ||
position: number; | ||
degree: string; | ||
initialYear: number; | ||
finalYear: number; | ||
amount: number; | ||
numberOfDonors: number; | ||
}[]; | ||
}; | ||
|
||
export type GetClassData = { | ||
amount: number; | ||
numberOfDonors: number; | ||
donors: { | ||
name: string; | ||
url?: string; | ||
}[]; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import styles from "./index.module.css"; | ||
import Header from "../../../../../components/ranking/Header/Header"; | ||
import DonationCallout from "../../../../../components/ranking/Donate/DonateBanner"; | ||
import ResultsDisplay from "../../../../../components/ranking/ResultsDisplay/ResultsDisplay"; | ||
import ShareCallout from "../../../../../components/ranking/Share/ShareCallout"; | ||
import BackButton from "../../../../../components/ranking/BackButton/BackButton"; | ||
import { useRouter } from "next/router"; | ||
import { GetClassData } from "../../../../api/ranking/types"; | ||
import { useState, useEffect } from "react"; | ||
|
||
const getClassData = async (degree: string, year: string) => { | ||
const response = await fetch(`/api/ranking/degrees/${degree}/${year}`); | ||
return (await response.json()) as GetClassData; | ||
}; | ||
|
||
export default function ClassPage() { | ||
const [data, setData] = useState<GetClassData>({ | ||
amount: 0, | ||
numberOfDonors: 0, | ||
donors: [], | ||
}); | ||
|
||
const donated = true; | ||
|
||
const router = useRouter(); | ||
const degree = router.query.degree as string; | ||
const year = router.query.year as string; | ||
|
||
useEffect(() => { | ||
getClassData(degree, year).then(setData); | ||
}); | ||
|
||
if (Number(year) < 1900) { | ||
return <p>Invalid year</p>; | ||
} | ||
|
||
return ( | ||
<div className={styles.html}> | ||
<main className={styles.rankingPage}> | ||
<DonationCallout /> | ||
<div className={styles.content}> | ||
<BackButton /> | ||
<Header | ||
tag={degree} | ||
tagOnClick={() => { | ||
window.open("/ranking/degrees/" + degree, "_self"); | ||
}} | ||
title={year + "-" + String(Number(year) + 4)} | ||
description={"Any"} | ||
/> | ||
<ResultsDisplay amount={data.amount} count={data.numberOfDonors} /> | ||
<Donors userDonated={donated} donors={data.donors} /> | ||
|
||
{!donated && ( | ||
<p>Você precisa doar para poder visualizar aos doadores</p> | ||
)} | ||
|
||
<ShareCallout donate whatsApp linkedIn copy /> | ||
</div> | ||
</main> | ||
</div> | ||
); | ||
} | ||
|
||
const Donors = ({ | ||
userDonated, | ||
donors, | ||
}: { | ||
userDonated: boolean; | ||
donors: { name: string; url?: string }[]; | ||
}) => { | ||
return ( | ||
<div className={userDonated ? styles.donors : styles.hideDonors}> | ||
{donors.map((donor) => ( | ||
<button | ||
className={styles.donor} | ||
onClick={() => { | ||
if (userDonated && donor.url) window.open(donor.url, "_blank"); | ||
}} | ||
> | ||
<p>{donor.name}</p> | ||
</button> | ||
))} | ||
</div> | ||
); | ||
}; |
Oops, something went wrong.