fix(layoutCounters): make it works
+ 51
- 133
app/components/Layout.tsx
@@ -10,7 +10,6 @@ import InstantRouterIndicator from "../islands/InstantRouterIndicator";
 // app components
 import { PageHeader } from "./PageHeader";
 import { DrawerPrimary } from "./DrawerPrimary";
-import { withLayoutCounters } from "../hocs/withLayoutCounters";
 
 const BRANDLINE_HEIGHT = 4;
 const HEADER_HEIGHT = 64;

...
@@ -134,8 +133,9 @@ const LayoutComponent: FC<LayoutProps & WithThemeSchemeProp> = (props) => {
 // const _LayoutWithCounters = withLayoutCounters(
 //   LayoutComponent as NonNullable<() => JSX.Element>,
 // );
-withLayoutCounters;
-export const Layout = LayoutComponent as NonNullable<() => JSX.Element>;
+// withLayoutCounters; // as NonNullable<() => JSX.Element>;
+// withLayoutCounters;
+export const Layout = LayoutComponent;
 
 /* Styled components (unchanged from previous layout structure) */
 const StyledLayoutWrapper = styled.div<WithThemeSchemeProp>`

file deleted
app/controllers/api/getRepositoryCounters.ts
@@ -1,40 +0,0 @@
-// 1st-party
-import type { ReqHandler } from "@ethicdevs/react-monolith";
-// app
-import type { RepositoryCountersDTO } from "../../types";
-import { AppRoute, AppRouteParams } from "../../routes.defs";
-
-const getRepositoryCounters: ReqHandler<
-  AppRouteParams,
-  AppRoute.REPOSITORY_COUNTERS_API
-> = async (request, reply) => {
-  const { orgSlug, repoSlug } = request.params;
-
-  // total pulls in repository (PRs targeting this repo)
-  const totalPulls = await request.prisma.pullRequest.count({
-    where: {
-      targetRepository: {
-        organization: { slug: orgSlug },
-        slug: repoSlug,
-      },
-    },
-  });
-
-  // for now provide pulls and zeros
-  const counters: RepositoryCountersDTO = {
-    pulls: totalPulls,
-    tests: 0,
-    builds: 0,
-    issues: 0,
-    apiRefSymbols: 0,
-    helpCenterNotifs: 0,
-  };
-
-  reply.send(JSON.stringify(counters) + "\n");
-};
-
-export default getRepositoryCounters;
-
-export const APIControllers = {
-  getRepositoryCounters,
-};

app/controllers/index.ts
@@ -8,4 +8,3 @@ export { SSHAuthController } from "./ssh-auth";
 export { SyntaxHighlightController } from "./syntaxHighlight";
 export { ThemeController } from "./theme";
 export { UserController } from "./user";
-export { APIControllers } from "./api/getRepositoryCounters";

app/controllers/syntaxHighlight/highlightCodeAction.ts
@@ -6,7 +6,7 @@ import { parse as parseHtmlToJson, TextNode, RealNode } from "himalaya";
 // app
 import type { AppThemeScheme } from "../../types";
 import { AppRoute, AppRouteParams } from "../../routes.defs";
-import { escapeHtmlCode } from "../../utils/shared/escapeHtmlCode";
+import escapeHtmlCode from "../../utils/shared/escapeHtmlCode";
 
 Prism.languages.prisma = Prism.languages.extend("javascript", {
   keyword: /\b(?:datasource|enum|generator|model|type)\b/,

...
@@ -32,7 +32,7 @@ const syntaxHighlightThemes: Record<AppThemeScheme, string> = {
 function getHighlightedCode(
   code: string,
   language: string,
-  themeScheme: AppThemeScheme
+  themeScheme: AppThemeScheme,
 ): { html: string; cssRules: string; durationMs: number } {
   const _startTimeMs = Date.now();
   const isLanguageSupportedByPrism = !!(

...
@@ -62,7 +62,7 @@ const getNodeTextRecursive = (node: TextNode | RealNode, depth = 0): string => {
 
 const getNodesRecursive = (
   node: TextNode | RealNode,
-  depth = 0
+  depth = 0,
 ): { text: string; type: string }[] => {
   if (depth > 1000) throw new Error("Too much recursion.");
   return node.type === "text"

...
@@ -78,8 +78,8 @@ const getNodesRecursive = (
           childNode.type === "text"
             ? "text"
             : childNode.attributes[0].key === "class"
-            ? childNode.attributes[0].value.replace(/^token /i, "")
-            : "attr",
+              ? childNode.attributes[0].value.replace(/^token /i, "")
+              : "attr",
       }));
 };
 

...
@@ -111,17 +111,20 @@ const highlightCodeAction: ReqHandler<
     return reply.status(200).send(result);
   } else if (outputFormat === "json") {
     const parsedJson = parseHtmlToJson(result.html);
-    const tokens = parsedJson.reduce((acc, node) => {
-      if (node.type === "text") {
-        acc = [
-          ...acc,
-          { text: node.content.replace(/\r\n/i, "\n"), type: "text" },
-        ];
-      } else {
-        acc = [...acc, ...getNodesRecursive(node)];
-      }
-      return acc;
-    }, [] as { text: string; type: string }[]);
+    const tokens = parsedJson.reduce(
+      (acc, node) => {
+        if (node.type === "text") {
+          acc = [
+            ...acc,
+            { text: node.content.replace(/\r\n/i, "\n"), type: "text" },
+          ];
+        } else {
+          acc = [...acc, ...getNodesRecursive(node)];
+        }
+        return acc;
+      },
+      [] as { text: string; type: string }[],
+    );
 
     return reply.status(200).send(tokens);
   }

file deleted
app/hocs/withLayoutCounters.tsx
@@ -1,58 +0,0 @@
-// 3rd-party
-import React, { useEffect, useState } from "react";
-// app
-import type { LayoutProps, RepositoryCountersDTO } from "../types";
-import { Env } from "../env";
-
-// A lightweight HOC that loads SSR counters from /api/repos/:orgSlug/:repoSlug/counters
-// and injects them as `layoutCounters` prop into the wrapped Layout component.
-export function withLayoutCounters<P extends object>(
-  WrappedComponent: React.FC<P & LayoutProps>,
-): React.FC<P & LayoutProps> {
-  const ComponentWithCounters: React.FC<P & LayoutProps> = ({
-    orgSlug,
-    repoSlug,
-    ...props
-  }) => {
-    const [layoutCounters, setLayoutCounters] = useState<
-      RepositoryCountersDTO | undefined
-    >(undefined);
-
-    // SSR: on first render, layoutCounters may be pre-populated by the server.
-    // Client: fetch the API endpoint to hydrate counters.
-    useEffect(() => {
-      // SSR: allow server to inject counters via HTML if possible; fallback to fetch
-      const baseUrl = `${Env.DEPLOYMENT_SCHEME}://${Env.DEPLOYMENT_DOMAIN}${Env.DEPLOYMENT_SCHEME !== "https" ? `:${Env.PORT}` : ""}`;
-      const endpoint = `${baseUrl}/api/repos/${orgSlug}/${repoSlug}/counters`;
-
-      console.log("hoc(layoutCounters): will fetch url:", endpoint);
-
-      fetch(endpoint, { credentials: "same-origin" })
-        .then((r) => (r.ok ? r.json() : {}))
-        .then((data) => {
-          if (data && Object.keys(data).length > 0) {
-            console.log("hoc(layoutCounters): fetched data:", data);
-            setLayoutCounters(data);
-          }
-        })
-        .catch((err) => {
-          const { message } = err as Error;
-          console.error("hoc(layoutCounters): fetch error:", message);
-        });
-    }, [orgSlug, repoSlug]);
-
-    const WrappedComponentEl = WrappedComponent as React.FC<P & LayoutProps>;
-
-    return (
-      <WrappedComponentEl
-        {...(props as unknown as P)}
-        {...(props as unknown as LayoutProps)}
-        orgSlug={orgSlug}
-        repoSlug={repoSlug}
-        layoutCounters={layoutCounters}
-      />
-    );
-  };
-
-  return ComponentWithCounters;
-}

