From 47b41eaeb419719c04d7029ff2479e89507ec05e Mon Sep 17 00:00:00 2001 From: Faisal Ahmed Sifat Date: Sat, 22 Feb 2025 17:39:25 +0600 Subject: [PATCH 01/10] feat: adding functionality to change template manually --- package-lock.json | 5 +- .../[id]/resources/new/NewResourceForm.tsx | 450 ++++++++++++++++-- src/server/actions/projects.ts | 2 +- 3 files changed, 423 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index ff2a8d0..914fb6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,13 @@ { - "name": "mockapi.io", + "name": "mock-api", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "mockapi.io", + "name": "mock-api", "version": "0.1.0", + "hasInstallScript": true, "dependencies": { "@faker-js/faker": "^9.5.0", "@paralleldrive/cuid2": "^2.2.2", diff --git a/src/app/projects/[id]/resources/new/NewResourceForm.tsx b/src/app/projects/[id]/resources/new/NewResourceForm.tsx index 44daf07..926f0bb 100644 --- a/src/app/projects/[id]/resources/new/NewResourceForm.tsx +++ b/src/app/projects/[id]/resources/new/NewResourceForm.tsx @@ -1,19 +1,222 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import { createResource } from "@/server/actions/resources"; +// Enhanced template field structure to support nesting +interface TemplateField { + key: string; + type: "simple" | "object" | "array"; + arrayType?: "simple" | "object"; // New field to specify array type + module?: string; + method?: string; + params?: string[]; + fields?: TemplateField[]; // For nested objects + items?: TemplateField; // For array items + count?: number; // Number of items to generate +} + +// Define available faker modules and their methods +const FAKER_MODULES = { + internet: { + label: "Internet", + methods: { + userName: "Username", + email: "Email Address", + password: "Password", + url: "URL", + ip: "IP Address", + avatar: "Avatar URL", + }, + }, + name: { + label: "Person", + methods: { + firstName: "First Name", + lastName: "Last Name", + fullName: "Full Name", + jobTitle: "Job Title", + }, + }, + number: { + label: "Numbers", + methods: { + int: "Integer", + float: "Decimal Number", + }, + }, + date: { + label: "Dates", + methods: { + past: "Past Date", + future: "Future Date", + recent: "Recent Date", + }, + }, + lorem: { + label: "Text", + methods: { + word: "Single Word", + words: "Multiple Words", + sentence: "Sentence", + paragraph: "Paragraph", + }, + }, + // Add more modules as needed +}; + +const FIELD_TYPES = [ + { value: "simple", label: "Simple Value" }, + { value: "object", label: "Object" }, + { value: "array", label: "Array" }, +]; + interface NewResourceFormProps { projectId: string; } +interface FieldBuilderProps { + field: TemplateField; + onUpdate: (updates: Partial) => void; + onRemove: () => void; + depth?: number; +} + +function FieldBuilder({ field, onUpdate, onRemove, depth = 0 }: FieldBuilderProps) { + const handleTypeChange = (type: "simple" | "object" | "array") => { + const updates: Partial = { type }; + if (type === "object") { + updates.fields = []; + updates.module = undefined; + updates.method = undefined; + } else if (type === "array") { + updates.arrayType = "simple"; + updates.count = 3; + updates.items = { key: "", type: "simple" }; + updates.module = undefined; + updates.method = undefined; + } + onUpdate(updates); + }; + + return ( +
0 ? 'ml-8' : ''}`}> +
+ onUpdate({ key: e.target.value })} + placeholder="Field name" + className="w-full rounded-md border border-gray-600 bg-gray-700 text-gray-100 p-2" + /> +
+ +
+ +
+ + {field.type === "simple" && ( + <> +
+ +
+ +
+ +
+ + )} + + {field.type === "array" && ( + <> +
+ +
+
+ onUpdate({ count: parseInt(e.target.value) })} + min={1} + max={10} + className="w-full rounded-md border border-gray-600 bg-gray-700 text-gray-100 p-2" + /> +
+ + )} + + +
+ ); +} + +interface TemplateValue { + [key: string]: string | TemplateValue | TemplateValue[]; +} + +interface EditorMode { + mode: "visual" | "manual"; + template: string; +} + export default function NewResourceForm({ projectId }: NewResourceFormProps) { const router = useRouter(); const [name, setName] = useState(""); const [version, setVersion] = useState("v1"); - const [templateText, setTemplateText] = useState(""); + const [fields, setFields] = useState([]); const [error, setError] = useState(null); + const [editorState, setEditorState] = useState({ + mode: "visual", + template: "{}", + }); // Function to convert string to slug const toSlug = (str: string) => { @@ -30,10 +233,132 @@ export default function NewResourceForm({ projectId }: NewResourceFormProps) { setName(sluggedName); }; + const addField = (parentFields?: TemplateField[]) => { + const newField: TemplateField = { key: "", type: "simple" }; + if (parentFields) { + parentFields.push(newField); + setFields([...fields]); + } else { + setFields([...fields, newField]); + } + }; + + const updateField = (field: TemplateField, updates: Partial) => { + Object.assign(field, updates); + setFields([...fields]); + }; + + const removeField = (fieldToRemove: TemplateField, parentFields?: TemplateField[]) => { + if (parentFields) { + const index = parentFields.indexOf(fieldToRemove); + if (index !== -1) { + parentFields.splice(index, 1); + setFields([...fields]); + } + } else { + setFields(fields.filter(f => f !== fieldToRemove)); + } + }; + + const generateTemplate = (templateFields: TemplateField[] = fields): TemplateValue => { + return templateFields.reduce((acc: TemplateValue, field) => { + if (!field.key) return acc; + + if (field.type === "simple" && field.module && field.method) { + acc[field.key] = `$${field.module}.${field.method}`; + } else if (field.type === "object" && field.fields) { + acc[field.key] = generateTemplate(field.fields); + } else if (field.type === "array" && field.items) { + const count = field.count || 3; + + if (field.arrayType === "simple" && field.items.module && field.items.method) { + // For simple arrays, create an array of faker methods + acc[field.key] = Array(count).fill(`$${field.items.module}.${field.items.method}`); + } else if (field.arrayType === "object" && field.items.fields) { + // For object arrays, create an array of object templates + const itemTemplate = generateTemplate(field.items.fields); + acc[field.key] = Array(count).fill(itemTemplate); + } + } + return acc; + }, {}); + }; + + const renderFields = (templateFields: TemplateField[] = fields, depth = 0) => { + return ( +
+ {templateFields.map((field, index) => ( +
+ updateField(field, updates)} + onRemove={() => removeField(field, depth > 0 ? templateFields : undefined)} + depth={depth} + /> + + {field.type === "object" && field.fields && ( +
+ {renderFields(field.fields, depth + 1)} + +
+ )} + + {field.type === "array" && field.items && ( +
+
+ Array Item Template ({field.arrayType === "simple" ? "Simple Values" : "Objects"}): +
+ {field.arrayType === "simple" ? ( + updateField(field.items!, updates)} + onRemove={() => {}} + depth={depth + 1} + /> + ) : ( + <> + {renderFields(field.items.fields || [], depth + 1)} + + + )} +
+ )} +
+ ))} +
+ ); + }; + + const handleTemplateChange = (value: string) => { + try { + // Validate JSON + JSON.parse(value); + setEditorState({ ...editorState, template: value }); + setError(null); + } catch (e) { + setError("Invalid JSON format"); + } + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { - const template = JSON.parse(templateText); + const template = editorState.mode === "visual" + ? generateTemplate() + : JSON.parse(editorState.template); + const endpoint = `${version}/${name}`; await createResource(projectId, { name, @@ -43,10 +368,17 @@ export default function NewResourceForm({ projectId }: NewResourceFormProps) { router.push(`/projects/${projectId}`); } catch (error) { console.error(error); - setError("Invalid JSON format. Please check your template."); + setError("Failed to create resource. Please try again."); } }; + useEffect(() => { + if (editorState.mode === "visual") { + const template = generateTemplate(); + setEditorState(prev => ({ ...prev, template: JSON.stringify(template, null, 2) })); + } + }, [fields]); + return (

@@ -95,47 +427,103 @@ export default function NewResourceForm({ projectId }: NewResourceFormProps) {

-
- -