init with react+axum+spacetimedb
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-26 18:06:23 +08:00
commit cbc27bad4a
20199 changed files with 883714 additions and 0 deletions

View File

@@ -0,0 +1,258 @@
import { useMemo, useState } from 'react';
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
import type { CustomWorldProfile } from '../../types';
import { CustomWorldCreationStartCard } from './CustomWorldCreationStartCard';
import { CustomWorldWorkCard } from './CustomWorldWorkCard';
import {
type CustomWorldWorkFilter,
CustomWorldWorkTabs,
} from './CustomWorldWorkTabs';
import type { PlatformCreationTypeId } from '../platform-entry/platformEntryCreationTypes';
import {
buildCreationWorkShelfItems,
type CreationWorkShelfItem,
} from './creationWorkShelf';
type CustomWorldCreationHubProps = {
items: CustomWorldWorkSummary[];
loading: boolean;
error: string | null;
onRetry: () => void;
createError?: string | null;
createBusy?: boolean;
onCreateType: (type: PlatformCreationTypeId) => void;
onOpenDraft: (item: CustomWorldWorkSummary) => void;
onEnterPublished: (profileId: string) => void;
onDeletePublished?: ((item: CustomWorldWorkSummary) => void) | null;
deletingWorkId?: string | null;
onExperienceRpg?: ((item: CustomWorldWorkSummary) => void) | null;
rpgLibraryEntries?: CustomWorldLibraryEntry<CustomWorldProfile>[];
bigFishItems?: BigFishWorkSummary[];
onOpenBigFishDetail?: (item: BigFishWorkSummary) => void;
onExperienceBigFish?: ((item: BigFishWorkSummary) => void) | null;
onDeleteBigFish?: ((item: BigFishWorkSummary) => void) | null;
puzzleItems?: PuzzleWorkSummary[];
onOpenPuzzleDetail?: (item: PuzzleWorkSummary) => void;
onExperiencePuzzle?: ((profileId: string) => void) | null;
onDeletePuzzle?: ((item: PuzzleWorkSummary) => void) | null;
};
function EmptyState({ title }: { title: string }) {
return (
<div className="platform-subpanel flex min-h-[14rem] flex-col items-center justify-center rounded-[1.6rem] px-6 py-8 text-center">
<div className="text-lg font-semibold text-[var(--platform-text-strong)]">
{title}
</div>
</div>
);
}
export function CustomWorldCreationHub({
items,
loading,
error,
onRetry,
createError = null,
createBusy = false,
onCreateType,
onOpenDraft,
onEnterPublished,
onDeletePublished = null,
deletingWorkId = null,
onExperienceRpg = null,
rpgLibraryEntries = [],
bigFishItems = [],
onOpenBigFishDetail,
onExperienceBigFish = null,
onDeleteBigFish = null,
puzzleItems = [],
onOpenPuzzleDetail,
onExperiencePuzzle = null,
onDeletePuzzle = null,
}: CustomWorldCreationHubProps) {
const [activeFilter, setActiveFilter] =
useState<CustomWorldWorkFilter>('all');
const shelfItems = useMemo(
() =>
buildCreationWorkShelfItems({
rpgItems: items,
rpgLibraryEntries,
bigFishItems,
puzzleItems,
canDeleteRpg: Boolean(onDeletePublished),
canDeleteBigFish: Boolean(onDeleteBigFish),
canDeletePuzzle: Boolean(onDeletePuzzle),
}),
[
bigFishItems,
items,
onDeleteBigFish,
onDeletePublished,
onDeletePuzzle,
puzzleItems,
rpgLibraryEntries,
],
);
const draftCount = shelfItems.filter(
(entry) => entry.status === 'draft',
).length;
const publishedCount = shelfItems.filter(
(entry) => entry.status === 'published',
).length;
const filteredItems = useMemo(
() =>
shelfItems.filter((entry) =>
activeFilter === 'all' ? true : entry.status === activeFilter,
),
[activeFilter, shelfItems],
);
function handleOpenShelfItem(item: CreationWorkShelfItem) {
switch (item.source.kind) {
case 'puzzle':
onOpenPuzzleDetail?.(item.source.item);
return;
case 'big-fish':
onOpenBigFishDetail?.(item.source.item);
return;
case 'rpg':
if (item.status === 'draft') {
onOpenDraft(item.source.item);
return;
}
if (item.source.item.profileId) {
onEnterPublished(item.source.item.profileId);
}
}
}
function buildExperienceAction(item: CreationWorkShelfItem) {
if (!item.canExperience) {
return null;
}
switch (item.source.kind) {
case 'puzzle': {
const sourceItem = item.source.item;
return () => {
onExperiencePuzzle?.(sourceItem.profileId);
};
}
case 'big-fish': {
const sourceItem = item.source.item;
return () => {
onExperienceBigFish?.(sourceItem);
};
}
case 'rpg': {
const sourceItem = item.source.item;
return () => {
onExperienceRpg?.(sourceItem);
};
}
}
}
function buildDeleteAction(item: CreationWorkShelfItem) {
if (!item.canDelete) {
return null;
}
switch (item.source.kind) {
case 'puzzle': {
const sourceItem = item.source.item;
return () => {
onDeletePuzzle?.(sourceItem);
};
}
case 'big-fish': {
const sourceItem = item.source.item;
return () => {
onDeleteBigFish?.(sourceItem);
};
}
case 'rpg': {
const sourceItem = item.source.item;
return () => {
onDeletePublished?.(sourceItem);
};
}
}
}
return (
<div className="platform-page-stage platform-remap-surface space-y-4 px-3 pb-4 pt-3 sm:px-4 sm:pt-4 xl:px-5 xl:pb-5 xl:pt-5">
<div className="space-y-4 xl:space-y-3">
<CustomWorldCreationStartCard
busy={createBusy}
error={createError}
onCreateType={onCreateType}
/>
<CustomWorldWorkTabs
activeFilter={activeFilter}
draftCount={draftCount}
publishedCount={publishedCount}
onChange={setActiveFilter}
/>
{error ? (
<div className="platform-banner platform-banner--danger rounded-[1.4rem] px-4 py-4 text-sm leading-7">
<div>{error}</div>
<button
type="button"
onClick={onRetry}
className="platform-button platform-button--ghost mt-3 min-h-0 rounded-full px-4 py-2 text-sm"
>
</button>
</div>
) : null}
{loading ? (
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{Array.from({ length: 3 }).map((_, index) => (
<div
key={`skeleton-${index}`}
className="platform-subpanel min-h-[12rem] rounded-[1.6rem] p-5"
>
<div className="h-4 w-20 rounded-full bg-[var(--platform-track-fill)]" />
<div className="mt-6 h-8 w-36 rounded-full bg-[var(--platform-track-fill)]" />
<div className="mt-4 h-4 w-full rounded-full bg-[var(--platform-track-fill)]" />
<div className="mt-2 h-4 w-4/5 rounded-full bg-[var(--platform-track-fill)]" />
<div className="mt-8 flex gap-2">
<div className="h-7 w-20 rounded-full bg-[var(--platform-track-fill)]" />
<div className="h-7 w-20 rounded-full bg-[var(--platform-track-fill)]" />
</div>
</div>
))}
</div>
) : filteredItems.length > 0 ? (
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{filteredItems.map((item) => (
<CustomWorldWorkCard
key={`${item.kind}-${item.id}`}
item={item}
onOpen={() => handleOpenShelfItem(item)}
onExperience={buildExperienceAction(item)}
onDelete={buildDeleteAction(item)}
deleteBusy={deletingWorkId === item.id}
/>
))}
</div>
) : shelfItems.length === 0 ? (
<EmptyState title="还没有作品" />
) : (
<EmptyState title="当前筛选下没有内容" />
)}
</div>
</div>
);
}
export type { CustomWorldWorkFilter };