@@ -37,6 +37,7 @@ import { makeGitServerService } from "./services/gitServer";
 import {
   getEnv,
   getGitStamp,
+  loadRepositoryCounters,
   localAppDomainPreHandler,
   makeRequestHandler,
   sessionSetupPreHandler,

...
@@ -337,6 +338,9 @@ async function main(): Promise<AppServer> {
         },
       });
 
+      // load repository counters before rendering the page
+      s.addHook("preHandler", loadRepositoryCounters);
+
       // check that a session is started, or start it.
       s.addHook("preHandler", sessionSetupPreHandler);
 

app/services/repository/index.ts
@@ -23,6 +23,7 @@ import { default as makeGetRepositoryRefDiff } from "./getRepositoryRefDiff";
 import { default as makeGetRepositoryRemoteRefDiff } from "./getRepositoryRemoteRefDiff";
 import { default as makeGetRepositoryTags } from "./getRepositoryTags";
 import { default as makeIsFileInRepositoryPath } from "./isFileInRepositoryPath";
+import { default as makeGetRepositoryCounters } from "./getRepositoryCounters";
 
 export const makeRepositoryService = makeService<
   RepositoryServiceAPI,

...
@@ -48,4 +49,5 @@ export const makeRepositoryService = makeService<
   getRepositoryRemoteRefDiff: makeGetRepositoryRemoteRefDiff,
   getRepositoryTags: makeGetRepositoryTags,
   isFileInRepositoryPath: makeIsFileInRepositoryPath,
+  getRepositoryCounters: makeGetRepositoryCounters,
 });

