diff --git a/.vscode/settings.json b/.vscode/settings.json
index 8e47f58c..e45fd89d 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -4,5 +4,8 @@
"editor.formatOnSave": true,
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
+ },
+ "[typescript]": {
+ "editor.defaultFormatter": "biomejs.biome"
}
}
diff --git a/apps/hub/src/routeTree.gen.ts b/apps/hub/src/routeTree.gen.ts
index 9680fb94..ed5c9632 100644
--- a/apps/hub/src/routeTree.gen.ts
+++ b/apps/hub/src/routeTree.gen.ts
@@ -8,8 +8,6 @@
// This file is auto-generated by TanStack Router
-import { createFileRoute } from '@tanstack/react-router'
-
// Import Routes
import { Route as rootRoute } from './routes/__root'
@@ -41,6 +39,7 @@ import { Route as AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdIndexImpo
import { Route as AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdVersionsImport } from './routes/_authenticated/_layout/games/$gameId_/environments/$namespaceId/versions'
import { Route as AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdTokensImport } from './routes/_authenticated/_layout/games/$gameId_/environments/$namespaceId/tokens'
import { Route as AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdServersImport } from './routes/_authenticated/_layout/games/$gameId_/environments/$namespaceId/servers'
+import { Route as AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdModulesImport } from './routes/_authenticated/_layout/games/$gameId_/environments/$namespaceId/modules'
import { Route as AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdMatchmakerImport } from './routes/_authenticated/_layout/games/$gameId_/environments/$namespaceId/matchmaker'
import { Route as AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdLobbiesImport } from './routes/_authenticated/_layout/games/$gameId_/environments/$namespaceId/lobbies'
import { Route as AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdCdnImport } from './routes/_authenticated/_layout/games/$gameId_/environments/$namespaceId/cdn'
@@ -54,13 +53,6 @@ import { Route as AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdLobbiesLo
import { Route as AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdBackendVariablesImport } from './routes/_authenticated/_layout/games/$gameId_/environments/$namespaceId/backend/variables'
import { Route as AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdBackendLogsImport } from './routes/_authenticated/_layout/games/$gameId_/environments/$namespaceId/backend/logs'
-// Create Virtual Routes
-
-const AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdModulesLazyImport =
- createFileRoute(
- '/_authenticated/_layout/games/$gameId/environments/$namespaceId/modules',
- )()
-
// Create/Update Routes
const AuthenticatedRoute = AuthenticatedImport.update({
@@ -210,19 +202,6 @@ const AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdIndexRoute =
AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdRoute,
} as any)
-const AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdModulesLazyRoute =
- AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdModulesLazyImport.update(
- {
- path: '/modules',
- getParentRoute: () =>
- AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdRoute,
- } as any,
- ).lazy(() =>
- import(
- './routes/_authenticated/_layout/games/$gameId_/environments/$namespaceId/modules.lazy'
- ).then((d) => d.Route),
- )
-
const AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdVersionsRoute =
AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdVersionsImport.update({
path: '/versions',
@@ -244,6 +223,13 @@ const AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdServersRoute =
AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdRoute,
} as any)
+const AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdModulesRoute =
+ AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdModulesImport.update({
+ path: '/modules',
+ getParentRoute: () =>
+ AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdRoute,
+ } as any)
+
const AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdMatchmakerRoute =
AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdMatchmakerImport.update({
path: '/matchmaker',
@@ -544,6 +530,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdMatchmakerImport
parentRoute: typeof AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdImport
}
+ '/_authenticated/_layout/games/$gameId/environments/$namespaceId/modules': {
+ id: '/_authenticated/_layout/games/$gameId/environments/$namespaceId/modules'
+ path: '/modules'
+ fullPath: '/games/$gameId/environments/$namespaceId/modules'
+ preLoaderRoute: typeof AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdModulesImport
+ parentRoute: typeof AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdImport
+ }
'/_authenticated/_layout/games/$gameId/environments/$namespaceId/servers': {
id: '/_authenticated/_layout/games/$gameId/environments/$namespaceId/servers'
path: '/servers'
@@ -565,13 +558,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdVersionsImport
parentRoute: typeof AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdImport
}
- '/_authenticated/_layout/games/$gameId/environments/$namespaceId/modules': {
- id: '/_authenticated/_layout/games/$gameId/environments/$namespaceId/modules'
- path: '/modules'
- fullPath: '/games/$gameId/environments/$namespaceId/modules'
- preLoaderRoute: typeof AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdModulesLazyImport
- parentRoute: typeof AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdImport
- }
'/_authenticated/_layout/games/$gameId/environments/$namespaceId/': {
id: '/_authenticated/_layout/games/$gameId/environments/$namespaceId/'
path: '/'
@@ -690,6 +676,7 @@ export const routeTree = rootRoute.addChildren({
},
),
AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdMatchmakerRoute,
+ AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdModulesRoute,
AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdServersRoute:
AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdServersRoute.addChildren(
{
@@ -699,7 +686,6 @@ export const routeTree = rootRoute.addChildren({
),
AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdTokensRoute,
AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdVersionsRoute,
- AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdModulesLazyRoute,
AuthenticatedLayoutGamesGameIdEnvironmentsNamespaceIdIndexRoute,
}),
}),
@@ -849,10 +835,10 @@ export const routeTree = rootRoute.addChildren({
"/_authenticated/_layout/games/$gameId/environments/$namespaceId/cdn",
"/_authenticated/_layout/games/$gameId/environments/$namespaceId/lobbies",
"/_authenticated/_layout/games/$gameId/environments/$namespaceId/matchmaker",
+ "/_authenticated/_layout/games/$gameId/environments/$namespaceId/modules",
"/_authenticated/_layout/games/$gameId/environments/$namespaceId/servers",
"/_authenticated/_layout/games/$gameId/environments/$namespaceId/tokens",
"/_authenticated/_layout/games/$gameId/environments/$namespaceId/versions",
- "/_authenticated/_layout/games/$gameId/environments/$namespaceId/modules",
"/_authenticated/_layout/games/$gameId/environments/$namespaceId/"
]
},
@@ -890,6 +876,10 @@ export const routeTree = rootRoute.addChildren({
"filePath": "_authenticated/_layout/games/$gameId_/environments/$namespaceId/matchmaker.tsx",
"parent": "/_authenticated/_layout/games/$gameId/environments/$namespaceId"
},
+ "/_authenticated/_layout/games/$gameId/environments/$namespaceId/modules": {
+ "filePath": "_authenticated/_layout/games/$gameId_/environments/$namespaceId/modules.tsx",
+ "parent": "/_authenticated/_layout/games/$gameId/environments/$namespaceId"
+ },
"/_authenticated/_layout/games/$gameId/environments/$namespaceId/servers": {
"filePath": "_authenticated/_layout/games/$gameId_/environments/$namespaceId/servers.tsx",
"parent": "/_authenticated/_layout/games/$gameId/environments/$namespaceId",
@@ -906,10 +896,6 @@ export const routeTree = rootRoute.addChildren({
"filePath": "_authenticated/_layout/games/$gameId_/environments/$namespaceId/versions.tsx",
"parent": "/_authenticated/_layout/games/$gameId/environments/$namespaceId"
},
- "/_authenticated/_layout/games/$gameId/environments/$namespaceId/modules": {
- "filePath": "_authenticated/_layout/games/$gameId_/environments/$namespaceId/modules.lazy.tsx",
- "parent": "/_authenticated/_layout/games/$gameId/environments/$namespaceId"
- },
"/_authenticated/_layout/games/$gameId/environments/$namespaceId/": {
"filePath": "_authenticated/_layout/games/$gameId_/environments/$namespaceId/index.tsx",
"parent": "/_authenticated/_layout/games/$gameId/environments/$namespaceId"
diff --git a/apps/hub/src/routes/_authenticated/_layout/games/$gameId_/environments/$namespaceId/modules.tsx b/apps/hub/src/routes/_authenticated/_layout/games/$gameId_/environments/$namespaceId/modules.tsx
new file mode 100644
index 00000000..576dbd80
--- /dev/null
+++ b/apps/hub/src/routes/_authenticated/_layout/games/$gameId_/environments/$namespaceId/modules.tsx
@@ -0,0 +1,23 @@
+import { ModulesStore, loadModuleCategories } from "@rivet-gg/components";
+import { createFileRoute } from "@tanstack/react-router";
+
+function GameIdModules() {
+ const { categories } = Route.useLoaderData();
+ return (
+ <>
+
+ >
+ );
+}
+
+export const Route = createFileRoute(
+ "/_authenticated/_layout/games/$gameId/environments/$namespaceId/modules",
+)({
+ component: GameIdModules,
+ loader: async () => {
+ const categories = await loadModuleCategories();
+ return {
+ categories,
+ };
+ },
+});
diff --git a/package.json b/package.json
index 54ebf517..c8b7d4b4 100644
--- a/package.json
+++ b/package.json
@@ -2,10 +2,7 @@
"private": true,
"packageManager": "yarn@4.2.2",
"name": "hub",
- "workspaces": [
- "packages/*",
- "apps/*"
- ],
+ "workspaces": ["packages/*", "apps/*"],
"scripts": {
"start": "npx turbo dev",
"build": "npx turbo build",
diff --git a/packages/components/package.json b/packages/components/package.json
index 6d7ce8b9..e756c37d 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -3,11 +3,7 @@
"private": true,
"version": "1.0.0",
"type": "module",
- "files": [
- "dist",
- "src",
- "public"
- ],
+ "files": ["dist", "src", "public"],
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"exports": {
@@ -68,7 +64,7 @@
"input-otp": "^1.2.3",
"lucide-react": "^0.439.0",
"react": "^18.2.0",
- "react-day-picker": "^8.10.1",
+ "react-day-picker": "^9.0.9",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.1",
"react-resizable-panels": "^2.0.19",
diff --git a/packages/components/src/action-card.tsx b/packages/components/src/action-card.tsx
index aeee8f49..c8ab330e 100644
--- a/packages/components/src/action-card.tsx
+++ b/packages/components/src/action-card.tsx
@@ -1,3 +1,4 @@
+"use client";
import type { ReactNode } from "react";
import {
Card,
diff --git a/packages/components/src/animated-currency.tsx b/packages/components/src/animated-currency.tsx
index cc9ffe5c..8040dcc2 100644
--- a/packages/components/src/animated-currency.tsx
+++ b/packages/components/src/animated-currency.tsx
@@ -1,3 +1,4 @@
+"use client";
import {
LazyMotion,
animate,
diff --git a/packages/components/src/asset-image.tsx b/packages/components/src/asset-image.tsx
index 095e8d02..61e9f4b9 100644
--- a/packages/components/src/asset-image.tsx
+++ b/packages/components/src/asset-image.tsx
@@ -1,3 +1,4 @@
+"use client";
import { useConfig } from "./lib/config";
export function AssetImage(
diff --git a/packages/components/src/auto-form/fields/file.tsx b/packages/components/src/auto-form/fields/file.tsx
index 6f2390c7..a9c0925c 100644
--- a/packages/components/src/auto-form/fields/file.tsx
+++ b/packages/components/src/auto-form/fields/file.tsx
@@ -1,3 +1,4 @@
+"use client";
import { faTrash } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { type ChangeEvent, useState } from "react";
diff --git a/packages/components/src/auto-form/fields/union.tsx b/packages/components/src/auto-form/fields/union.tsx
index 5a9db4b0..bec891a2 100644
--- a/packages/components/src/auto-form/fields/union.tsx
+++ b/packages/components/src/auto-form/fields/union.tsx
@@ -1,3 +1,4 @@
+"use client";
import { useState } from "react";
import type * as z from "zod";
import { FormControl, FormItem, FormMessage } from "../../ui/form";
diff --git a/packages/components/src/auto-form/index.tsx b/packages/components/src/auto-form/index.tsx
index ee0136a7..2f830883 100644
--- a/packages/components/src/auto-form/index.tsx
+++ b/packages/components/src/auto-form/index.tsx
@@ -1,3 +1,4 @@
+"use client";
import React from "react";
import { type DefaultValues, type FormState, useForm } from "react-hook-form";
import type { z } from "zod";
diff --git a/packages/components/src/copy-area.tsx b/packages/components/src/copy-area.tsx
index cc0719ee..9bce293e 100644
--- a/packages/components/src/copy-area.tsx
+++ b/packages/components/src/copy-area.tsx
@@ -1,3 +1,4 @@
+"use client";
import { faCopy } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Slot } from "@radix-ui/react-slot";
diff --git a/packages/components/src/datepicker.tsx b/packages/components/src/datepicker.tsx
index 4935698e..fdffff0f 100644
--- a/packages/components/src/datepicker.tsx
+++ b/packages/components/src/datepicker.tsx
@@ -69,12 +69,7 @@ export function DatePicker({
) : null}
-
+
);
@@ -143,7 +138,6 @@ export function RangeDatePicker({
) : null}
+
+
+ Docs
+
+
+
+
+ Support
+
+
+
+ ),
}: HeaderProps) {
return (
@@ -60,26 +75,7 @@ export function Header({
{mobileBreadcrumbs}
-
-
-
- Docs
-
-
-
-
- Support
-
-
-
+ {support}
diff --git a/packages/components/src/hooks/use-dialog.tsx b/packages/components/src/hooks/use-dialog.tsx
index e19c02a9..af2cd4f7 100644
--- a/packages/components/src/hooks/use-dialog.tsx
+++ b/packages/components/src/hooks/use-dialog.tsx
@@ -1,3 +1,4 @@
+"use client";
import {
type ComponentProps,
type ComponentType,
diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts
index 71dc84d9..15fca40c 100644
--- a/packages/components/src/index.ts
+++ b/packages/components/src/index.ts
@@ -22,6 +22,9 @@ export * from "./animated-currency";
export * from "./live-badge";
export * from "./relative-time";
export * from "./code";
+export * from "./modules-store";
+export * from "./module-icon";
+export * from "./module-card";
export * from "./ui/typography";
export * from "./ui/skeleton";
export * from "./ui/sheet";
@@ -68,6 +71,7 @@ export * from "./lib/formatter";
export * from "./lib/exit-signals";
export * from "./lib/config";
export * from "./lib/create-schema-form";
+export * from "./lib/modules";
export * from "./auto-form";
export * from "./hooks";
export * as styleHelpers from "./ui/helpers/index";
diff --git a/packages/components/src/lib/config.ts b/packages/components/src/lib/config.ts
index 335286c5..e3e22366 100644
--- a/packages/components/src/lib/config.ts
+++ b/packages/components/src/lib/config.ts
@@ -1,3 +1,4 @@
+"use client";
import { createContext, useContext } from "react";
interface Config {
@@ -10,30 +11,30 @@ interface Config {
sentry?: {
dsn: string;
projectId: string;
- }
- outerbaseProviderToken: string,
+ };
+ outerbaseProviderToken: string;
}
export const ConfigContext = createContext({
apiUrl: "",
assetsUrl: "",
- outerbaseProviderToken: '',
+ outerbaseProviderToken: "",
});
export const useConfig = () => useContext(ConfigContext);
export const ConfigProvider = ConfigContext.Provider;
const getApiEndpoint = (apiEndpoint: string) => {
- if (apiEndpoint == '__AUTO__') {
- if (location.hostname.startsWith('hub.')) {
+ if (apiEndpoint == "__AUTO__") {
+ if (location.hostname.startsWith("hub.")) {
// Connect to the corresponding API endpoint
- return 'https://' + location.hostname.replace('hub.', 'api.');
+ return "https://" + location.hostname.replace("hub.", "api.");
} else {
// Default to staging servers for all other endpoints
- return 'https://api.staging2.gameinc.io';
+ return "https://api.staging2.gameinc.io";
}
}
return apiEndpoint;
-}
+};
export const getConfig = (): Config => {
const el = document.getElementById("RIVET_CONFIG");
@@ -46,5 +47,5 @@ export const getConfig = (): Config => {
return {
...parsed,
apiUrl: getApiEndpoint(parsed.apiUrl),
- }
+ };
};
diff --git a/packages/components/src/lib/create-schema-form.tsx b/packages/components/src/lib/create-schema-form.tsx
index 30194263..3d5388bc 100644
--- a/packages/components/src/lib/create-schema-form.tsx
+++ b/packages/components/src/lib/create-schema-form.tsx
@@ -1,3 +1,4 @@
+'use client';
import { Button, type ButtonProps, Form } from "@rivet-gg/components";
import { zodResolver } from "@hookform/resolvers/zod";
import { type ComponentProps, type ReactNode, useEffect } from "react";
diff --git a/packages/components/src/lib/modules.ts b/packages/components/src/lib/modules.ts
new file mode 100644
index 00000000..2ac5e713
--- /dev/null
+++ b/packages/components/src/lib/modules.ts
@@ -0,0 +1,141 @@
+const CATEGORIES = [
+ {
+ name: 'Multiplayer',
+ description: 'Engage players with live multiplayer gameplay, fostering competition and cooperation.',
+ slug: 'multiplayer'
+ },
+ {
+ name: 'Authentication',
+ description: 'Secure and manage user accounts to personalize and safeguard the player experience.',
+ slug: 'auth'
+ },
+ {
+ name: 'Social',
+ description: 'Facilitate player interaction and community-building to boost engagement and retention.',
+ slug: 'social'
+ },
+ {
+ name: 'Economy',
+ description: 'Drive player progression and monetization with virtual goods and currencies.',
+ slug: 'economy'
+ },
+ // {
+ // name: "Monetization",
+ // description: "TODO",
+ // slug: "monetization",
+ // },
+ {
+ name: 'Competitive',
+ description: 'Motivate and reward skilled play with rankings, tournaments, and leagues.',
+ slug: 'competitive'
+ },
+ {
+ name: 'Analytics',
+ description: 'Gain actionable insights to optimize game design, balance, and monetization.',
+ slug: 'analytics'
+ },
+ // {
+ // name: "Monitoring",
+ // description: "TODO",
+ // slug: "monitoring",
+ // },
+ {
+ name: 'Security',
+ description: 'Protect your game and players from cheating, hacking, and disruptive behavior.',
+ slug: 'security'
+ },
+ {
+ name: 'Utility',
+ description: 'Streamline development with foundational tools and reusable components.',
+ slug: 'utility'
+ },
+ {
+ name: 'Platform',
+ description: "Extend your game's reach and engage players across popular gaming platforms.",
+ slug: 'platform'
+ },
+ // {
+ // name: "Infrastructure",
+ // description: "Extend and integrate your game with custom backend services and third-party APIs.",
+ // slug: "infra",
+ // },
+ {
+ name: 'Service',
+ description: 'Integrate third-party services to enhance functionality and streamline operations.',
+ slug: 'service'
+ }
+];
+
+export async function loadModulesMeta() {
+ const versionMetaResponse = await fetch('https://releases.rivet.gg/backend/index.json');
+ const { latestVersion } = await versionMetaResponse.json();
+ const modulesMetaResponse = await fetch(`https://releases.rivet.gg/backend/${latestVersion}/index.json`);
+ return await modulesMetaResponse.json();
+}
+
+export async function loadModuleMeta(module: string) {
+ const meta = await loadModulesMeta();
+ const moduleMeta = meta.modules[module];
+ return {
+ ...moduleMeta,
+ category: CATEGORIES.find(category => moduleMeta.config.tags.indexOf(category.slug) !== -1) ?? {name: "Uncategorized", slug: "uncategorized", description: ""},
+ config: {
+ ...moduleMeta.config,
+ dependencies: Object.fromEntries(
+ Object.keys(moduleMeta.config.dependencies || {}).map(dep => [dep, meta.modules[dep]])
+ )
+ }
+ };
+}
+
+
+export interface Category {
+ name: string;
+ slug: string;
+ description: string;
+ modules: { id: string; module: any }[];
+}
+
+export async function loadModuleCategories() {
+ const meta = await loadModulesMeta();
+ const unsortedModules = new Set(Object.keys(meta.modules));
+ const allCategories: Category[] = [];
+ for (let categoryConfig of CATEGORIES) {
+ const category: Category = {
+ ...categoryConfig,
+ modules: []
+ };
+ allCategories.push(category);
+
+ // Find modules
+ for (const moduleId of new Set(unsortedModules)) {
+ const mod = meta.modules[moduleId]!;
+ if (mod.config.tags?.indexOf('internal') != -1) {
+ unsortedModules.delete(moduleId);
+ continue;
+ }
+
+ if (mod.config.tags.indexOf(category.slug) == -1) continue;
+
+ // Add to category
+ unsortedModules.delete(moduleId);
+ category.modules.push({ id: moduleId, module: mod });
+ }
+
+ // Sort modules
+ category.modules = category.modules.sort((a, b) => {
+ // Sink 'coming_soon' modules to the bottom
+ if (a.module.config.status === 'coming_soon' && b.module.config.status !== 'coming_soon') return 1;
+ if (a.module.config.status !== 'coming_soon' && b.module.config.status === 'coming_soon') return -1;
+ // For modules with the same status, sort alphabetically
+ return a.module.config.name!.localeCompare(b.module.config.name!);
+ });
+ }
+
+ // Check for unsorted modules
+ if (unsortedModules.size !== 0) {
+ throw new Error(`Modules do no have tag matching a category: ${[...unsortedModules].join(', ')}`);
+ }
+
+ return allCategories;
+}
\ No newline at end of file
diff --git a/packages/components/src/logs-view.tsx b/packages/components/src/logs-view.tsx
index 40e8cf43..9a529ba1 100644
--- a/packages/components/src/logs-view.tsx
+++ b/packages/components/src/logs-view.tsx
@@ -1,3 +1,4 @@
+"use client";
import { faArrowDownToLine } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import type { Virtualizer } from "@tanstack/react-virtual";
diff --git a/packages/components/src/module-card.tsx b/packages/components/src/module-card.tsx
new file mode 100644
index 00000000..b0a0c52d
--- /dev/null
+++ b/packages/components/src/module-card.tsx
@@ -0,0 +1,106 @@
+"use client";
+import type { IconProp } from "@fortawesome/fontawesome-svg-core";
+import { faExternalLink } from "@fortawesome/pro-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { motion } from "framer-motion";
+import { Suspense, lazy } from "react";
+import { Card, CardDescription, CardHeader, CardTitle } from "./ui/card";
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger,
+} from "./ui/sheet";
+import { Link, Text } from "./ui/typography";
+
+const ModuleIcon = lazy(async () => ({
+ default: (await import("./module-icon")).ModuleIcon,
+}));
+
+const animationProps = {
+ initial: { opacity: 0 },
+ animate: { opacity: 1 },
+ exit: { opacity: 0 },
+};
+
+interface ModuleCardProps {
+ id: string;
+ name: string;
+ description: string;
+ status: string;
+ icon: string;
+ layoutAnimation?: boolean;
+ onClick?: () => void;
+}
+
+export function ModuleCard({
+ id,
+ name,
+ icon,
+ description,
+ layoutAnimation = true,
+ onClick,
+}: ModuleCardProps) {
+ const moduleCard = (
+
+
+
+
+
+
+
+ {name}
+
+
+ {description}
+
+
+
+
+ );
+
+ if (onClick) {
+ return moduleCard;
+ }
+
+ return (
+
+ {moduleCard}
+
+
+ {name}
+
+
+ Open in New Tab
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/components/src/module-icon.tsx b/packages/components/src/module-icon.tsx
new file mode 100644
index 00000000..800491a4
--- /dev/null
+++ b/packages/components/src/module-icon.tsx
@@ -0,0 +1,26 @@
+import {
+ type IconPack,
+ type IconProp,
+ library,
+} from "@fortawesome/fontawesome-svg-core";
+import { fab } from "@fortawesome/free-brands-svg-icons";
+import { fas } from "@fortawesome/pro-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+const fasFab: IconPack = Object.fromEntries(
+ Object.entries(fab).map(([iconName, icon]) => [
+ iconName,
+ { ...icon, prefix: "fas" },
+ ]),
+);
+
+library.add(fasFab, fas);
+
+interface ModuleIconProps {
+ className?: string;
+ icon: IconProp;
+}
+
+export function ModuleIcon({ className, icon }: ModuleIconProps) {
+ return ;
+}
diff --git a/apps/hub/src/routes/_authenticated/_layout/games/$gameId_/environments/$namespaceId/modules.lazy.tsx b/packages/components/src/modules-store.tsx
similarity index 76%
rename from apps/hub/src/routes/_authenticated/_layout/games/$gameId_/environments/$namespaceId/modules.lazy.tsx
rename to packages/components/src/modules-store.tsx
index c1929754..204fc191 100644
--- a/apps/hub/src/routes/_authenticated/_layout/games/$gameId_/environments/$namespaceId/modules.lazy.tsx
+++ b/packages/components/src/modules-store.tsx
@@ -1,41 +1,46 @@
-import { ModuleCard } from "@/components/module-card";
+"use client";
import {
faBellConcierge,
faHammer,
faPlus,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { LayoutGroup, motion } from "framer-motion";
+import { useState } from "react";
+import type { Category } from "./lib/modules";
+import { cn } from "./lib/utils";
+import { ModuleCard } from "./module-card";
+import { SidebarNavigation } from "./sidebar-navigation";
+import { SidebarPage } from "./sidebar-page";
+import { Button } from "./ui/button";
import {
- Button,
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
- Flex,
- Grid,
- H1,
- H2,
- Input,
- Lead,
- SidebarNavigation,
- SidebarPage,
- cn,
-} from "@rivet-gg/components";
-import { Link, createLazyFileRoute } from "@tanstack/react-router";
-import { LayoutGroup, motion } from "framer-motion";
-import { useState } from "react";
-import OpenGBMeta from "../../../../../../../../vendor/opengb-modules-meta.json";
+} from "./ui/card";
+import { Flex } from "./ui/flex";
+import { Grid } from "./ui/grid";
+import { Input } from "./ui/input";
+import { H1, H2, Lead } from "./ui/typography";
+
+interface ModulesStoreProps {
+ categories: Category[];
+ onModuleClick?: (module: Category["modules"][number]) => void;
+}
-function GameIdModules() {
+export function ModulesStore({ categories, onModuleClick }: ModulesStoreProps) {
const [query, setQuery] = useState("");
- const categories = OpenGBMeta.categories
+ const filteredCategories = categories
.map((category) => {
const modules = category.modules.filter(
- (module) =>
- module.name.toLowerCase().includes((query || "").toLowerCase()) ||
- module.description
+ ({ module }) =>
+ module.config.name
+ .toLowerCase()
+ .includes((query || "").toLowerCase()) ||
+ module.config.description
.toLowerCase()
.includes((query || "").toLowerCase()),
);
@@ -71,19 +76,19 @@ function GameIdModules() {
- {OpenGBMeta.categories.map((category) => (
- (
+ c.slug === category.slug)
+ "transition-opacity",
+ filteredCategories.find((c) => c.slug === category.slug)
? "text-foreground"
: "opacity-50",
)}
>
{category.name}
-
+
))}