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
26 changes: 21 additions & 5 deletions src/lib/component-templates/codegen-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { SchemaField } from "~/lib/faker-options";
import type { TemplateOptions } from "./types";

function normalize(s: string): string {
return s.toLowerCase().replace(/[_\s-]/g, "");
Expand Down Expand Up @@ -72,14 +73,20 @@ export function buildFetchHook(
hookName: string,
endpointUrl: string,
itemTypeName: string,
options?: TemplateOptions,
): string {
const tokenVal = options?.bearerToken || "YOUR_TOKEN_HERE";
const fetchArgs = options?.requiresAuth
? `"${endpointUrl}", {\n headers: {\n "Authorization": "Bearer ${tokenVal}"\n }\n }`
: `"${endpointUrl}"`;

return `function use${hookName}Data() {
const [data, setData] = useState<${itemTypeName}[] | ${itemTypeName} | null>(null);
const [data, setData] = useState<${itemTypeName}[] | ${itemTypeName} | undefined>(undefined);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [error, setError] = useState<string | undefined>(undefined);

useEffect(() => {
fetch("${endpointUrl}")
fetch(${fetchArgs})
.then((res) => {
if (!res.ok) throw new Error(\`Request failed: \${res.status}\`);
return res.json();
Expand All @@ -104,13 +111,22 @@ ${fieldLines.map((l) => ` ${l}`).join("\n")}
* `renderFn` is a JS expression (string) that maps a single item to an
* HTML string — the caller provides the item-level template string.
*/
export function buildHtmlFetchScript(endpointUrl: string, renderFn: string): string {
export function buildHtmlFetchScript(
endpointUrl: string,
renderFn: string,
options?: TemplateOptions,
): string {
const tokenVal = options?.bearerToken || "YOUR_TOKEN_HERE";
const fetchArgs = options?.requiresAuth
? `"${endpointUrl}", {\n headers: {\n "Authorization": "Bearer ${tokenVal}"\n }\n }`
: `"${endpointUrl}"`;

return ` async function loadData() {
const root = document.getElementById('root');
const err = document.getElementById('error');
root.innerHTML = '<p class="loading">Loading…</p>';
try {
const res = await fetch("${endpointUrl}");
const res = await fetch(${fetchArgs});
if (!res.ok) throw new Error('HTTP ' + res.status);
const json = await res.json();
const items = Array.isArray(json) ? json : [json];
Expand Down
41 changes: 22 additions & 19 deletions src/lib/component-templates/comment-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const commentItemTemplate: ComponentTemplate = {
"$faker.date.past",
"$faker.string.uuid",
],
code: (fields, endpointUrl) => {
code: (fields, endpointUrl, options) => {
const idField =
findFieldByDataType(fields, ["string.uuid", "string.nanoid", "number.int"]) ??
findField(fields, ["id", "commentid", "uuid"]) ??
Expand All @@ -38,18 +38,16 @@ export const commentItemTemplate: ComponentTemplate = {
"person.firstName",
"internet.username",
]) ??
findField(fields, ["author", "fullname", "username", "name"]) ??
"author";
findField(fields, ["author", "fullname", "username", "name", "user"]) ??
fields.find((f) => f.fieldName !== idField)?.fieldName ??
fields[0]?.fieldName;
const commentField =
findFieldByDataType(fields, ["lorem.paragraph", "lorem.sentence", "lorem.sentences"]) ??
findField(fields, ["comment", "message", "content", "text"]) ??
"comment";
findField(fields, ["comment", "message", "content", "text", "body", "description"]) ??
fields.find((f) => f.fieldName !== idField && f.fieldName !== authorField)?.fieldName;
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 = fields.map((f) => {
const name = f.fieldName.includes(".") ? `"${f.fieldName}"?` : f.fieldName;
Expand All @@ -60,14 +58,14 @@ export const commentItemTemplate: ComponentTemplate = {

${buildInterface("Comment", interfaceLines)}

${buildFetchHook("CommentItem", endpointUrl, "Comment")}
${buildFetchHook("CommentItem", endpointUrl, "Comment", options)}

export function CommentList() {
const { data, loading, error } = useCommentItemData();

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;
if (!data) return <></>;

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

Expand All @@ -78,29 +76,34 @@ export function CommentList() {
${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>
${commentField ? ` <p style={{ marginTop: "0.25rem", marginBottom: 0, fontSize: "0.875rem", color: "#52525b", lineHeight: 1.5 }}>{comment.${commentField}}</p>\n` : ""} </div>
</div>
))}
</div>
);
}
`;
},
htmlCode: (fields, endpointUrl) => {
htmlCode: (fields, endpointUrl, options) => {
const idField =
findFieldByDataType(fields, ["string.uuid", "string.nanoid", "number.int"]) ??
findField(fields, ["id", "commentid", "uuid"]) ??
fields[0]?.fieldName ??
"id";
const authorField =
findFieldByDataType(fields, [
"person.fullName",
"person.firstName",
"internet.username",
]) ??
findField(fields, ["author", "fullname", "username", "name"]) ??
"author";
findField(fields, ["author", "fullname", "username", "name", "user"]) ??
fields.find((f) => f.fieldName !== idField)?.fieldName ??
fields[0]?.fieldName;
const commentField =
findFieldByDataType(fields, ["lorem.paragraph", "lorem.sentence", "lorem.sentences"]) ??
findField(fields, ["comment", "message", "content", "text"]) ??
"comment";
findField(fields, ["comment", "message", "content", "text", "body", "description"]) ??
fields.find((f) => f.fieldName !== idField && f.fieldName !== authorField)?.fieldName;
const avatarField =
findFieldByDataType(fields, ["image.avatar", "image.url", "image.datauri"]) ??
findField(fields, ["avatar", "image", "photo"]);
Expand Down Expand Up @@ -141,7 +144,7 @@ ${buildHtmlStyles()}
<p id="error"></p>
<div id="root" class="comment-list"></div>
<script type="module">
${buildHtmlFetchScript(endpointUrl, renderFn)}
${buildHtmlFetchScript(endpointUrl, renderFn, options)}
</script>
</body>
</html>
Expand Down
10 changes: 5 additions & 5 deletions src/lib/component-templates/dynamic-grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const dynamicGridTemplate: ComponentTemplate = {
description: "Adaptive UI card generated directly from schema fields and Faker types.",
requiredFields: [],
optionalFields: [],
code: (fields, endpointUrl) => {
code: (fields, endpointUrl, options) => {
const validFields = fields.filter((f) => f.fieldName.trim().length > 0);

const idField =
Expand Down Expand Up @@ -165,14 +165,14 @@ export const dynamicGridTemplate: ComponentTemplate = {

${buildInterface("DataItem", interfaceLines)}

${buildFetchHook("DataItem", endpointUrl, "DataItem")}
${buildFetchHook("DataItem", endpointUrl, "DataItem", options)}

export function DataCardList() {
const { data, loading, error } = useDataItemData();

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;
if (!data) return <></>;

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

Expand Down Expand Up @@ -235,7 +235,7 @@ ${detailFields
}
`;
},
htmlCode: (fields, endpointUrl) => {
htmlCode: (fields, endpointUrl, options) => {
const validFields = fields.filter((f) => f.fieldName.trim().length > 0);

const idField =
Expand Down Expand Up @@ -367,7 +367,7 @@ ${buildHtmlStyles()}
<p id="error"></p>
<div id="root" class="grid"></div>
<script type="module">
${buildHtmlFetchScript(endpointUrl, renderFn)}
${buildHtmlFetchScript(endpointUrl, renderFn, options)}
</script>
</body>
</html>
Expand Down
14 changes: 11 additions & 3 deletions src/lib/component-templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ export function matchTemplates(fields: SchemaField[], endpointPath: string = "")

// Endpoint path keyword bonus
let pathScoreBoost = 0;
const templateKeywords = [template.id, ...template.requiredFields];
const templateKeywords = [
template.id,
...template.id.split("-"),
...template.requiredFields,
];
if (normalizedPath) {
for (const kw of templateKeywords) {
const normKw = normalize(kw);
Expand Down Expand Up @@ -117,20 +121,24 @@ export function getTemplateById(id: string): ComponentTemplate | undefined {
return COMPONENT_TEMPLATES.find((t) => t.id === id);
}

import type { TemplateOptions } from "./types";

export function generateCode(
template: ComponentTemplate,
fields: SchemaField[],
endpointUrl: string,
options?: TemplateOptions,
): string {
return template.code(fields, endpointUrl);
return template.code(fields, endpointUrl, options);
}

export function generateHtmlCode(
template: ComponentTemplate,
fields: SchemaField[],
endpointUrl: string,
options?: TemplateOptions,
): string {
return template.htmlCode ? template.htmlCode(fields, endpointUrl) : "";
return template.htmlCode ? template.htmlCode(fields, endpointUrl, options) : "";
}

export * from "./types";
76 changes: 51 additions & 25 deletions src/lib/component-templates/post-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,31 @@ export const postCardTemplate: ComponentTemplate = {
"$faker.date.recent",
"$faker.string.uuid",
],
code: (fields, endpointUrl) => {
code: (fields, endpointUrl, options) => {
const idField =
findFieldByDataType(fields, ["string.uuid", "string.nanoid", "number.int"]) ??
findField(fields, ["id", "postid", "uuid"]) ??
fields[0]?.fieldName ??
"id";
const titleField =
findFieldByDataType(fields, ["book.title", "lorem.sentence", "company.catchPhrase"]) ??
findField(fields, ["title"]) ??
"title";
findField(fields, ["title", "headline", "subject", "name"]);
const contentField =
findFieldByDataType(fields, ["lorem.paragraph", "lorem.paragraphs", "person.bio"]) ??
findField(fields, ["content", "body", "paragraph"]) ??
"content";
findFieldByDataType(fields, [
"lorem.paragraph",
"lorem.paragraphs",
"person.bio",
"lorem.sentence",
]) ??
findField(fields, [
"content",
"body",
"paragraph",
"comment",
"text",
"message",
"description",
]);
const authorField =
findFieldByDataType(fields, [
"person.fullName",
Expand All @@ -51,8 +62,8 @@ export const postCardTemplate: ComponentTemplate = {
"book.author",
]) ?? findField(fields, ["author", "fullname", "username"]);
const coverImageField =
findFieldByDataType(fields, ["image.url", "image.datauri"]) ??
findField(fields, ["coverimage", "image", "thumbnail"]);
findFieldByDataType(fields, ["image.url", "image.datauri", "image.avatar"]) ??
findField(fields, ["coverimage", "image", "thumbnail", "avatar", "photo"]);
const publishedAtField =
findFieldByDataType(fields, [
"date.anytime",
Expand All @@ -70,25 +81,23 @@ export const postCardTemplate: ComponentTemplate = {

${buildInterface("Post", interfaceLines)}

${buildFetchHook("PostCard", endpointUrl, "Post")}
${buildFetchHook("PostCard", endpointUrl, "Post", options)}

export function PostCard() {
const { data, loading, error } = usePostCardData();

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;
if (!data) return <></>;

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

return (
<div style={{ display: "grid", gap: "1rem", gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))" }}>
{posts.map((post) => (
<article key={post.${idField}} style={{ overflow: "hidden", borderRadius: "12px", border: "1px solid #e4e4e7", backgroundColor: "#ffffff", boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" }}>
${coverImageField ? ` <img src={post.${coverImageField}} alt={post.${titleField}} style={{ height: "10rem", width: "100%", objectFit: "cover" }} />\n` : ""} <div style={{ padding: "1rem" }}>
<h3 style={{ margin: 0, fontWeight: 600, fontSize: "1rem", color: "#18181b" }}>{post.${titleField}}</h3>
<p style={{ marginTop: "0.25rem", marginBottom: 0, fontSize: "0.875rem", color: "#71717a", display: "-webkit-box", WebkitLineClamp: 3, WebkitBoxOrient: "vertical", overflow: "hidden" }}>{post.${contentField}}</p>
<div style={{ marginTop: "0.75rem", display: "flex", alignItems: "center", justifyContent: "space-between", fontSize: "0.75rem", color: "#a1a1aa" }}>
${coverImageField ? ` <img src={post.${coverImageField}} alt={${titleField ? `post.${titleField}` : `"Post image"`}} style={{ height: "10rem", width: "100%", objectFit: "cover" }} />\n` : ""} <div style={{ padding: "1rem" }}>
${titleField ? ` <h3 style={{ margin: 0, fontWeight: 600, fontSize: "1rem", color: "#18181b" }}>{post.${titleField}}</h3>\n` : ""}${contentField ? ` <p style={{ marginTop: ${titleField ? `"0.25rem"` : "0"}, marginBottom: 0, fontSize: "0.875rem", color: "#71717a", display: "-webkit-box", WebkitLineClamp: 3, WebkitBoxOrient: "vertical", overflow: "hidden" }}>{post.${contentField}}</p>\n` : ""} <div style={{ marginTop: "0.75rem", display: "flex", alignItems: "center", justifyContent: "space-between", fontSize: "0.75rem", color: "#a1a1aa" }}>
${authorField ? ` <span>{post.${authorField}}</span>\n` : ""}${publishedAtField ? ` <span>{post.${publishedAtField}}</span>\n` : ""} </div>
</div>
</article>
Expand All @@ -98,15 +107,26 @@ ${authorField ? ` <span>{post.${authorField}}</span>\n` : ""}${publ
}
`;
},
htmlCode: (fields, endpointUrl) => {
htmlCode: (fields, endpointUrl, options) => {
const titleField =
findFieldByDataType(fields, ["book.title", "lorem.sentence", "company.catchPhrase"]) ??
findField(fields, ["title"]) ??
"title";
findField(fields, ["title", "headline", "subject", "name"]);
const contentField =
findFieldByDataType(fields, ["lorem.paragraph", "lorem.paragraphs", "person.bio"]) ??
findField(fields, ["content", "body", "paragraph"]) ??
"content";
findFieldByDataType(fields, [
"lorem.paragraph",
"lorem.paragraphs",
"person.bio",
"lorem.sentence",
]) ??
findField(fields, [
"content",
"body",
"paragraph",
"comment",
"text",
"message",
"description",
]);
const authorField =
findFieldByDataType(fields, [
"person.fullName",
Expand All @@ -115,8 +135,8 @@ ${authorField ? ` <span>{post.${authorField}}</span>\n` : ""}${publ
"book.author",
]) ?? findField(fields, ["author", "fullname", "username"]);
const coverImageField =
findFieldByDataType(fields, ["image.url", "image.datauri"]) ??
findField(fields, ["coverimage", "image", "thumbnail"]);
findFieldByDataType(fields, ["image.url", "image.datauri", "image.avatar"]) ??
findField(fields, ["coverimage", "image", "thumbnail", "avatar", "photo"]);
const publishedAtField =
findFieldByDataType(fields, [
"date.anytime",
Expand All @@ -128,6 +148,12 @@ ${authorField ? ` <span>{post.${authorField}}</span>\n` : ""}${publ
const coverHtml = coverImageField
? `\${${safeGet(coverImageField)} ? \`<img class="cover" src="\${${safeGet(coverImageField)}}" alt="cover">\` : ''}`
: "";
const titleHtml = titleField
? `\${${safeGet(titleField)} ? \`<div class="article-title">\${${safeGet(titleField)}}</div>\` : ''}`
: "";
const contentHtml = contentField
? `\${${safeGet(contentField)} ? \`<div class="article-excerpt">\${${safeGet(contentField)}}</div>\` : ''}`
: "";
const authorHtml = authorField
? `\${${safeGet(authorField)} ? \`<span>\${${safeGet(authorField)}}</span>\` : ''}`
: "";
Expand All @@ -138,8 +164,8 @@ ${authorField ? ` <span>{post.${authorField}}</span>\n` : ""}${publ
const renderFn = `item => \`<article class="article">
${coverHtml}
<div class="article-body">
<div class="article-title">\${${safeGet(titleField)}}</div>
<div class="article-excerpt">\${${safeGet(contentField)}}</div>
${titleHtml}
${contentHtml}
<div class="article-footer">${authorHtml}${dateHtml}</div>
</div>
</article>\``;
Expand All @@ -159,7 +185,7 @@ ${buildHtmlStyles()}
<p id="error"></p>
<div id="root" class="grid"></div>
<script type="module">
${buildHtmlFetchScript(endpointUrl, renderFn)}
${buildHtmlFetchScript(endpointUrl, renderFn, options)}
</script>
</body>
</html>
Expand Down
Loading
Loading