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
|
||||
|
||||
# Lint before starting
|
||||
RUN bun --filter 'website' lint
|
||||
RUN bun --filter 'backend' lint
|
||||
|
||||
CMD ["bun", "run", "--filter", "backend", "start"]
|
||||
|
@ -12,6 +12,13 @@ export default class AppController {
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/health")
|
||||
public async getHealth() {
|
||||
return {
|
||||
status: "OK",
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/statistics")
|
||||
public async getStatistics() {
|
||||
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
|
||||
*/
|
||||
@ -24,3 +26,17 @@ export function delay(ms: number) {
|
||||
export function getPageFromRank(rank: number, itemsPerPage: number) {
|
||||
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 Link from "next/link";
|
||||
import ky from "ky";
|
||||
import { config } from "../../../config";
|
||||
import { AppStatistics } from "@ssr/common/types/backend/app-statistics";
|
||||
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 default async function HomePage() {
|
||||
const statistics = await ky.get(config.siteApi + "/statistics").json<AppStatistics>();
|
||||
const statistics = await kyFetch<AppStatistics>(config.siteApi + "/statistics");
|
||||
|
||||
return (
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{statistics && (
|
||||
<div className="flex items-center flex-col">
|
||||
<p className="font-semibold">Site Statistics</p>
|
||||
<Statistic title="Total Tracked Players" value={statistics.trackedPlayers} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Link href="/search">
|
||||
|
@ -14,6 +14,7 @@ import NavBar from "../components/navbar/navbar";
|
||||
import { Colors } from "@/common/colors";
|
||||
import OfflineNetwork from "@/components/offline-network";
|
||||
import Script from "next/script";
|
||||
import { ApiHealth } from "@/components/api/api-health";
|
||||
|
||||
const siteFont = localFont({
|
||||
src: "./fonts/JetBrainsMono.ttf",
|
||||
@ -79,6 +80,7 @@ export default function RootLayout({
|
||||
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem disableTransitionOnChange>
|
||||
<QueryProvider>
|
||||
<AnimatePresence>
|
||||
<ApiHealth />
|
||||
<main className="flex flex-col min-h-screen gap-2 text-white w-full">
|
||||
<NavBar />
|
||||
<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