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 + + + +
+