This commit is contained in:
 Илья Шалонин 2025-10-29 13:43:43 +05:00
commit 8538ab969a
434 changed files with 44051 additions and 0 deletions

View file

@ -0,0 +1,9 @@
import type { AppRouter } from "@repo/api";
import { getBaseUrl } from "@repo/utils";
import { hc } from "hono/client";
export const apiClient = hc<AppRouter>(getBaseUrl(), {
init: {
credentials: "include",
},
}).api;

View file

@ -0,0 +1,15 @@
"use server";
import { revalidatePath } from "next/cache";
export const clearCache = async (path?: string) => {
try {
if (path) {
revalidatePath(path);
} else {
revalidatePath("/", "layout");
}
} catch (error) {
console.error("Could not revalidate path", path, error);
}
};

View file

@ -0,0 +1,148 @@
import slugify from "slugify";
export type ContentStructureItem = {
label: string;
path: string;
children: ContentStructureItem[];
isPage: boolean;
};
export function getContentStructure({
documents,
meta,
locale,
}: {
documents: {
path: string;
locale: string;
title: string;
}[];
meta: {
path: string;
locale: string;
data: Record<string, string | { title: string }>;
}[];
locale?: string;
}) {
const contentStructure: ContentStructureItem[] = [];
function addToContentItemArray(
contentItemsArray: ContentStructureItem[],
subPath: string,
item: (typeof documents)[number],
) {
const pathParts = item.path
.replace(new RegExp(`^${subPath}[/]*`), "")
.split("/");
const rootItemPath = subPath
? [subPath, pathParts[0]].join("/")
: pathParts[0];
let rootItem = contentItemsArray.find(
(contentItem) => contentItem.path === rootItemPath,
);
const isPage = pathParts.length === 1;
if (!rootItem) {
const path = isPage ? item.path : rootItemPath;
const metaData = meta
.filter((meta) => meta.path === subPath)
.sort((page) => (page.locale === locale ? -1 : 1))
.at(0)?.data[pathParts[0]];
const label = metaData
? typeof metaData === "string"
? metaData
: metaData.title
: (documents
.filter((page) => page.path === rootItemPath)
.sort((page) => (page.locale === locale ? -1 : 1))
.at(0)?.title ?? pathParts[0]);
rootItem = {
label,
path,
children: [],
isPage,
};
contentItemsArray.push(rootItem);
}
if (isPage && !rootItem.isPage) {
rootItem.isPage = true;
}
if (pathParts.length > 1) {
addToContentItemArray(rootItem.children, rootItemPath, item);
}
}
documents.forEach((page) => {
addToContentItemArray(contentStructure, "", page);
});
// recusrively sort items and their children
function sortContentItems(items: ContentStructureItem[], basePath = "") {
items.sort((a, b) => {
if (a.path === "") {
return -1;
}
if (b.path === "") {
return 1;
}
const aIndex = Object.entries(
meta
.filter((meta) => meta.path === basePath)
.sort((page) => (page.locale === locale ? -1 : 1))
.at(0)?.data ?? {},
).findIndex(([key]) => key === a.path.replace(`${basePath}/`, ""));
const bIndex = Object.entries(
meta
.filter((meta) => meta.path === basePath)
.sort((page) => (page.locale === locale ? -1 : 1))
.at(0)?.data ?? {},
).findIndex(([key]) => key === b.path.replace(`${basePath}/`, ""));
// use position index from meta file or put the item at the end of the list
return (
(aIndex > -1 ? aIndex : items.length) -
(bIndex > -1 ? bIndex : items.length)
);
});
items.forEach((item) => {
if (item.children.length) {
sortContentItems(item.children, item.path);
}
});
}
sortContentItems(contentStructure);
return contentStructure;
}
export function getActivePathFromUrlParam(path: string | string[]) {
return Array.isArray(path) ? path.join("/") : path || "";
}
export function getLocalizedDocumentWithFallback<
T extends { path: string; locale: string },
>(documents: T[], path: string, locale: string) {
return documents
.filter((doc) => doc.path === path)
.sort((doc) => (doc.locale === locale ? -1 : 1))[0];
}
export function slugifyHeadline(headline: string) {
return slugify(headline, {
lower: true,
replacement: "-",
trim: true,
strict: true,
remove: /[*+~.()'"!:@]/g,
});
}

View file

@ -0,0 +1,74 @@
import type { Organization, Session } from "@repo/auth";
import { apiClient } from "@shared/lib/api-client";
import type { NextRequest } from "next/server";
export const getSession = async (req: NextRequest): Promise<Session | null> => {
const response = await fetch(
new URL(
"/api/auth/get-session?disableCookieCache=true",
req.nextUrl.origin,
),
{
headers: {
cookie: req.headers.get("cookie") || "",
},
},
);
if (!response.ok) {
return null;
}
try {
const session = await response.json();
return session;
} catch (error) {
console.error("Error parsing session JSON:", error);
return null;
}
};
export const getOrganizationsForSession = async (
req: NextRequest,
): Promise<Organization[]> => {
const response = await fetch(
new URL("/api/auth/organization/list", req.nextUrl.origin),
{
headers: {
cookie: req.headers.get("cookie") || "",
},
},
);
if (!response.ok) {
return [];
}
return (await response.json()) ?? [];
};
export const getPurchasesForSession = async (
req: NextRequest,
organizationId?: string,
) => {
const response = await apiClient.payments.purchases.$get(
{
query: {
organizationId,
},
},
{
headers: {
cookie: req.headers.get("cookie") || "",
},
},
);
if (!response.ok) {
return [];
}
const purchases = await response.json();
return purchases;
};

View file

@ -0,0 +1,35 @@
import {
QueryClient,
defaultShouldDehydrateQuery,
} from "@tanstack/react-query";
export function createQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 30 * 1000,
retry: false,
},
dehydrate: {
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) ||
query.state.status === "pending",
},
},
});
}
export function createQueryKeyWithParams(
key: string | string[],
params: Record<string, string | number>,
) {
return [
...(Array.isArray(key) ? key : [key]),
Object.entries(params)
.reduce((acc, [key, value]) => {
acc.push(`${key}:${value}`);
return acc;
}, [] as string[])
.join("_"),
] as const;
}

View file

@ -0,0 +1,21 @@
import "server-only";
import { createQueryClient } from "@shared/lib/query-client";
import type { AppRouter } from "@repo/api";
import { getBaseUrl } from "@repo/utils";
import { hc } from "hono/client";
import { headers } from "next/headers";
import { cache } from "react";
export const getServerQueryClient = cache(createQueryClient);
export const getServerApiClient = async () => {
const headerObject = Object.fromEntries((await headers()).entries());
return hc<AppRouter>(getBaseUrl(), {
init: {
credentials: "include",
headers: headerObject,
},
}).api;
};