1
0
Fork 0

fix: move Dashboard to frontend, pin dependencies in bun.lock to specific versions for stability

This commit is contained in:
Jason Fraley 2025-12-17 15:15:07 -05:00
parent dd6bcd30dc
commit def59ee1fb
16 changed files with 2211 additions and 2427 deletions

824
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -12,7 +12,20 @@ const nextConfig = {
return webpackConfig;
},
transpilePackages: [
"@payloadcms/richtext-lexical",
"lexical",
"@lexical/react",
"@lexical/rich-text",
"@lexical/history",
"@lexical/list",
"@lexical/link",
"@lexical/markdown",
"@lexical/selection",
"@lexical/table",
"@lexical/utils",
],
output: "standalone",
};
export default withPayload(nextConfig, { devBundleServerPackages: false });
export default withPayload(nextConfig, { devBundleServerPackages: true });

View file

@ -6,8 +6,9 @@
"type": "module",
"scripts": {
"build": "cross-env NODE_OPTIONS=\"--no-deprecation --max-old-space-size=8000\" next build",
"dev": "cross-env NODE_OPTIONS=--no-deprecation next dev",
"devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev",
"dev": "cross-env NODE_OPTIONS=--no-deprecation next dev --webpack",
"devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev --webpack",
"devturbo": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev --turbo",
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
import { AppSidebar } from "@/components/blocks/AppSidebar"
import { AppSidebar } from "@/components/frontend/blocks/AppSidebar"
import {
Breadcrumb,
BreadcrumbItem,

View file

@ -1,7 +1,7 @@
import React from "react";
import "./styles.css";
import { SidebarProvider } from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/blocks/AppSidebar";
import { AppSidebar } from "@/components/frontend/blocks/AppSidebar";
import { getPayload } from "payload";
import config from "@payload-config";
import { headers as nextHeaders } from "next/headers";

View file

@ -0,0 +1,113 @@
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemGroup,
ItemHeader,
ItemMedia,
ItemSeparator,
ItemTitle,
} from "@/components/ui/item";
import config from "@payload-config";
import { getPayload } from "payload";
import { GameStructure, Resource } from "@/payload-types";
import { convertLexicalToPlaintext } from "@payloadcms/richtext-lexical/plaintext";
import { BoxIcon, PackageOpenIcon } from "lucide-react";
export default async function LogisticsStoragePage() {
// const headers = await getHeaders();
const payloadConfig = await config;
const payload = await getPayload({ config: payloadConfig });
// const { user } = await payload.auth({ headers });
// const fileURL = `webstorm://file/${fileURLToPath(import.meta.url)}`;
const structures = await payload.find({
collection: "game-structures",
});
const ItemResource = ({
resource,
amount,
includeSeparator,
}: {
resource: Resource;
amount: number;
includeSeparator: boolean;
}) => (
<>
<Item>
<ItemHeader>
<ItemTitle>
<span>{resource.name}</span>
<span className="text-xs">
<span className="text-muted-foreground">{amount.toLocaleString()}</span>
{/*<span className="text-muted-foreground font-normal">x</span>*/}
</span>
</ItemTitle>
<ItemDescription>
{resource.description ? convertLexicalToPlaintext({ data: resource.description }) : ""}
</ItemDescription>
</ItemHeader>
{/*<ItemContent>
<Item size="sm" className="w-fit">
<ItemMedia variant="icon">
<PackageOpenIcon />
</ItemMedia>
<ItemContent>
<ItemTitle>In Storage</ItemTitle>
<ItemDescription>{amount}</ItemDescription>
</ItemContent>
</Item>
</ItemContent>*/}
</Item>
{includeSeparator && <ItemSeparator />}
</>
);
const ItemStructure = (structure: GameStructure) => (
<Item variant="outline">
<ItemHeader>
<p>{structure.name}</p>
<ItemDescription>A building.</ItemDescription>
</ItemHeader>
{/*<ItemMedia />*/}
<ItemContent>
<Item variant="muted" size="sm">
<ItemHeader>Stored Resources</ItemHeader>
<ItemMedia variant="icon">
<BoxIcon />
</ItemMedia>
<ItemContent>
<ItemGroup className="flex flex-col">
{structure.storedResources?.map((res, index) => {
const resource = res.resource as Resource;
return (
<ItemResource
key={resource.id}
resource={resource}
amount={res.amount}
includeSeparator={index !== structure.storedResources?.length - 1}
/>
);
})}
</ItemGroup>
</ItemContent>
</Item>
</ItemContent>
<ItemActions />
{/*<ItemFooter>{new Date(structure.updatedAt).toLocaleString()}</ItemFooter>*/}
</Item>
);
return (
<div className="p-5">
<div className="flex flex-col gap-2">
{structures.docs?.map((structure) => (
<ItemStructure key={structure.id} {...structure} />
))}
</div>
</div>
);
}

View file

@ -0,0 +1,113 @@
import { CollectionConfig } from "payload";
import { isDeveloper } from "@/utils/access-control/isRole";
import { GameStructure } from "@/payload-types";
export const GameStructures: CollectionConfig = {
slug: "game-structures",
admin: {
group: "Game",
useAsTitle: "name",
},
access: {
create: isDeveloper,
update: isDeveloper,
delete: isDeveloper,
admin: isDeveloper,
},
hooks: {
beforeValidate: [
({ data }) => {
const rows = data?.storedResources;
if (!Array.isArray(rows) || rows.length === 0) return data;
type NormalizedRow = { id?: string | null; resource: unknown; amount: number };
const byResource = new Map<string, NormalizedRow>();
for (const row of rows) {
if (!row) continue;
// Normalize relationship value to an ID-like scalar when possible
const rawResource = (row as any).resource;
const resourceId =
rawResource && typeof rawResource === "object" && "id" in rawResource
? (rawResource as any).id
: rawResource;
if (resourceId == null) continue;
const resourceKey = String(resourceId);
const amount =
typeof (row as any).amount === "number" && Number.isFinite((row as any).amount)
? (row as any).amount
: 0;
const existing = byResource.get(resourceKey);
if (existing) {
existing.amount += amount;
// Keep the first row's id to preserve admin UI row identity
} else {
byResource.set(resourceKey, {
id: (row as any).id ?? undefined,
resource: resourceId,
amount,
});
}
}
data!.storedResources = Array.from(byResource.values());
return data;
},
],
},
fields: [
{
name: "name",
type: "text",
label: "Display Name",
admin: {
description: "The name of the structure as displayed in-game and on the frontend.",
},
required: true,
},
{
name: "type",
type: "relationship",
relationTo: "structures",
required: true,
admin: {
description: "The type of structure this is.",
},
},
{
label: "Geolocation",
type: "group",
fields: [
{
name: "coordinates",
type: "point",
},
],
},
{
name: "storedResources",
type: "array",
fields: [
{
name: "resource",
type: "relationship",
relationTo: "resources",
required: true,
},
{
name: "amount",
type: "number",
required: true,
},
],
},
],
};

View file

@ -14,9 +14,9 @@ import {
Warehouse,
} from "lucide-react";
import { NavCore } from "@/components/blocks/NavCore";
import { NavSecondary } from "@/components/blocks/NavSecondary";
import { NavUser } from "@/components/blocks/NavUser";
import { NavCore } from "@/components/frontend/blocks/NavCore";
import { NavSecondary } from "@/components/frontend/blocks/NavSecondary";
import { NavUser } from "@/components/frontend/blocks/NavUser";
import {
Sidebar,
SidebarContent,
@ -68,7 +68,7 @@ const data = {
items: [
{
title: "Storage",
url: "#",
url: "/logistics/storage",
// icon: Box,
},
{
@ -130,7 +130,7 @@ export function AppSidebar({
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" asChild>
<Link href="/">
<Link href="/src/components/static">
{/*<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<Command className="size-4" />
</div>

193
src/components/ui/item.tsx Normal file
View file

@ -0,0 +1,193 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
role="list"
data-slot="item-group"
className={cn("group/item-group flex flex-col", className)}
{...props}
/>
)
}
function ItemSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="item-separator"
orientation="horizontal"
className={cn("my-0", className)}
{...props}
/>
)
}
const itemVariants = cva(
"group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border-border",
muted: "bg-muted/50",
},
size: {
default: "p-4 gap-4 ",
sm: "py-3 px-4 gap-2.5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Item({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"div"> &
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div"
return (
<Comp
data-slot="item"
data-variant={variant}
data-size={size}
className={cn(itemVariants({ variant, size, className }))}
{...props}
/>
)
}
const itemMediaVariants = cva(
"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5",
{
variants: {
variant: {
default: "bg-transparent",
icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
image:
"size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover",
},
},
defaultVariants: {
variant: "default",
},
}
)
function ItemMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
return (
<div
data-slot="item-media"
data-variant={variant}
className={cn(itemMediaVariants({ variant, className }))}
{...props}
/>
)
}
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-content"
className={cn(
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
className
)}
{...props}
/>
)
}
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-title"
className={cn(
"flex w-fit items-center gap-2 text-sm leading-snug font-medium",
className
)}
{...props}
/>
)
}
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="item-description"
className={cn(
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
className
)}
{...props}
/>
)
}
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-actions"
className={cn("flex items-center gap-2", className)}
{...props}
/>
)
}
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-header"
className={cn(
"flex basis-full items-center justify-between gap-2",
className
)}
{...props}
/>
)
}
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-footer"
className={cn(
"flex basis-full items-center justify-between gap-2",
className
)}
{...props}
/>
)
}
export {
Item,
ItemMedia,
ItemContent,
ItemActions,
ItemGroup,
ItemSeparator,
ItemTitle,
ItemDescription,
ItemHeader,
ItemFooter,
}

View file

@ -86,6 +86,7 @@ export interface Config {
'narrative-events': NarrativeEvent;
'mission-files': MissionFile;
'mod-lists': ModList;
'game-structures': GameStructure;
'payload-kv': PayloadKv;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
@ -119,6 +120,7 @@ export interface Config {
'narrative-events': NarrativeEventsSelect<false> | NarrativeEventsSelect<true>;
'mission-files': MissionFilesSelect<false> | MissionFilesSelect<true>;
'mod-lists': ModListsSelect<false> | ModListsSelect<true>;
'game-structures': GameStructuresSelect<false> | GameStructuresSelect<true>;
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
@ -1233,6 +1235,35 @@ export interface NarrativeEvent {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "game-structures".
*/
export interface GameStructure {
id: number;
/**
* The name of the structure as displayed in-game and on the frontend.
*/
name: string;
/**
* The type of structure this is.
*/
type: number | Structure;
/**
* @minItems 2
* @maxItems 2
*/
coordinates?: [number, number] | null;
storedResources?:
| {
resource: number | Resource;
amount: number;
id?: string | null;
}[]
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-kv".
@ -1332,6 +1363,10 @@ export interface PayloadLockedDocument {
| ({
relationTo: 'mod-lists';
value: number | ModList;
} | null)
| ({
relationTo: 'game-structures';
value: number | GameStructure;
} | null);
globalSlug?: string | null;
user: {
@ -1993,6 +2028,24 @@ export interface ModListsSelect<T extends boolean = true> {
focalX?: T;
focalY?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "game-structures_select".
*/
export interface GameStructuresSelect<T extends boolean = true> {
name?: T;
type?: T;
coordinates?: T;
storedResources?:
| T
| {
resource?: T;
amount?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-kv_select".

View file

@ -26,6 +26,7 @@ import { Structures } from "@/collections/logistics/Structures";
import { Assignments } from "@/collections/users/Assignments";
import { Technologies } from "@/collections/intelligence/Technologies";
import { GameRules } from "@/collections/game/GameRules";
import { GameStructures } from "@/collections/game/GameStructures";
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
@ -86,6 +87,9 @@ export default buildConfig({
// Server
MissionFiles,
ModLists,
// Game
GameStructures
],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || "",
@ -96,6 +100,7 @@ export default buildConfig({
pool: {
connectionString: process.env.DATABASE_URI || "",
},
push: process.env.NODE_ENV === "development",
}),
sharp,
plugins: [