Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion src/lib/component-templates/codegen-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,52 @@ function normalize(s: string): string {
}

export function safeGet(k: string | undefined): string {
return k ? `(item[${JSON.stringify(k)}] ?? '')` : "''";
if (!k) return "''";
if (k.includes(".")) {
const parts = k.split(".");
return `(${parts.map((p) => `item?.[${JSON.stringify(p)}]`).join(".") ?? "''"})`;
}
return `(item[${JSON.stringify(k)}] ?? '')`;
}

export function mapTsType(dt: string): string {
const d = dt.toLowerCase();
if (d.includes("boolean")) return "boolean";
if (
d.includes("number") ||
d.includes("int") ||
d.includes("float") ||
d.includes("price") ||
d.includes("amount")
)
return "number";
return "string";
}

/**
* Finds a field based on matching its faker dataType string.
*/
export function findFieldByDataType(
fields: SchemaField[],
dataTypePatterns: string[],
): string | undefined {
return fields.find((f) => {
const dt = f.dataType.toLowerCase();
return dataTypePatterns.some((pattern) => dt.includes(pattern.toLowerCase()));
})?.fieldName;
}

/**
* Finds all fields matching any of the given faker dataType patterns.
*/
export function filterFieldsByDataType(
fields: SchemaField[],
dataTypePatterns: string[],
): SchemaField[] {
return fields.filter((f) => {
const dt = f.dataType.toLowerCase();
return dataTypePatterns.some((pattern) => dt.includes(pattern.toLowerCase()));
});
}

