diff --git a/apps/web/index.html b/apps/web/index.html index 8bd2860..9597990 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -2,7 +2,8 @@ - + + Manifold diff --git a/apps/web/src/features/dialog-manager/index.ts b/apps/web/src/features/dialog-manager/index.ts index 8bc5e9c..5991c39 100644 --- a/apps/web/src/features/dialog-manager/index.ts +++ b/apps/web/src/features/dialog-manager/index.ts @@ -5,6 +5,7 @@ import { FindDependencyDialog } from "~features/editor/components/find-dependenc import { PreviewDependencyDialog } from "~features/editor/components/preview-dependency-dialog"; import { TableDeleteDialog } from "~features/table/components/table-delete-dialog"; import { TablePublishDialog } from "~features/table/components/table-publish-dialog"; +import { CompareVersionsDialog } from "~features/table-version/components/compare-versions-dialog"; /** * @NOTE: Workaround for TypeScript not being able to infer the correct type of @@ -51,6 +52,10 @@ export const DIALOGS = { ID: "PREVIEW_DEPENDENCY", COMPONENT: NiceModal.create(PreviewDependencyDialog), }, + COMPARE_VERSIONS: { + ID: "COMPARE_VERSIONS", + COMPONENT: NiceModal.create(CompareVersionsDialog), + }, } as const; for (const dialog of Object.values(DIALOGS)) { diff --git a/apps/web/src/features/editor/components/preview-dependency-dialog/index.tsx b/apps/web/src/features/editor/components/preview-dependency-dialog/index.tsx index 2dec06f..a7c2db0 100644 --- a/apps/web/src/features/editor/components/preview-dependency-dialog/index.tsx +++ b/apps/web/src/features/editor/components/preview-dependency-dialog/index.tsx @@ -12,7 +12,11 @@ import { DrawerHeader, DrawerTitle, } from "@manifold/ui/components/ui/drawer"; +import { useCallback } from "react"; import { GoX } from "react-icons/go"; +import { useBlocker } from "react-router-dom"; + +import { useRouteChange } from "~features/routing/hooks/use-route-change"; type Props = { dependency: RouterOutput["table"]["findDependencies"][number]; @@ -27,6 +31,16 @@ export function PreviewDependencyDialog({ }: Props) { const modal = useModal(); + /** + * Prevent navigation when the modal is open + */ + useBlocker(modal.visible); + + /** + * Close the modal when a route change would occur + */ + useRouteChange(useCallback(() => modal.hide(), [modal])); + return ( - +
diff --git a/apps/web/src/features/routing/components/route-meta.ts b/apps/web/src/features/routing/components/route-meta.ts index 5b8064a..b06af0b 100644 --- a/apps/web/src/features/routing/components/route-meta.ts +++ b/apps/web/src/features/routing/components/route-meta.ts @@ -1,6 +1,7 @@ import { isError } from "@tanstack/react-query"; import { useEffect } from "react"; import { + type Params, type UIMatch, useMatches, useParams, @@ -18,6 +19,37 @@ const DEFAULT_TITLE = "Manifold | Embrace the chaos"; const DEFAULT_DESCRIPTION = "A tool for curating your collection of random tables."; +type RouteMetaData = { title: string; description: string }; + +function getRouteMetaData( + matchedRoute: UIMatch>, + params: Readonly>, +) { + const { handle, data } = matchedRoute; + + if (handle && data) { + return { + title: handle.title?.({ params, data }), + description: handle.description?.({ params, data }), + }; + } + + return { title: undefined, description: undefined }; +} + +function getMostSpecificRouteMetaData( + metadata: Partial[], +): RouteMetaData { + return metadata.reduce( + (acc, current) => { + return { + title: current.title || acc.title, + description: current.description || acc.description, + }; + }, + { title: DEFAULT_TITLE, description: DEFAULT_DESCRIPTION }, + ); +} /** * When the route changes, updates the document title. * @@ -37,18 +69,14 @@ export function RouteMeta() { const params = useParams(); const matches = useMatches() as UIMatch>[]; const error = useRouteError(); - - const { handle, data } = matches[matches.length - 1]; - - let title = DEFAULT_TITLE; - let description = DEFAULT_DESCRIPTION; + const allMatchesMeta = matches.map((match) => + getRouteMetaData(match, params), + ); + let { title, description } = getMostSpecificRouteMetaData(allMatchesMeta); if (error) { title = isError(error) ? error.message : "Something went wrong"; description = "We're not sure what happened, but we're looking into it."; - } else if (handle && data) { - title = handle.title?.({ params, data }) ?? DEFAULT_TITLE; - description = handle.description?.({ params, data }) ?? DEFAULT_DESCRIPTION; } useEffect(() => { diff --git a/apps/web/src/features/routing/hooks/use-route-change.ts b/apps/web/src/features/routing/hooks/use-route-change.ts new file mode 100644 index 0000000..76a7dd2 --- /dev/null +++ b/apps/web/src/features/routing/hooks/use-route-change.ts @@ -0,0 +1,15 @@ +import { useAtomValue } from "jotai"; +import { useEffect } from "react"; + +import { routerAtom } from "~features/routing/state"; + +/** + * Calls the provided callback when the route changes + */ +export function useRouteChange(callback: () => void) { + const router = useAtomValue(routerAtom); + + useEffect(() => { + return router?.subscribe(() => callback()); + }, [router, callback]); +} diff --git a/apps/web/src/features/routing/index.tsx b/apps/web/src/features/routing/index.tsx index 0901db6..89c4184 100644 --- a/apps/web/src/features/routing/index.tsx +++ b/apps/web/src/features/routing/index.tsx @@ -11,13 +11,14 @@ import { protectedLoaderBuilder, routerBuilder, } from "~features/routing/router"; -import { routesAtom } from "~features/routing/state"; +import { routerAtom, routesAtom } from "~features/routing/state"; import { trpc } from "~utils/trpc"; export function Router() { const session = useAuth(); const trpcUtils = trpc.useUtils(); const setRoutes = useSetAtom(routesAtom); + const setRouter = useSetAtom(routerAtom); const guestLoader = useMemo(() => guestLoaderBuilder(session), [session]); const protectedLoader = useMemo( @@ -39,6 +40,14 @@ export function Router() { setRoutes(routes); }, [routes, setRoutes]); + /** + * Make router instance available for descendents (e.g. Drawers/Dialogs) to + * listen to route changes + */ + useEffect(() => { + setRouter(routerInstance); + }, [routerInstance, setRouter]); + return ( <> diff --git a/apps/web/src/features/routing/router.tsx b/apps/web/src/features/routing/router.tsx index 9993249..21eaae3 100644 --- a/apps/web/src/features/routing/router.tsx +++ b/apps/web/src/features/routing/router.tsx @@ -106,8 +106,20 @@ export function buildAppRoutes({ lazy: loadTableDetailRoute(trpcUtils), }, { + id: "table-version-detail", path: "v/:version", lazy: loadTableVersionDetailRoute(trpcUtils), + children: [ + { + index: true, + lazy: () => + import("~features/table-version/pages/detail").then( + (mod) => ({ + Component: mod.TableVersionDetail, + }), + ), + }, + ], }, { path: "edit", diff --git a/apps/web/src/features/routing/state.ts b/apps/web/src/features/routing/state.ts index 8c17260..b4cd990 100644 --- a/apps/web/src/features/routing/state.ts +++ b/apps/web/src/features/routing/state.ts @@ -1,4 +1,7 @@ import { atom } from "jotai"; -import { RouteObject } from "react-router-dom"; +import { createBrowserRouter, RouteObject } from "react-router-dom"; export const routesAtom = atom([]); +export const routerAtom = atom | null>( + null, +); diff --git a/apps/web/src/features/table-version/components/compare-versions-dialog.tsx b/apps/web/src/features/table-version/components/compare-versions-dialog.tsx index dc1706c..79194c4 100644 --- a/apps/web/src/features/table-version/components/compare-versions-dialog.tsx +++ b/apps/web/src/features/table-version/components/compare-versions-dialog.tsx @@ -1,6 +1,6 @@ import { useModal } from "@ebay/nice-modal-react"; import type { RouterOutput } from "@manifold/router"; -import { ReactiveButton } from "@manifold/ui/components/reactive-button"; +import { FullScreenLoader } from "@manifold/ui/components/full-screen-loader"; import { TableIdentifier } from "@manifold/ui/components/table-identifier"; import { Button } from "@manifold/ui/components/ui/button"; import { @@ -12,127 +12,272 @@ import { DrawerHeader, DrawerTitle, } from "@manifold/ui/components/ui/drawer"; -import { GoX } from "react-icons/go"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, +} from "@manifold/ui/components/ui/select"; +import { useReturnFocus } from "@manifold/ui/hooks/use-return-focus"; +import type { Change, WordsOptions } from "diff"; +import { Fragment, useCallback, useEffect, useMemo, useState } from "react"; +import { GoArrowSwitch, GoDash, GoGitCompare, GoX } from "react-icons/go"; +import { useBlocker } from "react-router-dom"; + +import { useRouteChange } from "~features/routing/hooks/use-route-change"; type Props = { - dependency: RouterOutput["table"]["findDependencies"][number]; - onAddDependency: () => void; - canAddDependency: boolean; + versions: RouterOutput["tableVersion"]["get"]["versions"]; + table: RouterOutput["tableVersion"]["get"]["table"]; + currentVersion?: number; +}; + +// @NOTE: @types/diff is pretty poorly designed +type DiffModule = { + diffWords: ( + oldStr: string, + newStr: string, + options?: WordsOptions, + ) => Change[]; }; +function getInitialVersions( + versions: Props["versions"], + currentVersion?: number, +) { + // default current to the most recent version + const current = currentVersion ?? versions[0].version; + + // if it's the most recent version, compare it to the previous version + if (current === versions[0].version) { + return { + from: versions[1].version, + to: current, + }; + } + + // otherwise compare the current version to the most recent version + return { + from: current, + to: versions[0].version, + }; +} + export function CompareVersionsDialog({ - dependency, - onAddDependency, - canAddDependency, + versions, + table, + currentVersion, }: Props) { const modal = useModal(); + const [diff, setDiff] = useState(null); + const returnFocus = useReturnFocus(modal.visible); + const [{ from, to }] = useState(() => + getInitialVersions(versions, currentVersion), + ); + const [fromVersionNumber, setFromVersionNumber] = useState(String(from)); + const [toVersionNumber, setToVersionNumber] = useState(String(to)); + + const versionsMap = useMemo(() => { + return Object.fromEntries(versions.map((v) => [v.version, v])); + }, [versions]); + + const sourceVersion = versionsMap[fromVersionNumber]; + const targetVersion = versionsMap[toVersionNumber]; + + /** + * Prevent navigation when the modal is open + */ + useBlocker(modal.visible); + + /** + * Close the modal when a route change would occur + */ + useRouteChange(useCallback(() => modal.hide(), [modal])); + + /** + * Lazy load the diff library + */ + useEffect(() => { + async function resolveDiff() { + const module = await import("diff"); + setDiff(module); + } + + if (!diff) { + resolveDiff(); + } + }, [modal, diff]); + + const diffElements = useMemo(() => { + if (diff) { + const computedDiff = diff.diffWords( + sourceVersion.definition, + targetVersion.definition, + ); + + return computedDiff.map((part, index) => { + return ( + + {part.added ? ( + {part.value} + ) : part.removed ? ( + {part.value} + ) : ( + part.value + )} + + ); + }); + } + + return []; + }, [diff, sourceVersion, targetVersion]); return ( modal.hide()} + onClose={() => { + modal.hide(); + returnFocus(); + }} onAnimationEnd={() => modal.remove()} shouldScaleBackground // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus > - + + + + +
-
+
- - + +

+ Comparing versions of{" "} + +

- {dependency.table?.description || "No table description"} + Compare versions of table {table.title}
-
-
-
- Description -
-
- {dependency.table?.description || ( - - No table description - - )} -
- -
- Last updated -
-
- {dependency.createdAt.toLocaleDateString()} -
- -
- Version -
-
- {dependency.version} -
- -
- Release notes -
-
- {dependency.releaseNotes || ( - No release notes - )} -
-
- Available Tables -
-
- {dependency.availableTables.map((tableId) => ( - - {tableId} - - ))} -
-
- -
-
-                  {dependency.definition}
-                
+
+
+ + + + +
+ + {!diff ? ( + + ) : ( +
+
+

+ Version {sourceVersion.version} +

+
+                      {sourceVersion.definition}
+                    
+
+ +
+

+ Version {targetVersion.version} +

+
+                      {diffElements}
+                    
+
+
+ )}
- { - modal.hide(); - onAddDependency(); - }} - disabled={!canAddDependency} - > - {canAddDependency - ? "Add this dependency" - : "Dependency already added"} - +
+ +
- - - - ); } + +function VersionSelect({ + value, + onValueChange, + versions, + versionsMap, +}: { + value: string; + onValueChange: (value: string) => void; + versions: RouterOutput["tableVersion"]["get"]["versions"]; + versionsMap: Record< + string, + RouterOutput["tableVersion"]["get"]["versions"][number] + >; +}) { + return ( + + ); +} diff --git a/apps/web/src/features/table-version/pages/detail/index.ts b/apps/web/src/features/table-version/pages/detail/index.ts index 294d1c5..9be3ac4 100644 --- a/apps/web/src/features/table-version/pages/detail/index.ts +++ b/apps/web/src/features/table-version/pages/detail/index.ts @@ -1,2 +1,3 @@ +export { TableVersionLayout } from "./layout"; export { loaderBuilder, type TableVersionDetailLoaderData } from "./loader"; export { TableVersionDetail } from "./page"; diff --git a/apps/web/src/features/table-version/pages/detail/layout.tsx b/apps/web/src/features/table-version/pages/detail/layout.tsx new file mode 100644 index 0000000..fb05749 --- /dev/null +++ b/apps/web/src/features/table-version/pages/detail/layout.tsx @@ -0,0 +1,259 @@ +import { buildTableIdentifier } from "@manifold/lib"; +import type { RouterOutput } from "@manifold/router"; +import { FullScreenLoader } from "@manifold/ui/components/full-screen-loader"; +import { TableIdentifier } from "@manifold/ui/components/table-identifier"; +import { Button } from "@manifold/ui/components/ui/button"; +import { FlexCol } from "@manifold/ui/components/ui/flex"; +import { + Tooltip, + TooltipArrow, + TooltipContent, + TooltipTrigger, +} from "@manifold/ui/components/ui/tooltip"; +import { transitionAlpha } from "@manifold/ui/lib/animation"; +import { AnimatePresence, motion } from "framer-motion"; +import { + GoArrowLeft, + GoChevronLeft, + GoChevronRight, + GoCopy, + GoDiff, + GoPackage, + GoPencil, +} from "react-icons/go"; +import { Outlet, useLocation } from "react-router-dom"; + +import { DialogManager, DIALOGS } from "~features/dialog-manager"; +import { useRequiredUserProfile } from "~features/onboarding/hooks/use-required-user-profile"; +import { PrefetchableLink } from "~features/routing/components/prefetchable-link"; +import { useRouteParams } from "~features/routing/hooks/use-route-params"; +import { tableVersionDetailParams } from "~features/table-version/pages/detail/params"; +import { trpc } from "~utils/trpc"; + +export function TableVersionLayout() { + const location = useLocation(); + const userProfile = useRequiredUserProfile(); + const { username, slug, version } = useRouteParams(tableVersionDetailParams); + const tableVersion = trpc.tableVersion.get.useQuery({ + tableIdentifier: buildTableIdentifier(username, slug), + version, + }); + + if (tableVersion.isLoading) { + // @TODO: better loading state + return ; + } + + if (tableVersion.isError) { + // @TODO: better error state + return
Error: {tableVersion.error.message}
; + } + + const direction = + location.state?.previousVersion !== undefined + ? location.state.previousVersion > version + ? "up" + : "down" + : location.state?.fromTable + ? "left" + : undefined; + + const variants = { + exit: (direction: "left" | "up" | "down") => { + return { + left: { x: -12, opacity: 0 }, + up: { y: "-100%", opacity: 0 }, + down: { y: "100%", opacity: 0 }, + }[direction]; + }, + enter: (direction: "left" | "up" | "down") => { + return { + left: { x: -12 }, + up: { y: "100%" }, + down: { y: "-100%" }, + }[direction]; + }, + }; + + return ( + +
+
+ + + +

+ {tableVersion.data.table.title}{" "} + + + v{version} + + + + + +

+ +
+ +
+ + {tableVersion.data.table.description ? ( +

+ {tableVersion.data.table.description} +

+ ) : null} +
+
+ +
+ + + + + + + + + + Copy table + + + + + {userProfile.userId === tableVersion.data.table.ownerId ? ( + + ) : null} +
+
+ + +
+ ); +} + +function VersionNavigation({ + version, +}: { + version: RouterOutput["tableVersion"]["get"]; +}) { + const currentIndex = version.versions.findIndex( + (v) => v.version === version.version, + ); + const totalVersions = version.versions.length; + + // versions are in descending order by `version`, so these seem backwards + const hasNextVersion = currentIndex > 0; + const hasPreviousVersion = currentIndex < totalVersions - 1; + + const PrevLinkComponent = hasPreviousVersion ? PrefetchableLink : "span"; + const NextLinkComponent = hasNextVersion ? PrefetchableLink : "span"; + + return ( + <> + + + + + + Previous version + + + + + + + + + + Next version + + + + + ); +} diff --git a/apps/web/src/features/table-version/pages/detail/lazy.ts b/apps/web/src/features/table-version/pages/detail/lazy.ts index 194bc47..9ef7883 100644 --- a/apps/web/src/features/table-version/pages/detail/lazy.ts +++ b/apps/web/src/features/table-version/pages/detail/lazy.ts @@ -4,13 +4,13 @@ import type { TrpcUtils } from "~utils/trpc"; export function loadTableVersionDetailRoute(trpcUtils: TrpcUtils): LazyRoute { return async () => { - const { TableVersionDetail, loaderBuilder } = await import( + const { TableVersionLayout, loaderBuilder } = await import( "~features/table-version/pages/detail" ); return { loader: loaderBuilder(trpcUtils), - Component: TableVersionDetail, + Component: TableVersionLayout, handle: { title: ({ data }) => `Manifold | ${data.title} v${data.version}`, description: ({ data }) => diff --git a/apps/web/src/features/table-version/pages/detail/page.tsx b/apps/web/src/features/table-version/pages/detail/page.tsx index 2584da4..5b4bcec 100644 --- a/apps/web/src/features/table-version/pages/detail/page.tsx +++ b/apps/web/src/features/table-version/pages/detail/page.tsx @@ -1,24 +1,12 @@ import { buildTableIdentifier } from "@manifold/lib"; -import type { RouterOutput } from "@manifold/router"; import { FullScreenLoader } from "@manifold/ui/components/full-screen-loader"; -import { TableIdentifier } from "@manifold/ui/components/table-identifier"; import { Badge } from "@manifold/ui/components/ui/badge"; import { Button } from "@manifold/ui/components/ui/button"; -import { FlexCol } from "@manifold/ui/components/ui/flex"; import { transitionAlpha } from "@manifold/ui/lib/animation"; import { cn } from "@manifold/ui/lib/utils"; import { motion } from "framer-motion"; -import { - GoArrowLeft, - GoArrowRight, - GoChevronLeft, - GoChevronRight, - GoCopy, - GoPackage, - GoPencil, -} from "react-icons/go"; +import { GoArrowRight } from "react-icons/go"; -import { useRequiredUserProfile } from "~features/onboarding/hooks/use-required-user-profile"; import { PrefetchableLink } from "~features/routing/components/prefetchable-link"; import { useRouteParams } from "~features/routing/hooks/use-route-params"; import { tableVersionDetailParams } from "~features/table-version/pages/detail/params"; @@ -27,7 +15,6 @@ import { trpc } from "~utils/trpc"; const COLLAPSED_AVAILABLE_TABLES_COUNT = 3; export function TableVersionDetail() { - const userProfile = useRequiredUserProfile(); const { username, slug, version } = useRouteParams(tableVersionDetailParams); const tableVersion = trpc.tableVersion.get.useQuery({ tableIdentifier: buildTableIdentifier(username, slug), @@ -45,149 +32,87 @@ export function TableVersionDetail() { } return ( - -
-
- - - -

- {tableVersion.data.table.title}{" "} - v{version} - - - -

- -
- -
- - {tableVersion.data.table.description ? ( -

- {tableVersion.data.table.description} -

- ) : null} -
-
- -
- - - - - {userProfile.userId === tableVersion.data.table.ownerId ? ( - +
+
+

Table details

+ +
+
+ Published +
+
+ {tableVersion.data.createdAt.toLocaleDateString()} +
+ + {tableVersion.data.versions.length > 0 ? ( + <> +
+ Release notes +
+
+ {tableVersion.data.releaseNotes || ( + No release notes + )} +
+
+ Available Tables +
+
+ {tableVersion.data.versions[0].availableTables.map( + (tableId) => ( + + {tableId} + + ), + )} +
+ ) : null} -
-
- -
-
-

Table details

- -
-
- Last updated -
-
- {tableVersion.data.table.updatedAt.toLocaleDateString()} -
- -
- Total versions -
-
- {tableVersion.data.versions.length} -
- - {tableVersion.data.versions.length > 0 ? ( - <> -
- Latest release notes -
-
- {tableVersion.data.versions[0].releaseNotes || ( - No release notes - )} -
-
- Available Tables -
-
- {tableVersion.data.versions[0].availableTables.map( - (tableId) => ( - - {tableId} - - ), - )} -
- - ) : null} -
+ -

Definition

+

Definition

-
-
-              {tableVersion.data.definition}
-            
-
-
- -
-

Other Versions

- -
    - {tableVersion.data.versions.map((version) => { - const isCurrentVersion = - version.version === tableVersion.data.version; - const LinkComponent = isCurrentVersion - ? "span" - : PrefetchableLink; +
    +
    +            {tableVersion.data.definition}
    +          
    +
    +
- return ( -
  • - +
    +

    Versions

    + +
      + {tableVersion.data.versions.map((version) => { + const isCurrentVersion = + version.version === tableVersion.data.version; + const LinkComponent = isCurrentVersion ? "span" : PrefetchableLink; + + return ( +
    • + + {isCurrentVersion ? ( + + ) : null} + +
      @@ -198,7 +123,7 @@ export function TableVersionDetail() { {version.createdAt.toLocaleDateString()} {isCurrentVersion ? ( - Current + Current ) : null}
      @@ -217,7 +142,7 @@ export function TableVersionDetail() { .map((tableId) => ( {tableId} @@ -242,71 +167,13 @@ export function TableVersionDetail() { ) : null} - -
    • - ); - })} -
    -
    +
  • + + + ); + })} + - - ); -} - -function VersionNavigation({ - version, -}: { - version: RouterOutput["tableVersion"]["get"]; -}) { - const currentIndex = version.versions.findIndex( - (v) => v.version === version.version, - ); - const totalVersions = version.versions.length; - - // versions are in descending order by `version`, so these seem backwards - const hasNextVersion = currentIndex > 0; - const hasPreviousVersion = currentIndex < totalVersions - 1; - - const PrevLinkComponent = hasPreviousVersion ? PrefetchableLink : "span"; - const NextLinkComponent = hasNextVersion ? PrefetchableLink : "span"; - - return ( - <> - - - - + ); } diff --git a/apps/web/src/features/table/components/table-update-form/publish-button.tsx b/apps/web/src/features/table/components/table-update-form/publish-button.tsx index 56de4d7..2a24fed 100644 --- a/apps/web/src/features/table/components/table-update-form/publish-button.tsx +++ b/apps/web/src/features/table/components/table-update-form/publish-button.tsx @@ -9,7 +9,7 @@ import { } from "@manifold/ui/components/ui/tooltip"; import { useStateGuard } from "@manifold/ui/hooks/use-state-guard"; import { useAtomValue } from "jotai"; -import { GoPackageDependents } from "react-icons/go"; +import { GoGitBranch } from "react-icons/go"; import { DialogManager, DIALOGS } from "~features/dialog-manager"; import { currentAllResolvedDependenciesAtom } from "~features/editor/components/editor/state"; @@ -76,11 +76,7 @@ export function PublishButton({ }) } > - {isPending ? ( - - ) : ( - - )} + {isPending ? : } Publish{recentVersions.length > 0 ? " Version" : null} diff --git a/apps/web/src/features/table/pages/detail/page.tsx b/apps/web/src/features/table/pages/detail/page.tsx index 68b0bfb..55c5eb3 100644 --- a/apps/web/src/features/table/pages/detail/page.tsx +++ b/apps/web/src/features/table/pages/detail/page.tsx @@ -13,6 +13,8 @@ import { useRouteParams } from "~features/routing/hooks/use-route-params"; import { tableDetailParams } from "~features/table/pages/detail/params"; import { trpc } from "~utils/trpc"; +const COLLAPSED_AVAILABLE_TABLES_COUNT = 3; + export function TableDetail() { const userProfile = useRequiredUserProfile(); const { username, slug } = useRouteParams(tableDetailParams); @@ -70,7 +72,10 @@ export function TableDetail() { {userProfile.userId === table.data.ownerId ? (