diff --git a/.gitignore b/.gitignore
index bc5557fd..613a4cc7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
node_modules
dist
.DS_Store
-bun.lockb
+tester/
+bun.lockb
\ No newline at end of file
diff --git a/README.md b/README.md
index e4225e84..4e6ae72a 100644
--- a/README.md
+++ b/README.md
@@ -182,7 +182,7 @@ To run locally:
```sh
pnpm i
-pnpm run dev
+pnpm run build
npm install -g . (in a second terminal - this will then make kirimase available across your machine using "kirimase *command*")
```
diff --git a/package.json b/package.json
index a894b525..0c09aade 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,9 @@
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
- "build": "npx tsc",
+ "build": "npx tsc && rm -rf dist/templates && cp -r src/templates dist/templates",
+ "reinstall": "bun run build && npm i -g .",
+ "createTesterApp": "rm -rf tester/ && bunx create-next-app tester && cd tester/ && kirimase init",
"dev2": "node dist/index.js",
"dev": "tsc -w"
},
@@ -29,6 +31,7 @@
"chalk": "^5.3.0",
"commander": "^11.0.0",
"consola": "^3.2.3",
+ "eta": "^3.2.0",
"execa": "^8.0.1",
"figlet": "^1.7.0",
"ora": "^8.0.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 66c23900..5c35e179 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -17,6 +17,9 @@ dependencies:
consola:
specifier: ^3.2.3
version: 3.2.3
+ eta:
+ specifier: ^3.2.0
+ version: 3.2.0
execa:
specifier: ^8.0.1
version: 8.0.1
@@ -691,6 +694,11 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /eta@3.2.0:
+ resolution: {integrity: sha512-Qzc3it7nLn49dbOb9+oHV9rwtt9qN8oShRztqkZ3gXPqQflF0VLin5qhWk0g/2ioibBwT4DU6OIMVft7tg/rVg==}
+ engines: {node: '>=6.0.0'}
+ dev: false
+
/execa@8.0.1:
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
engines: {node: '>=16.17'}
diff --git a/src/commands/add/componentLib/shadcn-ui/index.ts b/src/commands/add/componentLib/shadcn-ui/index.ts
index 4d705f5c..686bf753 100644
--- a/src/commands/add/componentLib/shadcn-ui/index.ts
+++ b/src/commands/add/componentLib/shadcn-ui/index.ts
@@ -1,6 +1,7 @@
import { consola } from "consola";
// import { execa } from "execa";
import { existsSync } from "fs";
+import { eta } from "../../../../eta.js";
import {
addPackageToConfig,
createFile,
@@ -47,6 +48,7 @@ const manualInstallShadCn = async (
addToInstallList({
regular: [
+ "@tanstack/react-table",
"tailwindcss-animate",
"class-variance-authority",
"clsx",
@@ -85,6 +87,16 @@ const manualInstallShadCn = async (
rootPath.concat("components/ui/ThemeToggle.tsx"),
generateThemeToggler()
);
+
+ // generate base Data Table
+ createFile(
+ rootPath.concat("components/ui/DataTable/index.tsx"),
+ eta.render("DataTable/index.eta", {})
+ );
+ createFile(
+ rootPath.concat("components/ui/DataTable/pagination.tsx"),
+ eta.render("DataTable/pagination.eta", {})
+ );
// add context provider to layout
addContextProviderToRootLayout("ThemeProvider");
};
@@ -131,6 +143,8 @@ export const installShadcnUI = async (
// "label",
// ]);
addToShadcnComponentList([
+ "table",
+ "pagination",
"button",
"sonner",
"avatar",
diff --git a/src/commands/add/orm/drizzle/generators.ts b/src/commands/add/orm/drizzle/generators.ts
index cabf2014..02737727 100644
--- a/src/commands/add/orm/drizzle/generators.ts
+++ b/src/commands/add/orm/drizzle/generators.ts
@@ -540,7 +540,7 @@ export const addScriptsToPackageJson = (
"db:migrate": `tsx ${libPath}/db/migrate.ts`,
"db:drop": "drizzle-kit drop",
"db:pull": `drizzle-kit introspect:${driver}`,
- ...(driver !== "pg" ? { "db:push": `drizzle-kit push:${driver}` } : {}),
+ "db:push": `drizzle-kit push:${driver}`,
"db:studio": "drizzle-kit studio",
"db:check": `drizzle-kit check:${driver}`,
};
diff --git a/src/commands/add/orm/drizzle/utils.ts b/src/commands/add/orm/drizzle/utils.ts
index ed9e3611..0209cf80 100644
--- a/src/commands/add/orm/drizzle/utils.ts
+++ b/src/commands/add/orm/drizzle/utils.ts
@@ -69,7 +69,6 @@ ${nanoidContent.split("\n")[1].trim()}
replaceFile(utilsPath, newContent);
}
};
-
export const checkTimestampsInUtils = () => {
const timestampsContent = `export const timestamps: { createdAt: true; updatedAt: true } = {
createdAt: true,
diff --git a/src/commands/generate/generators/views-with-server-actions.ts b/src/commands/generate/generators/views-with-server-actions.ts
index bacf0c15..e5e56c46 100644
--- a/src/commands/generate/generators/views-with-server-actions.ts
+++ b/src/commands/generate/generators/views-with-server-actions.ts
@@ -36,6 +36,7 @@ import { formatTableName, toCamelCase } from "../utils.js";
import { existsSync, readFileSync } from "fs";
import { consola } from "consola";
import { addToShadcnComponentList } from "../../add/utils.js";
+import eta from "../../../eta.js";
export const scaffoldViewsAndComponentsWithServerActions = async (
schema: ExtendedSchema
@@ -80,6 +81,15 @@ export const scaffoldViewsAndComponentsWithServerActions = async (
createListComponent(schema)
);
+ // create columns
+ createFile(
+ formatFilePath(
+ `app/(app)/${tableNameKebabCase}/columns.tsx`,
+ { prefix: "rootPath", removeExtension: false }
+ ),
+ eta.render('DataTable/columns.eta', { fields: schema.fields, tableNameKebabCase })
+ );
+
// create components/tableName/TableNameForm.tsx
createFile(
formatFilePath(
@@ -251,7 +261,8 @@ const generateView = (schema: Schema) => {
const relationsFormatted = formatRelations(relations);
return `import { Suspense } from "react";
-
+import { DataTable } from "${formatFilePath(`components/ui/DataTable`, {prefix: "alias", removeExtension: false})}";
+import { columns } from "./columns";
import Loading from "${formatFilePath("app/loading", {
prefix: "alias",
removeExtension: false,
@@ -313,6 +324,7 @@ const ${tableNameCapitalised} = async () => {
.join(" ")
: ""
} />
+
);
};
@@ -469,19 +481,6 @@ export default function ${tableNameSingularCapitalised}List({
+
- {optimistic${tableNamePluralCapitalised}.length === 0 ? (
-
- ) : (
-
- {optimistic${tableNamePluralCapitalised}.map((${tableNameSingular}) => (
- <${tableNameSingularCapitalised}
- ${tableNameSingular}={${tableNameSingular}}
- key={${entityName}.id}
- openModal={openModal}
- />
- ))}
-
- )}
);
}
diff --git a/src/commands/generate/generators/views.ts b/src/commands/generate/generators/views.ts
index 926cbd51..83007f62 100644
--- a/src/commands/generate/generators/views.ts
+++ b/src/commands/generate/generators/views.ts
@@ -15,13 +15,16 @@ import {
toCamelCase,
toNormalEnglish,
} from "../utils.js";
+import fs from 'fs';
import { addToShadcnComponentList } from "../../add/utils.js";
+import eta from "../../../eta.js";
export const scaffoldViewsAndComponents = async (schema: Schema) => {
const { hasSrc, packages } = readConfigFile();
const {
tableNameCamelCase,
tableNameSingularCapitalised,
+ tableNameSingular,
tableNameKebabCase,
} = formatTableName(schema.tableName);
// require trpc for these views
@@ -32,13 +35,22 @@ export const scaffoldViewsAndComponents = async (schema: Schema) => {
rootPath.concat(`app/(app)/${tableNameKebabCase}/page.tsx`),
generateView(schema)
);
+
+ // create tableName/[id]/page.tsx
+ createFile(
+ formatFilePath(
+ `app/(app)/${tableNameKebabCase}/[${tableNameSingular}Id]/page.tsx`,
+ { removeExtension: false, prefix: "rootPath" }
+ ),
+ createShowPage(schema)
+ );
// create components/tableName/TableNameList.tsx
createFile(
rootPath.concat(
`components/${tableNameCamelCase}/${tableNameSingularCapitalised}List.tsx`
- ),
+ ),
createListComponent(schema)
- );
+ );
// create components/tableName/TableNameForm.tsx
createFile(
rootPath.concat(
@@ -46,6 +58,11 @@ export const scaffoldViewsAndComponents = async (schema: Schema) => {
),
createFormComponent(schema)
);
+ // create components/tableName/columns.tsx
+ createFile(
+ rootPath.concat(`components/${tableNameCamelCase}/columns.tsx`),
+ eta.render('DataTable/columns.eta', { fields: schema.fields, tableNameKebabCase })
+ );
// create components/tableName/TableNameModal.tsx
createFile(
rootPath.concat(
@@ -101,6 +118,8 @@ import { api } from "${formatFilePath(trpc.trpcApiTs, {
})}";`
: ""
}
+import { DataTable } from "${alias}/components/ui/DataTable";
+import { columns } from "${alias}/components/${tableNameCamelCase}/columns";
export default async function ${tableNameCapitalised}() {
${
@@ -113,13 +132,49 @@ export default async function ${tableNameCapitalised}() {
${tableNameNormalEnglishCapitalised}
- <${tableNameSingularCapitalised}List ${tableNameCamelCase}={${tableNameCamelCase}} />
+
);
}
`;
};
+const createShowPage = (schema: Schema) => {
+ const {
+ tableNameCamelCase,
+ tableNameSingularCapitalised,
+ tableNameKebabCase,
+ tableNameSingular,
+ tableNameNormalEnglishCapitalised,
+ } = formatTableName(schema.tableName);
+ const { trpc } = getFilePaths();
+ const { alias } = readConfigFile();
+ const trpcRoute = formatFilePath(trpc.trpcApiTs, {
+ prefix: "alias",
+ removeExtension: true,
+ })
+ const { fields } = schema;
+ if (!fs.existsSync(`src/compononents/ui/breadcrumbs.tsx`)) {
+ createFile(
+ `src/components/ui/breadcrumbs.tsx`,
+ eta.render('components/ui/breadcrumbs.eta', {})
+ )
+ }
+
+ return eta.render(
+ 'resources/show.eta', {
+ fields,
+ tableNameSingular,
+ tableNameSingularCapitalised,
+ tableNameCamelCase,
+ tableNameKebabCase,
+ tableNameNormalEnglishCapitalised,
+ alias,
+ trpcRoute
+ }
+ )
+}
+
const queryHasJoins = (tableName: string) => {
// const { hasSrc } = readConfigFile();
const { shared } = getFilePaths();
diff --git a/src/commands/init/utils.ts b/src/commands/init/utils.ts
index 14706e4b..24f14734 100644
--- a/src/commands/init/utils.ts
+++ b/src/commands/init/utils.ts
@@ -268,4 +268,4 @@ export const toggleAnalytics = (input: { toggle?: boolean }) => {
`Anonymous analytics are currently ${analytics ? "on" : "off"}`
);
}
-};
+};
\ No newline at end of file
diff --git a/src/eta.ts b/src/eta.ts
new file mode 100644
index 00000000..7addb4a4
--- /dev/null
+++ b/src/eta.ts
@@ -0,0 +1,9 @@
+import { Eta } from "eta"
+import path from 'path';
+import { fileURLToPath } from 'url';
+import fs from 'fs';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+export const eta = new Eta({ views: path.join(__dirname, "templates") });
+export default eta;
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
index 65535cbd..aa085989 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -20,6 +20,7 @@ addCommonOptions(program.command("init"))
program
.command("generate")
+ .alias("g")
.description("Generate a new resource")
.action(buildSchema);
diff --git a/src/templates/DataTable/columns.eta b/src/templates/DataTable/columns.eta
new file mode 100644
index 00000000..d16df207
--- /dev/null
+++ b/src/templates/DataTable/columns.eta
@@ -0,0 +1,54 @@
+"use client"
+import { MoreHorizontal } from "lucide-react"
+import {
+ ColumnDef,
+} from "@tanstack/react-table"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import { Button } from "@/components/ui/button"
+import Link from 'next/link'
+
+export const columns: ColumnDef[] = [
+ <% it.fields.map(function (field) { %>
+ {
+ accessorKey: "<%_ = field.name _%>",
+ header: "<%_ = field.name _%>",
+ enableHiding: false,
+ cell: ({ row }) => {
+ const resource = row.original
+ return (
+ /${resource.id}`}>
+ {resource.<%_ = field.name _%>}
+
+ )
+ },
+ },
+ <% }) %>
+ {
+ id: "actions",
+ enableHiding: false,
+ cell: ({ row }) => {
+ const resource = row.original
+ return (
+
+
+
+
+
+ Actions
+ Edit
+ Delete
+
+
+ )
+ },
+ },
+]
\ No newline at end of file
diff --git a/src/templates/DataTable/index.eta b/src/templates/DataTable/index.eta
new file mode 100644
index 00000000..fcd71213
--- /dev/null
+++ b/src/templates/DataTable/index.eta
@@ -0,0 +1,133 @@
+"use client"
+import * as React from "react"
+import {
+ VisibilityState,
+ flexRender,
+ getCoreRowModel,
+ getPaginationRowModel,
+ useReactTable,
+} from "@tanstack/react-table"
+import { ArrowUpDown, ChevronDown, MoreHorizontal } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import {
+ DropdownMenu,
+ DropdownMenuCheckboxItem,
+ DropdownMenuContent,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import { DataTablePagination } from './pagination'
+
+export interface DataTableProps {
+ columns: ColumnDef[];
+ data: TData[];
+}
+
+export function DataTable({
+ columns,
+ data,
+}: DataTableProps) {
+ const [columnVisibility, setColumnVisibility] =
+ React.useState({})
+
+ const table = useReactTable({
+ data,
+ columns,
+ getCoreRowModel: getCoreRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ onColumnVisibilityChange: setColumnVisibility,
+ state: {
+ columnVisibility,
+ },
+ })
+
+ const hideableColumns = table.getAllColumns().filter((column) => column.getCanHide())
+
+ return (
+
+
+ {hideableColumns.length > 0 &&
+
+
+
+
+ {hideableColumns
+ .map((column) => {
+ return (
+
+ column.toggleVisibility(!!value)
+ }
+ >
+ {column.id}
+
+ )
+ })}
+
+ }
+
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => {
+ return (
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+
+ )
+ })}
+
+ ))}
+
+
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext()
+ )}
+
+ ))}
+
+ ))
+ ) : (
+
+
+ No results.
+
+
+ )}
+
+
+
+
+
+ )
+}
diff --git a/src/templates/DataTable/pagination.eta b/src/templates/DataTable/pagination.eta
new file mode 100644
index 00000000..0c4a1939
--- /dev/null
+++ b/src/templates/DataTable/pagination.eta
@@ -0,0 +1,110 @@
+import { Table } from "@tanstack/react-table";
+import {
+ Pagination,
+ PaginationContent,
+ PaginationItem,
+ PaginationLink,
+ PaginationNext,
+ PaginationPrevious,
+} from "@/components/ui/pagination";
+import { ChevronsLeft, ChevronsRight } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+const PaginationFirst = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+
+
+);
+PaginationFirst.displayName = "PaginationFirst";
+
+
+const PaginationLast = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+
+
+);
+PaginationLast.displayName = "PaginationLast";
+
+interface DataTablePaginationProps {
+ table: Table;
+}
+
+export function DataTablePagination({
+ table,
+}: DataTablePaginationProps) {
+ const currentPageIndex = table.getState().pagination.pageIndex;
+ const lastPageIndex = table.getPageCount() - 1;
+ return (
+
+
+
+ table.setPageIndex(0)}
+ isActive={table.getCanPreviousPage()}
+ />
+
+
+ table.previousPage()}
+ isActive={table.getCanPreviousPage()}
+ />
+
+ {table.getCanPreviousPage() && (
+
+ table.previousPage()}
+ isActive={table.getCanPreviousPage()}
+ >
+ {currentPageIndex}
+
+
+ )}
+
+
+ {currentPageIndex + 1}
+
+
+ {table.getCanNextPage() && (
+
+ table.nextPage()}
+ isActive={table.getCanNextPage()}
+ >
+ {currentPageIndex + 2}
+
+
+ )}
+
+ {
+ if (table.getCanNextPage()) table.nextPage();
+ }}
+ isActive={table.getCanNextPage()}
+ />
+
+
+ table.setPageIndex(lastPageIndex)}
+ isActive={table.getCanNextPage()}
+ />
+
+
+
+ );
+}
diff --git a/src/templates/components/ui/breadcrumbs.eta b/src/templates/components/ui/breadcrumbs.eta
new file mode 100644
index 00000000..15283481
--- /dev/null
+++ b/src/templates/components/ui/breadcrumbs.eta
@@ -0,0 +1,153 @@
+import * as React from "react"
+import { ChevronRight } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function getValidChildren(children: React.ReactNode) {
+ return React.Children.toArray(children).filter((child) =>
+ React.isValidElement(child)
+ ) as React.ReactElement[]
+}
+
+export interface BreadcrumbProps extends React.ComponentPropsWithoutRef<"nav"> {
+ /* The visual separator between each breadcrumb item */
+ separator?: React.ReactNode
+ /**
+ * If `true`, adds a separator between each breadcrumb item.
+ * @default true
+ */
+ addSeparator?: boolean
+}
+
+export const Breadcrumb = React.forwardRef(
+ (
+ {
+ children,
+ className,
+ separator = ,
+ addSeparator = true,
+ ...props
+ },
+ forwardedRef
+ ) => {
+ const validChildren = getValidChildren(children)
+ const clones = validChildren.map((child, index) => {
+ return React.cloneElement(child, {
+ addSeparator,
+ separator,
+ isLastChild: validChildren.length === index + 1,
+ })
+ })
+
+ return (
+
+ )
+ }
+)
+Breadcrumb.displayName = "Breadcrumb"
+
+export interface BreadcrumbItemProps extends BreadcrumbProps {
+ /**
+ * If `true`, indicates that the breadcrumb item is active, adds
+ * `aria-current=page` and renders a `span`
+ */
+ isCurrentPage?: boolean
+ isLastChild?: boolean
+}
+
+export const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ BreadcrumbItemProps
+>(
+ (
+ {
+ children,
+ className,
+ isCurrentPage,
+ isLastChild,
+ separator,
+ addSeparator,
+ ...props
+ },
+ forwardedRef
+ ) => {
+ const validChildren = getValidChildren(children)
+ const clones = validChildren.map((child) => {
+ if (child.type === BreadcrumbLink) {
+ return React.cloneElement(child, { isCurrentPage })
+ }
+
+ if (child.type === BreadcrumbSeparator) {
+ return React.cloneElement(child, {
+ children: separator || child.props.children,
+ })
+ }
+
+ return child
+ })
+
+ return (
+
+ {clones}
+ {!isLastChild && addSeparator && (
+ {separator}
+ )}
+
+ )
+ }
+)
+BreadcrumbItem.displayName = "BreadcrumbItem"
+
+export interface BreadcrumbLinkProps
+ extends React.ComponentPropsWithoutRef<"a">,
+ Pick {
+ as?: React.ElementType
+}
+
+export const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ BreadcrumbLinkProps
+>(({ className, as: asComp, isCurrentPage, ...props }, forwardedRef) => {
+ const Comp = (isCurrentPage ? "span" : asComp || "a") as "a"
+
+ return (
+
+ )
+})
+BreadcrumbLink.displayName = "BreadcrumbLink"
+
+export type BreadcrumbSeparatorProps = React.ComponentPropsWithoutRef<"span">
+
+export const BreadcrumbSeparator = React.forwardRef<
+ HTMLSpanElement,
+ BreadcrumbSeparatorProps
+>(({ className, ...props }, forwardedRef) => {
+ return (
+
+ )
+})
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
\ No newline at end of file
diff --git a/src/templates/resources/show.eta b/src/templates/resources/show.eta
new file mode 100644
index 00000000..9811624c
--- /dev/null
+++ b/src/templates/resources/show.eta
@@ -0,0 +1,39 @@
+import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from "<%_ = it.alias _%>/components/ui/breadcrumbs"
+import { api } from "<%_ = it.trpcRoute _%>"
+import <%= it.tableNameSingularCapitalised _%>Modal from "<%_ = it.alias _%>/components/<%_ = it.tableNameCamelCase _%>/<%_ = it.tableNameSingularCapitalised _%>Modal"
+
+export default async function Page({params} : {params: { <% = it.tableNameSingular _%>Id: string}}) {
+ const { <% = it.tableNameSingular %> } = await api.<%_ = it.tableNameCamelCase _%>.get<%_ = it.tableNameSingularCapitalised _%>ById.query({ id: params.<%_ = it.tableNameSingular _%>Id })
+ return (
+
+
+
+
+ Home
+
+
+ <%_ = it.tableNameNormalEnglishCapitalised _%>
+
+
+
+ {params.<% = it.tableNameSingular _%>Id}
+
+
+
+
+ <%_ = it.tableNameSingularCapitalised _%>: {params.<% = it.tableNameSingular _%>Id}
+ <<%_ = it.tableNameSingularCapitalised _%>Modal <% = it.tableNameSingular _%>={<%_ = it.tableNameSingular _%>} />
+
+
+ <% it.fields.map(function (field) { %>
+
+
<%_ = field.name _%>
+
{<%_ = it.tableNameSingular _%>.<%_ = field.name _%>}
+
+ <% }) %>
+
+
+
+ )
+}
+
diff --git a/src/utils.ts b/src/utils.ts
index 69acef04..3df5cdf3 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -269,3 +269,11 @@ export const sendEvent = async (
return;
}
};
+
+export const humanize = (input: string) => {
+ return input
+ .replace(/([A-Z])/g, (match) => ` ${match}`)
+ .replace(/_|-/g, " ")
+ .toLowerCase()
+ .replace(/(?:^|\s)\S/g, (str) => str.toUpperCase());
+};