/**
Expand Down
91 changes: 64 additions & 27 deletions src/lib/component-templates/comment-item.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { ComponentTemplate } from "./types";
import {
findField,
findFieldByDataType,
mapTsType,
buildFetchHook,
buildInterface,
buildHtmlFetchScript,
Expand All @@ -12,24 +14,47 @@ export const commentItemTemplate: ComponentTemplate = {
id: "comment-item",
name: "Comment Item",
description: "Avatar + author + comment text list. Handles single or list response.",
requiredFields: ["comment", "author"],
optionalFields: ["avatar", "createdAt", "id"],
requiredFields: [
"$faker.lorem.paragraph",
"$faker.lorem.sentence",
"$faker.person.fullName",
"$faker.internet.username",
],
optionalFields: [
"$faker.image.avatar",
"$faker.date.recent",
"$faker.date.past",
"$faker.string.uuid",
],
code: (fields, endpointUrl) => {
const idField = findField(fields, ["id", "commentid", "uuid"]) ?? "id";
const idField =
findFieldByDataType(fields, ["string.uuid", "string.nanoid", "number.int"]) ??
findField(fields, ["id", "commentid", "uuid"]) ??
fields[0]?.fieldName ??
"id";
const authorField =
findField(fields, ["author", "fullname", "username", "name"]) ?? "author";
findFieldByDataType(fields, [
"person.fullName",
"person.firstName",
"internet.username",
]) ??
findField(fields, ["author", "fullname", "username", "name"]) ??
"author";
const commentField =
findField(fields, ["comment", "message", "content", "text"]) ?? "comment";
const avatarField = findField(fields, ["avatar", "image", "photo"]);
const createdAtField = findField(fields, ["createdat", "date", "timestamp"]);
findFieldByDataType(fields, ["lorem.paragraph", "lorem.sentence", "lorem.sentences"]) ??
findField(fields, ["comment", "message", "content", "text"]) ??
"comment";
const avatarField =
findFieldByDataType(fields, ["image.avatar", "image.url", "image.datauri"]) ??
findField(fields, ["avatar", "image", "photo"]);
const createdAtField =
findFieldByDataType(fields, ["date.recent", "date.past", "date.anytime"]) ??
findField(fields, ["createdat", "date", "timestamp"]);

const interfaceLines = [
`${idField}: string;`,
`${authorField}: string;`,
`${commentField}: string;`,
];
if (avatarField) interfaceLines.push(`${avatarField}: string;`);
if (createdAtField) interfaceLines.push(`${createdAtField}: string;`);
const interfaceLines = fields.map((f) => {
const name = f.fieldName.includes(".") ? `"${f.fieldName}"?` : f.fieldName;
return `${name}: ${mapTsType(f.dataType)};`;
});

return `import { useEffect, useState } from "react";

Expand All @@ -40,21 +65,21 @@ ${buildFetchHook("CommentItem", endpointUrl, "Comment")}
export function CommentList() {
const { data, loading, error } = useCommentItemData();

if (loading) return <div className="p-4 text-sm text-gray-500">Loading...</div>;
if (error) return <div className="p-4 text-sm text-red-500">Error: {error}</div>;
if (loading) return <div style={{ padding: "1rem", fontSize: "0.875rem", color: "#71717a" }}>Loading...</div>;
if (error) return <div style={{ padding: "1rem", fontSize: "0.875rem", color: "#ef4444" }}>Error: {error}</div>;
if (!data) return null;

const comments = Array.isArray(data) ? data : [data];

return (
<div className="space-y-3">
<div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
{comments.map((comment) => (
<div key={comment.${idField}} className="flex gap-3">
${avatarField ? ` <img src={comment.${avatarField}} alt={comment.${authorField}} className="h-8 w-8 rounded-full object-cover" />\n` : ""} <div className="min-w-0 flex-1 rounded-lg border p-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">{comment.${authorField}}</span>
${createdAtField ? ` <span className="text-xs text-gray-400">{comment.${createdAtField}}</span>\n` : ""} </div>
<p className="mt-1 text-sm text-gray-600">{comment.${commentField}}</p>
<div key={comment.${idField}} style={{ display: "flex", gap: "0.75rem" }}>
${avatarField ? ` <img src={comment.${avatarField}} alt={comment.${authorField}} style={{ height: "2rem", width: "2rem", borderRadius: "9999px", objectFit: "cover", flexShrink: 0 }} />\n` : ""} <div style={{ minWidth: 0, flex: 1, borderRadius: "12px", border: "1px solid #e4e4e7", padding: "0.75rem", backgroundColor: "#ffffff" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<span style={{ fontSize: "0.875rem", fontWeight: 600, color: "#18181b" }}>{comment.${authorField}}</span>
${createdAtField ? ` <span style={{ fontSize: "0.75rem", color: "#a1a1aa" }}>{comment.${createdAtField}}</span>\n` : ""} </div>
<p style={{ marginTop: "0.25rem", marginBottom: 0, fontSize: "0.875rem", color: "#52525b", lineHeight: 1.5 }}>{comment.${commentField}}</p>
</div>
</div>
))}
Expand All @@ -65,11 +90,23 @@ ${createdAtField ? ` <span className="text-xs text-gray-400">{comme
},
htmlCode: (fields, endpointUrl) => {
const authorField =
findField(fields, ["author", "fullname", "username", "name"]) ?? "author";
findFieldByDataType(fields, [
"person.fullName",
"person.firstName",
"internet.username",
]) ??
findField(fields, ["author", "fullname", "username", "name"]) ??
"author";
const commentField =
findField(fields, ["comment", "message", "content", "text"]) ?? "comment";
const avatarField = findField(fields, ["avatar", "image", "photo"]);
const createdAtField = findField(fields, ["createdat", "date", "timestamp"]);
findFieldByDataType(fields, ["lorem.paragraph", "lorem.sentence", "lorem.sentences"]) ??
findField(fields, ["comment", "message", "content", "text"]) ??
"comment";
const avatarField =
findFieldByDataType(fields, ["image.avatar", "image.url", "image.datauri"]) ??
findField(fields, ["avatar", "image", "photo"]);
const createdAtField =
findFieldByDataType(fields, ["date.recent", "date.past", "date.anytime"]) ??
findField(fields, ["createdat", "date", "timestamp"]);

const avatarHtml = avatarField
? `\${${safeGet(avatarField)} ? \`<img class="avatar" style="flex-shrink:0;align-self:flex-start" src="\${${safeGet(avatarField)}}" alt="avatar">\` : \`<div class="avatar-placeholder" style="flex-shrink:0;align-self:flex-start">\${String(${safeGet(authorField)}).charAt(0).toUpperCase() || 'U'}</div>\`}`
Expand Down
38 changes: 19 additions & 19 deletions src/lib/component-templates/dynamic-grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,58 +170,58 @@ ${buildFetchHook("DataItem", endpointUrl, "DataItem")}
export function DataCardList() {
const { data, loading, error } = useDataItemData();

if (loading) return <div className="p-4 text-sm text-zinc-500 font-mono">Loading...</div>;
if (error) return <div className="p-4 text-sm text-rose-500 font-mono">Error: {error}</div>;
if (loading) return <div style={{ padding: "1rem", fontSize: "0.875rem", color: "#71717a", fontFamily: "monospace" }}>Loading...</div>;
if (error) return <div style={{ padding: "1rem", fontSize: "0.875rem", color: "#ef4444", fontFamily: "monospace" }}>Error: {error}</div>;
if (!data) return null;

const items = Array.isArray(data) ? data : [data];

return (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 font-sans">
<div style={{ display: "grid", gap: "1rem", gridTemplateColumns: "repeat(auto-fill, minmax(260px, 1fr))" }}>
{items.map((item, idx) => (
<div
key={String(${renderPropAccess(idField)} ?? idx)}
className="flex flex-col justify-between rounded-xl border border-zinc-200 dark:border-zinc-800 p-4 shadow-sm hover:shadow-md transition-shadow bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100"
style={{ display: "flex", flexDirection: "column", justifyContent: "space-between", borderRadius: "12px", border: "1px solid #e4e4e7", padding: "1rem", backgroundColor: "#ffffff", boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" }}
>
<div className="space-y-3">
<div className="flex items-center gap-3">
<div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
${
avatarField
? ` {${renderPropAccess(avatarField)} ? (
<img
src={String(${renderPropAccess(avatarField)})}
alt="Avatar"
className="h-11 w-11 rounded-full object-cover shrink-0 border border-zinc-200 dark:border-zinc-700 shadow-xs"
style={{ height: "2.75rem", width: "2.75rem", borderRadius: "9999px", objectFit: "cover", flexShrink: 0, border: "1px solid #e4e4e7" }}
/>
) : (
<div className="flex h-11 w-11 items-center justify-center rounded-full bg-zinc-100 dark:bg-zinc-800 font-bold text-zinc-600 dark:text-zinc-300 shrink-0 border border-zinc-200/50 dark:border-zinc-700/50">
<div style={{ display: "flex", height: "2.75rem", width: "2.75rem", alignItems: "center", justifyContent: "center", borderRadius: "9999px", backgroundColor: "#f4f4f5", fontWeight: 700, color: "#52525b", flexShrink: 0, border: "1px solid #e4e4e7" }}>
{${titleField ? `String(${renderPropAccess(titleField)} ?? "").charAt(0).toUpperCase() || ` : ""} "#"}
</div>
)}`
: ` <div className="flex h-11 w-11 items-center justify-center rounded-lg bg-zinc-100 dark:bg-zinc-800 font-bold text-zinc-600 dark:text-zinc-300 shrink-0 border border-zinc-200/50 dark:border-zinc-700/50">
: ` <div style={{ display: "flex", height: "2.75rem", width: "2.75rem", alignItems: "center", justifyContent: "center", borderRadius: "8px", backgroundColor: "#f4f4f5", fontWeight: 700, color: "#52525b", flexShrink: 0, border: "1px solid #e4e4e7" }}>
{${titleField ? `String(${renderPropAccess(titleField)} ?? "").charAt(0).toUpperCase() || ` : ""} "D"}
</div>`
}
<div className="min-w-0 flex-1">
<div style={{ minWidth: 0, flex: 1 }}>
${
titleField
? ` <h4 className="truncate font-semibold text-sm text-zinc-900 dark:text-zinc-100">{${renderPropAccess(titleField)}}</h4>\n`
: ` <h4 className="truncate font-semibold text-sm text-zinc-900 dark:text-zinc-100">Item #{idx + 1}</h4>\n`
}${subtitleField ? ` <p className="truncate text-xs text-zinc-500 dark:text-zinc-400 font-mono mt-0.5">{${renderPropAccess(subtitleField)}}</p>\n` : ""} </div>
? ` <h4 style={{ margin: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontWeight: 600, fontSize: "0.875rem", color: "#18181b" }}>{${renderPropAccess(titleField)}}</h4>\n`
: ` <h4 style={{ margin: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontWeight: 600, fontSize: "0.875rem", color: "#18181b" }}>Item #{idx + 1}</h4>\n`
}${subtitleField ? ` <p style={{ margin: "0.125rem 0 0 0", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontSize: "0.75rem", color: "#71717a", fontFamily: "monospace" }}>{${renderPropAccess(subtitleField)}}</p>\n` : ""} </div>
</div>

${descriptionField ? ` <p className="text-xs text-zinc-600 dark:text-zinc-300 leading-relaxed line-clamp-2">{${renderPropAccess(descriptionField)}}</p>\n` : ""} </div>
${descriptionField ? ` <p style={{ margin: 0, fontSize: "0.75rem", color: "#52525b", lineHeight: 1.5, display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical", overflow: "hidden" }}>{${renderPropAccess(descriptionField)}}</p>\n` : ""} </div>

${
detailFields.length > 0
? ` <div className="mt-4 pt-3 border-t border-zinc-100 dark:border-zinc-800 text-xs space-y-1.5">
? ` <div style={{ marginTop: "1rem", paddingTop: "0.75rem", borderTop: "1px solid #f4f4f5", fontSize: "0.75rem", display: "flex", flexDirection: "column", gap: "0.375rem" }}>
${detailFields
.map((f) => {
const propAccess = renderPropAccess(f.fieldName);
return ` {${propAccess} !== undefined && (
<div className="flex justify-between items-center gap-2">
<span className="text-zinc-400 font-mono text-[11px] font-medium">${f.fieldName}:</span>
<span className="font-mono text-zinc-700 dark:text-zinc-300 truncate text-[11px]">{String(${propAccess})}</span>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: "0.5rem" }}>
<span style={{ color: "#a1a1aa", fontFamily: "monospace", fontSize: "11px", fontWeight: 500 }}>${f.fieldName}:</span>
<span style={{ fontFamily: "monospace", color: "#3f3f46", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontSize: "11px" }}>{String(${propAccess})}</span>
</div>
)}`;
})
Expand Down Expand Up @@ -338,7 +338,7 @@ ${detailFields
: "";
const detailsHtml =
detailFields.length > 0
? `<div class="details">\${[${detailFields.map((f) => `[${JSON.stringify(f.fieldName)}, item[${JSON.stringify(f.fieldName)}]]`).join(", ")}].filter(([,v]) => v !== undefined).map(([k,v]) => \`<div class="detail-row"><span class="detail-key">\${k}:</span><span>\${v}</span></div>\`).join('')}</div>`
? `<div class="details">\${[${detailFields.map((f) => `[${JSON.stringify(f.fieldName)}, ${safeGet(f.fieldName)}]`).join(", ")}].filter(([,v]) => v !== undefined && v !== '').map(([k,v]) => \`<div class="detail-row"><span class="detail-key">\${k}:</span><span>\${v}</span></div>\`).join('')}</div>`
: "";

const renderFn = `item => \`<div class="card">
Expand Down
12 changes: 10 additions & 2 deletions src/lib/component-templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ function fieldMatches(fieldNameNormalized: string, candidateNormalized: string):

/**
* Scores every template against the given schema fields and endpoint URL path.
* Matches against field $faker dataTypes (and fieldNames as fallback).
* requiredFields matches are weighted heavily (10x);
* path segment matches add a strong boost (8x);
* optionalFields matches add a smaller boost.
*/
export function matchTemplates(fields: SchemaField[], endpointPath: string = ""): TemplateMatch[] {
const normalizedFieldNames = fields.map((f) => normalize(f.fieldName));
const normalizedDataTypes = fields.map((f) => normalize(f.dataType));
const normalizedPath = normalize(endpointPath);

const results: TemplateMatch[] = COMPONENT_TEMPLATES.map((template) => {
Expand All @@ -58,13 +60,19 @@ export function matchTemplates(fields: SchemaField[], endpointPath: string = "")

for (const required of template.requiredFields) {
const normalizedRequired = normalize(required);
if (normalizedFieldNames.some((fn) => fieldMatches(fn, normalizedRequired))) {
if (
normalizedDataTypes.some((dt) => fieldMatches(dt, normalizedRequired)) ||
normalizedFieldNames.some((fn) => fieldMatches(fn, normalizedRequired))
) {
matchedFields.push(required);
}
}
for (const optional of template.optionalFields) {
const normalizedOptional = normalize(optional);
if (normalizedFieldNames.some((fn) => fieldMatches(fn, normalizedOptional))) {
if (
normalizedDataTypes.some((dt) => fieldMatches(dt, normalizedOptional)) ||
normalizedFieldNames.some((fn) => fieldMatches(fn, normalizedOptional))
) {
matchedFields.push(optional);
}
}
Expand Down
Loading
Loading