@mlw-packages/create-template 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/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # malwee · templates
2
+
3
+ Coleção de templates e um gerador simples para iniciar projetos rapidamente.
4
+
5
+ **Resumo:** este repositório contém templates de projetos (por exemplo, Next.js) e um utilitário CLI leve para copiar um template e ajustar os metadados do package.json para criar um novo projeto.
6
+
7
+ **Principais recursos**
8
+
9
+ - Gerador interativo via bin/create-template.js para escolher template, nome, gerenciador de pacotes e extras.
10
+ - Templates prontos em src/templates/ (ex.: Next.js).
11
+ - Suporte opcional a extras como Prisma, Husky, React Hook Form e Docker.
12
+
13
+ **Requisitos**
14
+
15
+ - Node.js 18+ instalado
16
+ - Um gerenciador de pacotes (recomendado: pnpm, também funciona com npm ou yarn)
17
+
18
+ **Instalação (no repositório)**
19
+
20
+ 1. Clone este repositório ou baixe os fontes.
21
+ 2. Instale dependências de desenvolvimento (repositório raiz):
22
+
23
+ ```bash
24
+ pnpm install
25
+ # ou
26
+ npm install
27
+ ```
28
+
29
+ **Como usar o gerador**
30
+
31
+ O gerador interativo está em [bin/create-template.js](bin/create-template.js#L1-L200). Execute-o diretamente com Node a partir da raiz do repositório:
32
+
33
+ ```bash
34
+ node ./bin/create-template.js
35
+ ```
36
+
37
+ Fluxo básico que o gerador realiza:
38
+
39
+ - Escolha se o projeto será criado no diretório atual ou em uma nova pasta.
40
+ - Escolha o template (por exemplo, Next.js).
41
+ - Escolha o gerenciador de pacotes (pnpm, npm, yarn).
42
+ - Opcionalmente selecione extras (Prisma, Docker, Husky, React Hook Form, Zod, Zustand).
43
+ - O gerador copia os arquivos do template para o diretório destino, atualiza package.json com o name e adiciona dependências extras quando selecionadas.
44
+ - Opcionalmente executa install no diretório criado.
45
+
46
+ Exemplo mínimo (cria no diretório atual):
47
+
48
+ ```bash
49
+ # no diretório do template (root)
50
+ node ./bin/create-template.js
51
+ # siga as opções interativas (escolha "Neste diretório (root)")
52
+ ```
53
+
54
+ Ou para criar em uma pasta nova chamada meu-app:
55
+
56
+ ```bash
57
+ node ./bin/create-template.js
58
+ # escolha "Em uma nova pasta" e informe "meu-app"
59
+ ```
60
+
61
+ Após a criação, os próximos passos geralmente são:
62
+
63
+ ```bash
64
+ cd meu-app # se criou em nova pasta
65
+ pnpm install # ou npm install / yarn
66
+ pnpm run dev # iniciar modo desenvolvimento conforme template
67
+ ```
68
+
69
+ **Templates incluídos**
70
+
71
+ - src/templates/next — template Next.js (App Router, Tailwind, tRPC)
72
+ - src/templates/vite-react — template Vite + React
73
+
74
+ Veja um exemplo de template e documentação interna em [src/templates/next/README.md](src/templates/next/README.md).
75
+
76
+ **Como adicionar um novo template**
77
+
78
+ 1. Crie uma nova pasta em src/templates/<nome-do-template> com a estrutura do projeto que deseja usar como base.
79
+ 2. Inclua um package.json com os scripts padrões (dev, build, start) para o template.
80
+ 3. Atualize as opções no gerador em [bin/create-template.js](bin/create-template.js#L1-L200) para expor o novo template (adicione uma opção no array options do prompt Template).
81
+ 4. Teste localmente executando node ./bin/create-template.js e selecionando seu novo template.
82
+
83
+ Dicas:
84
+
85
+ - Mantenha os templates o mais enxutos possível e parametrize nomes no package.json apenas pelo campo name (o gerador substitui esse campo automaticamente).
86
+ - Se o template precisar de instruções específicas (migrations, variáveis de ambiente), adicione um README.md dentro da pasta do template.
87
+
88
+ **Estrutura do repositório**
89
+
90
+ - bin/ — utilitários CLI, incluindo [bin/create-template.js](bin/create-template.js#L1-L200)
91
+ - src/templates/ — pasta com templates disponíveis (next, vite-react, ...)
92
+ - package.json — manifesto do projeto (raiz)
93
+
@@ -0,0 +1,295 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs-extra";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { spawnSync } from "child_process";
6
+ import {
7
+ intro,
8
+ outro,
9
+ text,
10
+ select,
11
+ multiselect,
12
+ confirm,
13
+ isCancel,
14
+ cancel,
15
+ log,
16
+ spinner,
17
+ } from "@clack/prompts";
18
+
19
+
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = path.dirname(__filename);
22
+
23
+ const timeSince = (start) =>
24
+ `${((Date.now() - start) / 1000).toFixed(1)}s`;
25
+
26
+ const slugify = (name) =>
27
+ name
28
+ .toLowerCase()
29
+ .replace(/\s+/g, "-")
30
+ .replace(/[^a-z0-9-_]/g, "")
31
+ .replace(/^-+|-+$/g, "") || "my-app";
32
+
33
+
34
+ const DEP_VERSIONS = {
35
+ prisma: "^5.10.0",
36
+ "@prisma/client": "^5.10.0",
37
+ "react-hook-form": "^7.50.0",
38
+ "@hookform/resolvers": "^3.3.4",
39
+ zod: "^3.23.8",
40
+ zustand: "^4.5.2",
41
+ };
42
+
43
+ const DEV_DEP_VERSIONS = {
44
+ husky: "^9.0.11",
45
+ };
46
+
47
+
48
+ async function main() {
49
+ console.clear();
50
+ intro("malwee · templates");
51
+
52
+ const startAll = Date.now();
53
+
54
+ const location = await select({
55
+ message: "Onde deseja criar o projeto?",
56
+ options: [
57
+ {
58
+ label: "Neste diretório (root)",
59
+ hint: "usa o diretório atual",
60
+ value: "root",
61
+ },
62
+ {
63
+ label: "Em uma nova pasta",
64
+ hint: "cria uma pasta com o nome do projeto",
65
+ value: "folder",
66
+ },
67
+ ],
68
+ });
69
+
70
+ if (isCancel(location)) {
71
+ cancel("Cancelado");
72
+ process.exit(0);
73
+ }
74
+
75
+ let projectName;
76
+ let targetDir = process.cwd();
77
+
78
+ if (location === "folder") {
79
+ const rawName = await text({
80
+ message: "Nome do projeto",
81
+ initialValue: "prj-malwee",
82
+ });
83
+
84
+ if (isCancel(rawName)) {
85
+ cancel("Cancelado");
86
+ process.exit(0);
87
+ }
88
+
89
+ projectName = slugify(rawName);
90
+ targetDir = path.resolve(process.cwd(), projectName);
91
+
92
+ if (fs.existsSync(targetDir)) {
93
+ cancel("Diretório já existe");
94
+ process.exit(1);
95
+ }
96
+ } else {
97
+ projectName = path.basename(process.cwd());
98
+ }
99
+
100
+ const template = await select({
101
+ message: "Template",
102
+ options: [
103
+ {
104
+ label: "Next.js",
105
+ hint: "App Router · Tailwind · tRPC",
106
+ value: "next",
107
+ },
108
+ {
109
+ label: "Vite + React",
110
+ hint: "React · TypeScript · Tailwind",
111
+ value: "vite-react",
112
+ },
113
+ ],
114
+ });
115
+
116
+ if (isCancel(template)) {
117
+ cancel("Cancelado");
118
+ process.exit(0);
119
+ }
120
+
121
+ const pm = await select({
122
+ message: "Gerenciador de pacotes",
123
+ options: [
124
+ { label: "pnpm (recomendado)", value: "pnpm" },
125
+ { label: "npm", value: "npm" },
126
+ { label: "yarn", value: "yarn" },
127
+ ],
128
+ });
129
+
130
+ if (isCancel(pm)) {
131
+ cancel("Cancelado");
132
+ process.exit(0);
133
+ }
134
+
135
+ const addExtras = await confirm({
136
+ message: "Adicionar dependências extras?",
137
+ initialValue: true,
138
+ });
139
+
140
+ if (isCancel(addExtras)) {
141
+ cancel("Cancelado");
142
+ process.exit(0);
143
+ }
144
+
145
+ let extras = [];
146
+
147
+ if (addExtras) {
148
+ const selected = await multiselect({
149
+ message: "Selecione as dependências",
150
+ required: false,
151
+ options: [
152
+ { label: "Prisma", value: "prisma" },
153
+ { label: "Docker", value: "docker" },
154
+ { label: "Husky", value: "husky" },
155
+ { label: "React Hook Form", value: "hookform" },
156
+ { label: "Hook Form Resolvers + Zod", value: "resolvers" },
157
+ { label: "Zustand", value: "zustand" },
158
+ ],
159
+ });
160
+
161
+ if (isCancel(selected)) {
162
+ cancel("Cancelado");
163
+ process.exit(0);
164
+ }
165
+
166
+ extras = selected;
167
+ }
168
+
169
+ const runInstall = await confirm({
170
+ message: `Executar "${pm} install" automaticamente?`,
171
+ initialValue: false,
172
+ });
173
+
174
+ if (isCancel(runInstall)) {
175
+ cancel("Cancelado");
176
+ process.exit(0);
177
+ }
178
+
179
+ log.step("1/4 Criando estrutura do projeto");
180
+
181
+ const templateDir = path.resolve(
182
+ __dirname,
183
+ `../src/templates/${template}`
184
+ );
185
+
186
+ fs.copySync(templateDir, targetDir);
187
+
188
+ log.step("2/4 Configurando dependências");
189
+
190
+ const pkgPath = path.join(targetDir, "package.json");
191
+ const pkg = fs.readJsonSync(pkgPath);
192
+
193
+ pkg.name = projectName;
194
+ pkg.dependencies ||= {};
195
+ pkg.devDependencies ||= {};
196
+
197
+ if (extras.includes("prisma")) {
198
+ pkg.dependencies["@prisma/client"] = DEP_VERSIONS["@prisma/client"];
199
+ pkg.devDependencies["prisma"] = DEP_VERSIONS.prisma;
200
+ }
201
+
202
+ if (extras.includes("hookform")) {
203
+ pkg.dependencies["react-hook-form"] =
204
+ DEP_VERSIONS["react-hook-form"];
205
+ }
206
+
207
+ if (extras.includes("resolvers")) {
208
+ pkg.dependencies["@hookform/resolvers"] =
209
+ DEP_VERSIONS["@hookform/resolvers"];
210
+ pkg.dependencies["zod"] = DEP_VERSIONS.zod;
211
+ }
212
+
213
+ if (extras.includes("zustand")) {
214
+ pkg.dependencies["zustand"] = DEP_VERSIONS.zustand;
215
+ }
216
+
217
+ if (extras.includes("husky")) {
218
+ pkg.devDependencies["husky"] = DEV_DEP_VERSIONS.husky;
219
+ }
220
+
221
+ fs.writeJsonSync(pkgPath, pkg, { spaces: 2 });
222
+ log.success("✔ package.json atualizado");
223
+
224
+ if (extras.includes("docker")) {
225
+ fs.writeFileSync(
226
+ path.join(targetDir, "Dockerfile"),
227
+ `FROM node:20-alpine
228
+ WORKDIR /app
229
+ COPY . .
230
+ RUN ${pm} install
231
+ RUN ${pm} run build
232
+ CMD ["${pm}", "start"]
233
+ `
234
+ );
235
+ log.success("✔ Dockerfile criado");
236
+ }
237
+
238
+ log.step("3/4 Gerando arquivos básicos");
239
+
240
+ fs.writeFileSync(
241
+ path.join(targetDir, "README.md"),
242
+ `# ${projectName}
243
+
244
+ Template: ${template}
245
+
246
+ ## Comandos
247
+
248
+ \`\`\`bash
249
+ ${pm} install
250
+ ${pm} run dev
251
+ \`\`\`
252
+
253
+ Extras: ${extras.length ? extras.join(", ") : "nenhum"}
254
+ `
255
+ );
256
+
257
+ fs.writeFileSync(
258
+ path.join(targetDir, ".gitignore"),
259
+ `node_modules
260
+ .env
261
+ dist
262
+ build
263
+ `
264
+ );
265
+
266
+ if (runInstall) {
267
+ const sp = spinner();
268
+ sp.start({ text: `${pm} install` });
269
+
270
+ spawnSync(pm, ["install"], {
271
+ cwd: targetDir,
272
+ stdio: "inherit",
273
+ shell: process.platform === "win32",
274
+ });
275
+
276
+ sp.stop();
277
+ }
278
+
279
+ log.step("4/4 Finalizando");
280
+ log.success(`✔ Projeto criado em ${timeSince(startAll)}`);
281
+
282
+ outro(
283
+ `📦 Próximos passos:
284
+
285
+ cd ${location === "folder" ? projectName : "."}
286
+ ${pm} install
287
+ ${pm} run dev
288
+ `
289
+ );
290
+ }
291
+
292
+ main().catch((err) => {
293
+ console.error(err);
294
+ process.exit(1);
295
+ });
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@mlw-packages/create-template",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "bin": {
7
+ "create-template": "./bin/create-template.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "dependencies": {
17
+ "@clack/prompts": "^1.0.0",
18
+ "chalk": "^5.6.2",
19
+ "fs-extra": "^11.2.0",
20
+ "gradient-string": "^3.0.0",
21
+ "ora": "^9.3.0",
22
+ "prompts": "^2.4.2"
23
+ }
24
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "eslint.run": "onType",
3
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
4
+ "editor.formatOnSave": true,
5
+ "[typescript]": {
6
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
7
+ },
8
+ "[javascript]": {
9
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
10
+ },
11
+ "editor.codeActionsOnSave": {
12
+ "source.fixAll.eslint": "always",
13
+ "source.organizeImports": "always",
14
+ "source.addMissingImports.ts": "always",
15
+ "source.fixAll.ts": "always"
16
+ },
17
+ "editor.fontLigatures": true,
18
+ "javascript.updateImportsOnFileMove.enabled": "always",
19
+ "typescript.updateImportsOnFileMove.enabled": "always",
20
+ "[prisma]": {
21
+ "editor.defaultFormatter": "Prisma.prisma"
22
+ },
23
+ "editor.insertSpaces": true,
24
+ "editor.tabSize": 2,
25
+ "github.copilot.enable": {
26
+ "*": true,
27
+ "dotenv": false
28
+ }
29
+ }
@@ -0,0 +1,36 @@
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
@@ -0,0 +1,18 @@
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
@@ -0,0 +1,8 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ /* config options here */
5
+ reactCompiler: true,
6
+ };
7
+
8
+ export default nextConfig;
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "next",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "@tanstack/react-query": "^5.90.21",
13
+ "@trpc/client": "^11.10.0",
14
+ "@trpc/react-query": "^11.10.0",
15
+ "@trpc/server": "^11.10.0",
16
+ "next": "16.1.6",
17
+ "react": "19.2.3",
18
+ "react-dom": "19.2.3",
19
+ "server-only": "^0.0.1",
20
+ "zod": "^4.3.6"
21
+ },
22
+ "devDependencies": {
23
+ "@tailwindcss/postcss": "^4",
24
+ "@types/node": "^20",
25
+ "@types/react": "^19",
26
+ "@types/react-dom": "^19",
27
+ "babel-plugin-react-compiler": "1.0.0",
28
+ "eslint": "^9",
29
+ "eslint-config-next": "16.1.6",
30
+ "tailwindcss": "^4",
31
+ "typescript": "^5"
32
+ }
33
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1 @@
1
+ <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
@@ -0,0 +1,12 @@
1
+ import { appRouter } from '@/trpc/server';
2
+ import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
3
+
4
+ const handler = (req: Request) =>
5
+ fetchRequestHandler({
6
+ endpoint: '/api/trpc',
7
+ req,
8
+ router: appRouter,
9
+ createContext: () => ({}),
10
+ });
11
+
12
+ export { handler as GET, handler as POST };
@@ -0,0 +1,26 @@
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --background: #ffffff;
5
+ --foreground: #171717;
6
+ }
7
+
8
+ @theme inline {
9
+ --color-background: var(--background);
10
+ --color-foreground: var(--foreground);
11
+ --font-sans: var(--font-geist-sans);
12
+ --font-mono: var(--font-geist-mono);
13
+ }
14
+
15
+ @media (prefers-color-scheme: dark) {
16
+ :root {
17
+ --background: #0a0a0a;
18
+ --foreground: #ededed;
19
+ }
20
+ }
21
+
22
+ body {
23
+ background: var(--background);
24
+ color: var(--foreground);
25
+ font-family: Arial, Helvetica, sans-serif;
26
+ }
@@ -0,0 +1,25 @@
1
+ import type { Metadata } from 'next';
2
+ import { Inter } from 'next/font/google';
3
+ import './globals.css';
4
+ import TRPCProvider from '@/trpc/Provider';
5
+
6
+ const inter = Inter({ subsets: ['latin'] });
7
+
8
+ export const metadata: Metadata = {
9
+ title: 'Create Next App',
10
+ description: 'Generated by create next app',
11
+ };
12
+
13
+ export default function RootLayout({
14
+ children,
15
+ }: {
16
+ children: React.ReactNode;
17
+ }) {
18
+ return (
19
+ <html lang="en">
20
+ <body className={inter.className}>
21
+ <TRPCProvider>{children}</TRPCProvider>
22
+ </body>
23
+ </html>
24
+ );
25
+ }
@@ -0,0 +1,15 @@
1
+ 'use client';
2
+
3
+ import { Suspense } from 'react';
4
+ import { ResourcesList } from '@/features/resources/components/ResourcesList';
5
+
6
+ export default function Home() {
7
+ return (
8
+ <main className="flex min-h-screen flex-col items-center p-24">
9
+ <h1 className="text-4xl font-bold mb-8">Resource List</h1>
10
+ <Suspense fallback={<div className="text-2xl font-bold">Loading...</div>}>
11
+ <ResourcesList />
12
+ </Suspense>
13
+ </main>
14
+ );
15
+ }
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+
3
+ import { useListResourcesQuery } from '../queries/useListResourcesQuery';
4
+
5
+ export function ResourcesList() {
6
+ const [data] = useListResourcesQuery();
7
+
8
+ return (
9
+ <div className="grid gap-4 w-full max-w-2xl">
10
+ {data.map((resource) => (
11
+ <div
12
+ key={resource.id}
13
+ className="p-6 border rounded-lg shadow-sm hover:shadow-md transition-shadow bg-gray-100 border-gray-500"
14
+ >
15
+ <h2 className="text-xl font-semibold">{resource.title}</h2>
16
+ <p className={`mt-2 inline-block px-2 py-1 rounded text-sm ${
17
+ resource.status === 'active' ? 'bg-green-900 text-green-100' :
18
+ resource.status === 'inactive' ? 'bg-gray-700 text-gray-300' :
19
+ 'bg-yellow-900 text-yellow-100'
20
+ }`}>
21
+ {resource.status}
22
+ </p>
23
+ </div>
24
+ ))}
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,5 @@
1
+ import { trpc } from '@/trpc/client';
2
+
3
+ export const useListResourcesQuery = () => {
4
+ return trpc.resource.list.useSuspenseQuery();
5
+ };
@@ -0,0 +1,29 @@
1
+ import 'server-only';
2
+
3
+ export type Resource = {
4
+ id: string;
5
+ title: string;
6
+ status: 'active' | 'inactive' | 'archived';
7
+ };
8
+
9
+ const MOCK_RESOURCES: Resource[] = [
10
+ { id: '1', title: 'Resource 1', status: 'active' },
11
+ { id: '2', title: 'Resource 2', status: 'inactive' },
12
+ { id: '3', title: 'Resource 3', status: 'archived' },
13
+ { id: '4', title: 'Resource 4', status: 'active' },
14
+ { id: '5', title: 'Resource 5', status: 'active' },
15
+ ];
16
+
17
+ export async function getResources(query?: string): Promise<Resource[]> {
18
+ // Simulate database delay of 1000ms
19
+ await new Promise((resolve) => setTimeout(resolve, 1000));
20
+
21
+ if (!query) {
22
+ return MOCK_RESOURCES;
23
+ }
24
+
25
+ const lowerQuery = query.toLowerCase();
26
+ return MOCK_RESOURCES.filter((r) =>
27
+ r.title.toLowerCase().includes(lowerQuery)
28
+ );
29
+ }
@@ -0,0 +1,25 @@
1
+ 'use client';
2
+
3
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
+ import { httpBatchLink } from '@trpc/client';
5
+ import React, { useState } from 'react';
6
+ import { trpc } from './client';
7
+
8
+ export default function TRPCProvider({ children }: { children: React.ReactNode }) {
9
+ const [queryClient] = useState(() => new QueryClient());
10
+ const [trpcClient] = useState(() =>
11
+ trpc.createClient({
12
+ links: [
13
+ httpBatchLink({
14
+ url: '/api/trpc',
15
+ }),
16
+ ],
17
+ })
18
+ );
19
+
20
+ return (
21
+ <trpc.Provider client={trpcClient} queryClient={queryClient}>
22
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
23
+ </trpc.Provider>
24
+ );
25
+ }
@@ -0,0 +1,4 @@
1
+ import { createTRPCReact } from '@trpc/react-query';
2
+ import { AppRouter } from './server';
3
+
4
+ export const trpc = createTRPCReact<AppRouter>();
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+ import { router, publicProcedure } from './trpc';
3
+ import { getResources } from '@/features/resources/services/resource-service';
4
+
5
+ export const appRouter = router({
6
+ resource: router({
7
+ list: publicProcedure
8
+ .input(z.string().optional())
9
+ .query(async ({ input }) => {
10
+ return await getResources(input);
11
+ }),
12
+ }),
13
+ });
14
+
15
+ export type AppRouter = typeof appRouter;
@@ -0,0 +1,6 @@
1
+ import { initTRPC } from '@trpc/server';
2
+
3
+ const t = initTRPC.create();
4
+
5
+ export const router = t.router;
6
+ export const publicProcedure = t.procedure;
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./src/*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }