Resource Manager

A customizable frontend for any data source

An enhanced Shadcn/UI data table with TypeScript support, resizable columns, and a dynamic form generator powered by Zod validation and integrated file uploads.

Installation

npx shadcn@latest add https://alex-amayo.github.io/resource-manager/ui/r/resource-manager.json

Demo

Live Code Editor


function Demo() {
  const [data, setData] = React.useState(initialData);
  const fields = [
    {
      key: 'title',
      label: 'Title',
      inputType: 'text',
      fieldType: 'string',
      zodSchema: z.string().min(1, 'Title is required')
    },
    {
      key: 'author',
      label: 'Author',
      inputType: 'text',
      fieldType: 'string',
      zodSchema: z.string().min(1, 'Author is required')
    },
    {
      key: 'year',
      label: 'Year Released',
      inputType: 'number',
      fieldType: 'number',
      zodSchema: z.number()
    },
    {
      key: 'description',
      label: 'Description',
      inputType: 'textarea',
      fieldType: 'string',
      zodSchema: z.string().optional()
    },
    {
      key: 'genre',
      label: 'Genre',
      inputType: 'select',
      fieldType: 'string',
      options: [
        { value: 'fiction', label: 'Fiction' },
        { value: 'nonfiction', label: 'Nonfiction' },
        { value: 'other', label: 'Other' }
      ],
      zodSchema: z.string().min(1, 'Genre is required')
    },
    {
      key: 'cover',
      label: 'Book Cover',
      inputType: 'file',
      fieldType: 'file',
      renderCell: (value: any) => {
        if (typeof value === 'string' && value.startsWith('http')) {
          return value;
        }
        return '-';
      },
      onFileUpload: async (file) => {
        await new Promise(res => setTimeout(res, 500));
        return `https://dummyimage.com/100x150/cccccc/000000&text=${encodeURIComponent(file.name)}`;
      },
      zodSchema: z.string().optional()
    }
  ];
  function handleAdd(resource: any) {
    setData(prev => [
      ...prev,
      {
        ...resource,
        id: String(Date.now()),
        cover: resource.cover instanceof File ? (resource.cover.url || resource.cover.name) : resource.cover || ''
      }
    ]);
  }
  function handleDelete(id: string) {
    setData(prev => prev.filter(item => item.id !== id));
  }
  function handleUpdate(id: string, values: any) {
    setData(prev => prev.map(item =>
      item.id === id
        ? {
          ...item,
          ...values,
          cover: values.cover instanceof File ? (values.cover.url || values.cover.name) : values.cover || item.cover
        }
        : item
    ));
  }
  return (
    <ResourceManager
      data={data}
      fields={fields}
      handleCreate={handleAdd}
      handleUpdate={handleUpdate}
      handleDelete={handleDelete}
    />
  );
}