Compare commits
2 Commits
6d6e59ed13
...
cb7143ed3d
Author | SHA1 | Date | |
---|---|---|---|
cb7143ed3d | |||
1eed0e1e99 |
@ -25,6 +25,6 @@ RUN bun --filter '@ssr/common' build
|
|||||||
COPY --from=depends /app/projects/backend ./projects/backend
|
COPY --from=depends /app/projects/backend ./projects/backend
|
||||||
|
|
||||||
# Lint before starting
|
# Lint before starting
|
||||||
RUN bun --filter 'website' lint
|
RUN bun --filter 'backend' lint
|
||||||
|
|
||||||
CMD ["bun", "run", "--filter", "backend", "start"]
|
CMD ["bun", "run", "--filter", "backend", "start"]
|
||||||
|
@ -12,6 +12,13 @@ export default class AppController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("/health")
|
||||||
|
public async getHealth() {
|
||||||
|
return {
|
||||||
|
status: "OK",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Get("/statistics")
|
@Get("/statistics")
|
||||||
public async getStatistics() {
|
public async getStatistics() {
|
||||||
return await AppService.getAppStatistics();
|
return await AppService.getAppStatistics();
|
||||||
|
27
projects/common/src/utils/api-utils.ts
Normal file
27
projects/common/src/utils/api-utils.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import ky from "ky";
|
||||||
|
|
||||||
|
type ApiHealth = {
|
||||||
|
online: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the health of the api server.
|
||||||
|
*
|
||||||
|
* @param url the url of the api
|
||||||
|
*/
|
||||||
|
export async function getApiHealth(url: string): Promise<ApiHealth> {
|
||||||
|
try {
|
||||||
|
await ky
|
||||||
|
.get(url, {
|
||||||
|
cache: "no-cache",
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
return {
|
||||||
|
online: true,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
online: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
import ky from "ky";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if we're in production
|
* Checks if we're in production
|
||||||
*/
|
*/
|
||||||
@ -24,3 +26,17 @@ export function delay(ms: number) {
|
|||||||
export function getPageFromRank(rank: number, itemsPerPage: number) {
|
export function getPageFromRank(rank: number, itemsPerPage: number) {
|
||||||
return Math.floor(rank / itemsPerPage) + 1;
|
return Math.floor(rank / itemsPerPage) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches data from the given url.
|
||||||
|
*
|
||||||
|
* @param url the url to fetch
|
||||||
|
*/
|
||||||
|
export async function kyFetch<T>(url: string): Promise<T | undefined> {
|
||||||
|
try {
|
||||||
|
return await ky.get<T>(url).json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching data from ${url}:`, error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import ky from "ky";
|
|
||||||
import { config } from "../../../config";
|
import { config } from "../../../config";
|
||||||
import { AppStatistics } from "@ssr/common/types/backend/app-statistics";
|
import { AppStatistics } from "@ssr/common/types/backend/app-statistics";
|
||||||
import Statistic from "@/components/home/statistic";
|
import Statistic from "@/components/home/statistic";
|
||||||
|
import { kyFetch } from "@ssr/common/utils/utils";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic"; // Always generate the page on load
|
export const dynamic = "force-dynamic"; // Always generate the page on load
|
||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
const statistics = await ky.get(config.siteApi + "/statistics").json<AppStatistics>();
|
const statistics = await kyFetch<AppStatistics>(config.siteApi + "/statistics");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col items-center w-full gap-6 text-center">
|
<main className="flex flex-col items-center w-full gap-6 text-center">
|
||||||
@ -21,10 +21,12 @@ export default async function HomePage() {
|
|||||||
<p>ScoreSaber Reloaded is a website that allows you to track your ScoreSaber data over time.</p>
|
<p>ScoreSaber Reloaded is a website that allows you to track your ScoreSaber data over time.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center flex-col">
|
{statistics && (
|
||||||
<p className="font-semibold">Site Statistics</p>
|
<div className="flex items-center flex-col">
|
||||||
<Statistic title="Total Tracked Players" value={statistics.trackedPlayers} />
|
<p className="font-semibold">Site Statistics</p>
|
||||||
</div>
|
<Statistic title="Total Tracked Players" value={statistics.trackedPlayers} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
<Link href="/search">
|
<Link href="/search">
|
||||||
|
@ -14,6 +14,7 @@ import NavBar from "../components/navbar/navbar";
|
|||||||
import { Colors } from "@/common/colors";
|
import { Colors } from "@/common/colors";
|
||||||
import OfflineNetwork from "@/components/offline-network";
|
import OfflineNetwork from "@/components/offline-network";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
|
import { ApiHealth } from "@/components/api/api-health";
|
||||||
|
|
||||||
const siteFont = localFont({
|
const siteFont = localFont({
|
||||||
src: "./fonts/JetBrainsMono.ttf",
|
src: "./fonts/JetBrainsMono.ttf",
|
||||||
@ -79,6 +80,7 @@ export default function RootLayout({
|
|||||||
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem disableTransitionOnChange>
|
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem disableTransitionOnChange>
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
|
<ApiHealth />
|
||||||
<main className="flex flex-col min-h-screen gap-2 text-white w-full">
|
<main className="flex flex-col min-h-screen gap-2 text-white w-full">
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<div className="z-[1] m-auto flex flex-col flex-grow items-center w-full md:max-w-[1600px]">
|
<div className="z-[1] m-auto flex flex-col flex-grow items-center w-full md:max-w-[1600px]">
|
||||||
|
46
projects/website/src/components/api/api-health.tsx
Normal file
46
projects/website/src/components/api/api-health.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { getApiHealth } from "@ssr/common/utils/api-utils";
|
||||||
|
import { config } from "../../../config";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { useIsFirstRender } from "@uidotdev/usehooks";
|
||||||
|
|
||||||
|
export function ApiHealth() {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const firstRender = useIsFirstRender();
|
||||||
|
const [online, setOnline] = useState<boolean>(true);
|
||||||
|
const previousOnlineStatus = useRef<boolean>(true);
|
||||||
|
|
||||||
|
useQuery({
|
||||||
|
queryKey: ["api-health"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const status = (await getApiHealth(config.siteApi)).online;
|
||||||
|
setOnline(status);
|
||||||
|
return status;
|
||||||
|
},
|
||||||
|
refetchInterval: 1000 * 15,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (firstRender) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger toast only if the online status changes
|
||||||
|
if (previousOnlineStatus.current !== online) {
|
||||||
|
toast({
|
||||||
|
title: `The API is now ${online ? "Online" : "Offline"}!`,
|
||||||
|
description: online ? "The API has recovered connectivity." : "The API has lost connectivity.",
|
||||||
|
variant: online ? "success" : "destructive",
|
||||||
|
duration: 10_000, // 10 seconds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the previous online status
|
||||||
|
previousOnlineStatus.current = online;
|
||||||
|
}, [firstRender, online, toast]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
}
|
Reference in New Issue
Block a user