@liferay-react/create-react-liferay-client-extension 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Command } = require('commander');
4
+ const inquirer = require('inquirer');
5
+ const fs = require('fs-extra');
6
+ const path = require('path');
7
+ const chalk = require('chalk');
8
+
9
+ const program = new Command();
10
+
11
+ program
12
+ .version('1.0.0')
13
+ .description('CLI to create a React Liferay Client Extension template')
14
+ .argument('[name]', 'Project name')
15
+ .action(async (name) => {
16
+ let projectName = name;
17
+
18
+ if (!projectName) {
19
+ const answers = await inquirer.prompt([
20
+ {
21
+ type: 'input',
22
+ name: 'name',
23
+ message: 'What is the name of your project?',
24
+ default: 'React Exemplo',
25
+ validate: (input) => {
26
+ if (input.trim().length > 0) return true;
27
+ return 'Project name cannot be empty.';
28
+ }
29
+ }
30
+ ]);
31
+ projectName = answers.name;
32
+ }
33
+
34
+ // Helper for normalization (remove accents)
35
+ const normalize = (str) => str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
36
+
37
+ // 1. Define names for replacement
38
+ const titleName = projectName; // Keep original for title
39
+
40
+ // Create kebab-case: remove accents, spaces to hyphens, lowercase, remove non-alphanumeric except hyphen
41
+ const kebabName = normalize(projectName)
42
+ .toLowerCase()
43
+ .trim()
44
+ .replace(/\s+/g, '-')
45
+ .replace(/[^a-z0-9\-]/g, '');
46
+
47
+ // Create PascalCase: remove accents, capitalize each word, remove non-alphanumeric
48
+ const pascalName = normalize(projectName)
49
+ .split(/[\s\-]+/)
50
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
51
+ .join('')
52
+ .replace(/[^a-zA-Z0-9]/g, '');
53
+
54
+ const targetDir = path.join(process.cwd(), kebabName);
55
+
56
+ if (fs.existsSync(targetDir)) {
57
+ console.error(chalk.red(`Error: Directory ${kebabName} already exists.`));
58
+ process.exit(1);
59
+ }
60
+
61
+ console.log(chalk.blue(`Creating project in ${targetDir}...`));
62
+
63
+ const templateDir = path.join(__dirname, '..', 'template');
64
+
65
+ try {
66
+ // 1. Copy template
67
+ await fs.copy(templateDir, targetDir);
68
+
69
+ // 2. Walk through files and replace content
70
+ await transformDirectory(targetDir, kebabName, pascalName, titleName);
71
+
72
+ console.log(chalk.green('\nProject created successfully!'));
73
+ console.log(`\nTo get started:\n cd ${kebabName}\n npm install\n npm run dev`);
74
+
75
+ } catch (err) {
76
+ console.error(chalk.red('Error creating project:'), err);
77
+ process.exit(1);
78
+ }
79
+ });
80
+
81
+ async function transformDirectory(dir, kebab, pascal, title) {
82
+ const files = await fs.readdir(dir);
83
+
84
+ for (const file of files) {
85
+ const fullPath = path.join(dir, file);
86
+ const stat = await fs.stat(fullPath);
87
+
88
+ if (stat.isDirectory()) {
89
+ await transformDirectory(fullPath, kebab, pascal, title);
90
+ } else {
91
+ await transformFile(fullPath, kebab, pascal, title);
92
+ }
93
+ }
94
+ }
95
+
96
+ async function transformFile(filePath, kebab, pascal, title) {
97
+ // Read file
98
+ let content = await fs.readFile(filePath, 'utf8');
99
+
100
+ // Replace placeholders
101
+ // react-exemplo -> kebab
102
+ // ReactExemplo -> pascal
103
+ // React Exemplo -> title
104
+
105
+ content = content.replace(/react-exemplo/g, kebab);
106
+ content = content.replace(/ReactExemplo/g, pascal);
107
+ content = content.replace(/React Exemplo/g, title);
108
+
109
+ // Write file back
110
+ await fs.writeFile(filePath, content, 'utf8');
111
+ }
112
+
113
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@liferay-react/create-react-liferay-client-extension",
3
+ "version": "1.0.0",
4
+ "description": "CLI to create a React Liferay Client Extension template",
5
+ "license": "MIT",
6
+ "main": "bin/cli.js",
7
+ "keywords": [
8
+ "liferay",
9
+ "react",
10
+ "boilerplate",
11
+ "generator",
12
+ "cli",
13
+ "dxp",
14
+ "portlet",
15
+ "npm-bundler",
16
+ "frontend"
17
+ ],
18
+ "bin": {
19
+ "create-client-react": "./bin/cli.js"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "scripts": {
25
+ "test": "echo \"Error: no test specified\" && exit 1"
26
+ },
27
+ "dependencies": {
28
+ "chalk": "^4.1.2",
29
+ "commander": "^11.1.0",
30
+ "fs-extra": "^11.1.1",
31
+ "inquirer": "^8.2.5"
32
+ }
33
+ }
@@ -0,0 +1,48 @@
1
+ assemble:
2
+ - from: build/vite
3
+ into: static
4
+
5
+ react-exemplo:
6
+ name: React Exemplo
7
+ type: customElement
8
+ htmlElementName: react-exemplo
9
+ friendlyURLMapping: react-exemplo
10
+ instanceable: true
11
+ portletCategoryName: "Empresas"
12
+ virtualInstanceSupport: true
13
+ dxp.lxc.liferay.com.virtualInstanceId: "empresas-hml.unimedcnu.coop.br"
14
+ urls:
15
+ - "*.js"
16
+ useESM: true
17
+ properties:
18
+ url: ""
19
+
20
+ react-exemplo-dev:
21
+ name: React Exemplo
22
+ type: customElement
23
+ htmlElementName: react-exemplo
24
+ friendlyURLMapping: react-exemplo
25
+ instanceable: true
26
+ portletCategoryName: "Empresas"
27
+ virtualInstanceSupport: true
28
+ dxp.lxc.liferay.com.virtualInstanceId: "empresas-dev.unimedcnu.coop.br"
29
+ urls:
30
+ - "*.js"
31
+ useESM: true
32
+ properties:
33
+ url: ""
34
+
35
+ react-exemplo-prd:
36
+ name: React Exemplo
37
+ type: customElement
38
+ htmlElementName: react-exemplo
39
+ friendlyURLMapping: react-exemplo
40
+ instanceable: true
41
+ portletCategoryName: "Empresas"
42
+ virtualInstanceSupport: true
43
+ dxp.lxc.liferay.com.virtualInstanceId: "default"
44
+ urls:
45
+ - "*.js"
46
+ useESM: true
47
+ properties:
48
+ url: ""
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "react-exemplo",
3
+ "version": "1.0.0",
4
+ "scripts": {
5
+ "build": "vite build",
6
+ "dev": "vite"
7
+ },
8
+ "devDependencies": {
9
+ "@tanstack/react-query": "^5.100.10",
10
+ "@vitejs/plugin-react": "^4.3.0",
11
+ "react": "^19.0.0",
12
+ "react-dom": "^19.0.0",
13
+ "sass": "^1.99.0",
14
+ "vite": "^5.0.0",
15
+ "vite-plugin-css-injected-by-js": "^4.0.1"
16
+ }
17
+ }
@@ -0,0 +1,35 @@
1
+ import { createRoot } from "react-dom/client";
2
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3
+ import { App } from "./page/App";
4
+
5
+ const queryClient = new QueryClient({
6
+ defaultOptions: {
7
+ queries: {
8
+ retry: false,
9
+ refetchOnWindowFocus: false,
10
+ },
11
+ },
12
+ });
13
+
14
+ class ReactExemplo extends HTMLElement {
15
+ #root = null;
16
+
17
+ connectedCallback() {
18
+ const props = {
19
+ url: this.getAttribute("url"),
20
+ };
21
+
22
+ this.#root = createRoot(this);
23
+ this.#root.render(
24
+ <QueryClientProvider client={queryClient}>
25
+ <App {...props} />
26
+ </QueryClientProvider>,
27
+ );
28
+ }
29
+
30
+ disconnectedCallback() {
31
+ this.#root?.unmount();
32
+ }
33
+ }
34
+
35
+ customElements.define("react-exemplo", ReactExemplo);
@@ -0,0 +1,21 @@
1
+ import React, { useState } from "react";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { getExemplo } from "../services/services";
4
+ import "./app.scss";
5
+
6
+ export const App = () => {
7
+ const [search, setSearch] = useState("");
8
+ const [filter, setFilter] = useState({
9
+ page: 1,
10
+ pageSize: 5,
11
+ });
12
+ const debouncedSearch = useDebounce(search, 400);
13
+ const queryFilter = { ...filter, search: debouncedSearch };
14
+
15
+ const { data, isLoading, isError } = useQuery({
16
+ queryKey: ["exemplo-query", queryFilter],
17
+ queryFn: () => getExemplo(queryFilter),
18
+ });
19
+
20
+ return <div></div>;
21
+ };
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,12 @@
1
+ import axios from "axios";
2
+
3
+ const api = axios.create({
4
+ baseURL: "",
5
+ });
6
+
7
+ export async function getExemplo({ page = 1, pageSize = 5, search = "" } = {}) {
8
+ const response = await api.get("/exemplo", {
9
+ params: { page, pageSize, search },
10
+ });
11
+ return response.data;
12
+ }
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ import { Button as ButtonUI } from "@unimed/components";
3
+
4
+ /**
5
+ * Componente de botão customizado da Unimed.
6
+ *
7
+ * @param {Object} props - Propriedades do componente.
8
+ * @param {React.ReactNode} props.children - Conteúdo interno do botão.
9
+ * @param {"small" | "large"} [props.size] - Tamanho do botão. O padrão é médio.
10
+ * @param {"secondary" | "tertiary"} [props.variant] - Variante visual do botão. O padrão é primary.
11
+ * @param {string} [props.className] - Classes CSS adicionais.
12
+ * @param {string} [props.iconLeft] - Nome do ícone do Material Symbols para exibir à esquerda.
13
+ * @param {string} [props.iconRight] - Nome do ícone do Material Symbols para exibir à direita.
14
+ * @param {boolean} [props.loading] - Estado de carregamento que exibe um spinner.
15
+ * @param {React.ButtonHTMLAttributes<HTMLButtonElement>} props - Todas as outras propriedades nativas de um botão HTML.
16
+ */
17
+ const Button = (props) => <ButtonUI {...props} />;
18
+
19
+ export { Button };
@@ -0,0 +1,6 @@
1
+ import React from "react";
2
+ import { Card as CardUI } from "@unimed/components";
3
+
4
+ const Card = (props) => <CardUI {...props} />;
5
+
6
+ export { Card };
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+ import { EmptyState as EmptyStateUI } from "@unimed/components";
3
+
4
+ /**
5
+ * Componente para exibição de estados vazios ou de erro.
6
+ *
7
+ * Oferece variantes visuais predefinidas com ícones e textos adequados para cada situação.
8
+ *
9
+ * @param {Object} props - Propriedades do componente.
10
+ * @param {"empty" | "error"} [props.variant] - Variante do estado (padrão: "empty").
11
+ * @param {string} [props.title] - Título customizado (sobrescreve o padrão da variante).
12
+ * @param {string} [props.description] - Descrição customizada (sobrescreve o padrão da variante).
13
+ */
14
+ const EmptyState = (props) => <EmptyStateUI {...props} />;
15
+
16
+ export { EmptyState };
@@ -0,0 +1,18 @@
1
+ import React from "react";
2
+ import { Pagination as PaginationUI } from "@unimed/components";
3
+
4
+ /**
5
+ * Componente de Paginação para listagem de dados.
6
+ *
7
+ * @param {Object} props - Propriedades do componente.
8
+ * @param {number} props.page - Página atual (1-indexed).
9
+ * @param {number} props.totalPages - Total de páginas disponíveis.
10
+ * @param {number} props.pageSize - Quantidade de itens por página.
11
+ * @param {number} props.totalItems - Total de itens na base de dados.
12
+ * @param {(page: number) => void} props.onPageChange - Callback chamado ao trocar de página.
13
+ * @param {(size: number) => void} props.onPageSizeChange - Callback chamado ao trocar a quantidade de itens por página.
14
+ * @param {Array<{label: string, value: number}>} [props.pageSizeOptions] - Opções para o seletor de quantidade de itens.
15
+ */
16
+ const Pagination = (props) => <PaginationUI {...props} />;
17
+
18
+ export { Pagination };
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import { TextField as TextFieldUI } from "@unimed/components";
3
+
4
+ /**
5
+ * Componente de campo de texto customizado da Unimed.
6
+ *
7
+ * @param {Object} props - Propriedades do componente.
8
+ * @param {string} [props.label] - Rótulo exibido acima do campo.
9
+ * @param {string} [props.placeholder] - Texto de dica dentro do campo.
10
+ * @param {string | number} props.value - Valor atual do campo (controlado).
11
+ * @param {Function} props.onChange - Callback chamado ao alterar o valor.
12
+ * @param {string} [props.type] - Tipo do input (text, password, email, etc).
13
+ * @param {string} [props.iconLeft] - Nome do ícone do Material Symbols para exibir à esquerda.
14
+ * @param {string} [props.iconRight] - Nome do ícone do Material Symbols para exibir à direita.
15
+ * @param {string} [props.helpText] - Texto de auxílio exibido abaixo do campo.
16
+ * @param {string} [props.error] - Mensagem de erro que muda o estado visual do campo.
17
+ * @param {boolean} [props.required] - Indica se o campo é obrigatório.
18
+ * @param {boolean} [props.loading] - Exibe um spinner de carregamento dentro do campo.
19
+ * @param {React.InputHTMLAttributes<HTMLInputElement>} props - Todas as outras propriedades nativas de um input HTML.
20
+ */
21
+ const TextField = (props) => <TextFieldUI {...props} />;
22
+
23
+ export { TextField };
@@ -0,0 +1,32 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js";
4
+ import { resolve } from "path";
5
+
6
+ export default defineConfig({
7
+ plugins: [react(), cssInjectedByJsPlugin()],
8
+ define: {
9
+ "process.env.NODE_ENV": '"production"',
10
+ },
11
+ build: {
12
+ outDir: "build/vite",
13
+ lib: {
14
+ entry: resolve(__dirname, "src/main.jsx"),
15
+ formats: ["es"],
16
+ fileName: () => "index.js",
17
+ },
18
+ rollupOptions: {
19
+ external: [
20
+ "axios",
21
+ "react",
22
+ "react-dom/client",
23
+ "react/jsx-runtime",
24
+ /^react\/.*/,
25
+ "@tanstack/react-query",
26
+ "@unimed/components",
27
+ "@unimed/hooks",
28
+ "@unimed/utils",
29
+ ],
30
+ },
31
+ },
32
+ });