fix: move Dashboard to frontend, pin dependencies in bun.lock to specific versions for stability
This commit is contained in:
parent
dd6bcd30dc
commit
def59ee1fb
16 changed files with 2211 additions and 2427 deletions
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
import { AppSidebar } from "@/components/blocks/AppSidebar"
|
||||
import { AppSidebar } from "@/components/frontend/blocks/AppSidebar"
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
113
src/app/(frontend)/logistics/storage/page.tsx
Normal file
113
src/app/(frontend)/logistics/storage/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
113
src/collections/game/GameStructures.ts
Normal file
113
src/collections/game/GameStructures.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -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
193
src/components/ui/item.tsx
Normal 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,
|
||||
}
|
||||
|
|
@ -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".
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
Loading…
Reference in a new issue