app/services/repository/types.ts
@@ -8,6 +8,7 @@ import type { FastifyRequest } from "fastify";
 import type { Organization, Repository, User } from "@prisma/client";
 // app
 import type {
+  RepositoryCountersDTO,
   RepositoryFile,
   RepositoryFileContent,
   RepositoryFileDiff,

...
@@ -50,29 +51,29 @@ export interface ForkRepositoryDTO {
 export interface RepositoryServiceAPI extends ServiceApiContract {
   canUserAccessRepository(
     user: User | null,
-    repo: Repository
+    repo: Repository,
   ): Promise<boolean>;
   createRepository(dto: CreateRepositoryDTO): Promise<Repository>;
   forkRepository(dto: ForkRepositoryDTO): Promise<Repository>;
   getCurrentUserRepositoryForks(
-    repository: Repository
+    repository: Repository,
   ): Promise<RepositoryWithParentAndForkedFromRepos[]>;
   getRepository(
     orgSlug: string,
-    repoSlug: string
+    repoSlug: string,
   ): Promise<RepositoryWithForkedFromRepo | null>;
   getRepositoryBranches(
     repository: Repository,
-    onlyLocalBranches?: boolean
+    onlyLocalBranches?: boolean,
   ): Promise<string[]>;
   getRepositoryById(
-    repoId: string
+    repoId: string,
   ): Promise<RepositoryWithForkedFromRepo | null>;
   getRepositoryCommitLog(
     repository: Repository,
     path?: string,
     ref?: string,
-    onlyLast?: boolean
+    onlyLast?: boolean,
   ): Promise<RepositoryLog[]>;
   getRepositoryExploreCollection(): Promise<
     (Repository & { parentOrg: Organization })[]

...
@@ -80,46 +81,50 @@ export interface RepositoryServiceAPI extends ServiceApiContract {
   getRepositoryFileContent(
     repository: Repository,
     path: string,
-    ref?: string
+    ref?: string,
   ): Promise<null | RepositoryFileContent>;
   getRepositoryFileContentBase64(
     repository: Repository,
     path: string,
-    ref?: string
+    ref?: string,
   ): Promise<null | RepositoryFileContent>;
   getRepositoryFiles(
     repository: Repository,
     path?: string,
-    ref?: string
+    ref?: string,
   ): Promise<RepositoryFile[]>;
   getRepositoryHead(
     repository: Repository,
-    ref?: string
+    ref?: string,
   ): Promise<RepositoryHead>;
   getRepositoryHTTPCloneUrl(repository: Repository): Promise<string>;
   getRepositorySSHCloneUrl(repository: Repository): Promise<string>;
   getRepositoryObject(
     repository: Repository,
-    objectId: String
+    objectId: String,
   ): Promise<RepositoryObject | null>;
   getRepositoryRefDiff(
     repository: Repository,
     refA: string,
-    refB?: string
+    refB?: string,
   ): Promise<RepositoryFileDiff[]>;
   getRepositoryRemoteRefDiff(
     sourceRepo: Repository,
     sourceFromBranch: string,
     targetRepo: Repository,
-    targetDestBranch: string
+    targetDestBranch: string,
   ): Promise<RepositoryFileDiff[]>;
   getRepositoryTags(repository: Repository): Promise<string[]>;
   isFileInRepositoryPath(
     repository: Repository,
     path: string,
     filesToMatch: string[],
-    ref?: string
+    ref?: string,
   ): Promise<string[]>;
+  getRepositoryCounters(
+    orgSlug: string,
+    repoSlug: string,
+  ): Promise<RepositoryCountersDTO>;
 }
 
 export interface RepositoryServiceDeps {

@@ -38,6 +38,7 @@ export interface CommonViewProps {
   themeScheme: AppThemeScheme;
   title?: string;
   path?: string;
+  layoutCounters?: RepositoryCountersDTO;
 }
 
 export type CommonProps = { commonProps: CommonViewProps };

app/utils/server/index.ts
@@ -8,3 +8,4 @@ export { guestOrRedirect } from "./guestOrRedirect";
 export { localAppDomainPreHandler } from "./localAppDomainPreHandler";
 export { makeRequestHandler } from "./makeRequestHandler";
 export { sessionSetupPreHandler } from "./sessionSetupPreHandler";
+export { loadRepositoryCounters } from "./loadRepositoryCounters";

app/utils/server/makeRequestHandler.ts
@@ -10,7 +10,7 @@ import { Const } from "../../const";
 export const makeRequestHandler = {
   getter: () => {
     return (request: FastifyRequest, reply: FastifyReply) => {
-      return <T extends Record<string, unknown>>(
+      return async <T extends Record<string, unknown>>(
         viewName: string,
         props?: T & CommonViewProps,
         viewCtx?: ViewContext,

...
@@ -50,6 +50,7 @@ export const makeRequestHandler = {
             themeScheme,
             title,
             path: request.url,
+            layoutCounters: reply.context.layoutCounters,
           },
         } as T & { commonProps: CommonViewProps };