+ + + + + {t('whoWeAre.title')} +
+ {t('whoWeAre.description')} + + + + + + 0 + ? `${process.env.NEXT_PUBLIC_CDN_URL}/uploads/${showcaseImages[1].image.name}` + : '/images/landing_bg_default.jpg' // TODO: replace with better cdn fallback + } + w="100%" + alt={`${showcaseImages[1].title}, ${showcaseImages[1].city}`} + /> + + + {`${showcaseImages[1].title}, ${showcaseImages[1].city}`} + + + + + + + + 0 + ? `${process.env.NEXT_PUBLIC_CDN_URL}/uploads/${showcaseImages[2].image.name}` + : '/images/landing_bg_default.jpg' // TODO: replace with better cdn fallback + } + w="100%" + alt={`${showcaseImages[2].title}, ${showcaseImages[2].city}`} + /> + + + {`${showcaseImages[2].title}, ${showcaseImages[2].city}`} + + + + + + + + + + {t('whatWeHaveDone.title')} +
+ + + + + {[ + { + count: statClaims._sum.buildings, + title: t('whatWeHaveDone.buildings'), + icon: IconBuildingSkyscraper, + suffix: ' ', + }, + { + count: (statClaims._sum.size || 1) / 1_000_000, + title: t('whatWeHaveDone.area'), + icon: IconMap, + suffix: 'km² ', + }, + { count: statUsers, title: t('whatWeHaveDone.users'), icon: IconUsersGroup, suffix: ' ' }, + ].map((stat) => ( +
+ + + {formatter.number(stat.count as number, { maximumSignificantDigits: 3, roundingMode: 'floor' })} + {stat.suffix}+ + + + {stat.title} + +
+ ))} +
+ + {[ + { + count: statClaims._sum.buildings, + title: t('whatWeHaveDone.buildings'), + icon: IconBuildingSkyscraper, + suffix: ' ', + }, + { + count: (statClaims._sum.size || 1) / 1_000_000, + title: t('whatWeHaveDone.area'), + icon: IconMap, + suffix: 'km² ', + }, + { count: statUsers, title: t('whatWeHaveDone.users'), icon: IconUsersGroup, suffix: ' ' }, + ].map((stat) => ( +
+ + + {formatter.number(stat.count as number, { maximumSignificantDigits: 3, roundingMode: 'floor' })} + {stat.suffix}+ + + + {stat.title} + +
+ ))} +
+
+ + + + + {t('globalCommunity.title')} +
+ {t('globalCommunity.description')} + } + mt="md" + > + {t('globalCommunity.cta')} + + + + + + + 0 + ? `${process.env.NEXT_PUBLIC_CDN_URL}/uploads/${showcaseImages[3].image.name}` + : '/images/landing_bg_default.jpg' // TODO: replace with better cdn fallback + } + w="100%" + alt={`${showcaseImages[3].title}, ${showcaseImages[3].city}`} + /> + + + {`${showcaseImages[3].title}, ${showcaseImages[3].city}`} + + + + + + + + 0 + ? `${process.env.NEXT_PUBLIC_CDN_URL}/uploads/${showcaseImages[4].image.name}` + : '/images/landing_bg_default.jpg' // TODO: replace with better cdn fallback + } + w="100%" + alt={`${showcaseImages[4].title}, ${showcaseImages[4].city}`} + /> + + + {`${showcaseImages[4].title}, ${showcaseImages[4].city}`} + + + + + + + + {t('explore.title')} +
+ {t('explore.description')} + } + mt="md" + > + {t('explore.cta')} + + + + + + + + {t('howToHelp.title')} +
+ {t('howToHelp.description')} + + + + + + + }> + }> + }> + + + + + + + + {showcaseImages.slice(5, 9).map((image) => ( + + + + ))} + + } + mt="md" + > + {t('gallery.cta')} + + + + + {t('mediaList.title')} +
+ + + + + + {outreachArticles.slice(0, 2).map((item) => ( + + + + ))} + + + {outreachArticles.slice(0, 3).map((item) => ( + + + + ))} + + + + } + mt="md" + > + {t('mediaList.mainCta')} + + + + + +
+ + ); +} diff --git a/apps/frontend/src/app/[locale]/statistics/page.tsx b/apps/frontend/src/app/[locale]/statistics/page.tsx new file mode 100644 index 00000000..8d45b867 --- /dev/null +++ b/apps/frontend/src/app/[locale]/statistics/page.tsx @@ -0,0 +1,879 @@ +import Wrapper from '@/components/layout/Wrapper'; +import prisma from '@/util/db'; +import { getLanguageAlternates } from '@/util/seo'; +import { BarChart, PieChart } from '@mantine/charts'; +import { Badge, Box, Code, Container, Grid, GridCol, Text, Title } from '@mantine/core'; +// import { PrismaClient } from '@prisma/client'; +import { Metadata } from 'next'; +import { Locale } from 'next-intl'; +import { getFormatter, getTranslations, setRequestLocale } from 'next-intl/server'; + +export const dynamic = 'force-static'; +export const revalidate = 3600; // 60m + +export async function generateMetadata({ params }: { params: Promise<{ locale: Locale }> }): Promise { + const locale = (await params).locale; + const t = (await getTranslations({ locale, namespace: 'statistics.seo' })) as ( + key: 'title' | 'description', + ) => string; + + return { + title: t('title'), + description: t('description'), + alternates: { + languages: getLanguageAlternates('/statistics'), + }, + }; +} + +export default async function Page({ params }: { params: Promise<{ locale: Locale }> }) { + const locale = (await params).locale; + setRequestLocale(locale); + const t = await getTranslations('statistics'); + const formatter = await getFormatter(); + const counts = await prisma.$transaction([ + prisma.user.count({ where: { joinedBuildTeams: { some: {} } } }), + prisma.claim.count({ where: { active: true } }), + prisma.claim.aggregate({ _sum: { buildings: true }, where: { active: true, finished: true } }), + ]); + const claims = await prisma.$transaction([ + prisma.claim.count({ where: { active: true, finished: true } }), + prisma.claim.aggregate({ _sum: { buildings: true }, where: { active: true } }), + prisma.claim.aggregate({ _sum: { size: true }, where: { active: true } }), + prisma.claim.aggregate({ _sum: { size: true }, where: { active: true, finished: true } }), + prisma.claim.count(), + prisma.claim.aggregate({ _sum: { buildings: true } }), + prisma.claim.aggregate({ _sum: { size: true } }), + ]); + + const leaderboard = await prisma.$transaction([ + prisma.claim.findMany({ + orderBy: { buildings: 'desc' }, + where: { active: true, finished: true }, + take: 5, + select: { name: true, city: true, buildings: true }, + }), + prisma.claim.findMany({ + orderBy: { size: 'desc' }, + where: { active: true, finished: true }, + + take: 5, + select: { name: true, city: true, size: true }, + }), + prisma.user.findMany({ + orderBy: { claims: { _count: 'desc' } }, + take: 10, + select: { username: true, _count: { select: { claims: true } } }, + }), + prisma.buildTeam.findMany({ + orderBy: { claims: { _count: 'desc' } }, + take: 10, + select: { name: true, _count: { select: { claims: true } } }, + }), + ]); + + const buildTeams = await prisma.$transaction([ + prisma.buildTeam.findMany({ + orderBy: { members: { _count: 'desc' } }, + select: { name: true, slug: true, color: true, _count: { select: { members: true } } }, + }), + ]); + + return ( + + + + + {t('atAGlance')} +
+ + + + + + + {formatter.number(counts[0])} + + + {t('users.label')} + + + {t('users.description')} + + + + + {formatter.number(counts[2]._sum.buildings || 0, { maximumSignificantDigits: 4 })}+ + + + {t('buildings.label')} + + + {t('buildings.description')} + + + + + {formatter.number(counts[1], { maximumSignificantDigits: 4 })}+ + + + {t('claims.label')} + + + {t('claims.description')} + + + + + + + + {t('claims.sectionTitle')} + +
+ + + + + + + {formatter.number(claims[4])} + + + {t('claims.total.label')} + + + {t('claims.total.description')} + + + + + {formatter.number(counts[1])} + + + {t('claims.active.label')} + + + {t('claims.active.description')} + + + + + {formatter.number(claims[0])} + + + {t('claims.finished.label')} + + + {t('claims.finished.description')} + + + + + {formatter.number(counts[1] - claims[0])} + + + {t('claims.unfinished.label')} + + + {t('claims.unfinished.description')} + + + + + + + {(() => { + const finished = claims[0]; + const unfinished = counts[1] - claims[0]; + const total = counts[1]; + return ( + + + {t('charts.finishedVsUnfinishedCount')} + + + + +