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
168 changes: 124 additions & 44 deletions src/components/ComponentDetail/ButtonGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,37 @@
import { Plus } from "lucide-react";
import { Plus, Grid, MessageSquare, Reply } from "lucide-react";
import EditableButton from "./EditableButton";
import { GridItem } from "../../types/ComponentDetailForm";
import {
GridItem,
KeyboardType,
formValuesType,
} from "../../types/ComponentDetailForm";
import { generateUUID } from "./generateUUID";
import { componentSchemaType } from "./makeFormData";

type ButtonGridProps = {
rows: GridItem[][];
setRows: React.Dispatch<React.SetStateAction<GridItem[][]>>;
keyboardType: KeyboardType;
setKeyboardType: React.Dispatch<React.SetStateAction<KeyboardType>>;
formValues: formValuesType;
componentSchema: componentSchemaType;
componentName: string;
};

export default function ButtonGrid({ rows, setRows }: ButtonGridProps) {
const MAX_ROWS = 5;
const MAX_COLS = 4;
export default function ButtonGrid({
rows,
setRows,
keyboardType,
setKeyboardType,
}: ButtonGridProps) {
const MAX_ROWS = 3;
const MAX_COLS = 3;

const addItemToRow = (rowIndex: number) => {
setRows((prev) =>
prev.map((row, idx) =>
idx === rowIndex && row.length < MAX_COLS ?
[...row, { id: generateUUID(), label: "Item" }]
[...row, { id: generateUUID(), label: "New Button" }]
: row,
),
);
Expand All @@ -25,52 +40,117 @@ export default function ButtonGrid({ rows, setRows }: ButtonGridProps) {
const addRow = () => {
setRows((prev) => {
if (prev.length >= MAX_ROWS) return prev;
const lastRow = prev[prev.length - 1];
if (lastRow.length === 0) return prev;
return [...prev, [{ id: generateUUID(), label: "Item" }]];
return [...prev, [{ id: generateUUID(), label: "New Button" }]];
});
};

return (
<div className="mt-7 mb-10 flex flex-col gap-3 text-primary-content">
{rows.map((row, rowIndex) => (
<div key={rowIndex} className="relative flex flex-wrap gap-3">
{row.map((item, itemIndex) => {
return (
<div key={item.id} className="flex shrink grow items-center">
<div className="group card relative h-15 flex-1 bg-primary hover:bg-patina-400">
<EditableButton
item={item}
itemIndex={itemIndex}
rowIndex={rowIndex}
setRows={setRows}
/>
<div className="mt-4 flex flex-col gap-4 text-primary-content">
{rows.length === 0 || rows.every((row) => row.length === 0) ?
<div className="flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-primary/20 bg-base-200/50 p-8 transition-all duration-200 hover:border-primary/40 hover:bg-base-200">
<Grid className="mb-3 size-12 text-primary/40" />
<h3 className="mb-2 text-lg font-semibold text-base-content">
No Buttons Yet
</h3>
<p className="mb-4 text-center text-sm text-base-content/70">
Start by adding your first button to create an interactive grid
</p>
<button
type="button"
onClick={addRow}
className="btn btn-lg btn-primary"
>
<Plus className="mr-2 size-5" />
Add First Button
</button>
</div>
: <div className="rounded-xl border-2 border-dashed border-primary/20 bg-base-200/50 p-6 transition-all duration-200 hover:border-primary/40 hover:bg-base-200">
{/* Header with Type Toggle */}
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-4">
<span className="text-sm font-medium text-base-content/70">
Keyboard Type:
</span>

{/* Segmented Control */}
<div className="join">
<button
type="button"
onClick={() => setKeyboardType("inline")}
className={`btn join-item btn-xs ${
keyboardType === "inline" ? "btn-primary" : (
"text-base-content/60 btn-ghost hover:text-base-content"
)
}`}
>
<MessageSquare className="mr-1 size-3" />
Inline
</button>
<button
type="button"
onClick={() => setKeyboardType("reply")}
className={`btn join-item btn-xs ${
keyboardType === "reply" ? "btn-primary" : (
"text-base-content/60 btn-ghost hover:text-base-content"
)
}`}
>
<Reply className="mr-1 size-3" />
Reply
</button>
</div>
</div>
</div>

<div className="space-y-4">
{rows.map((row, rowIndex) => (
<div key={rowIndex} className="relative flex gap-4">
<div className="flex flex-1 gap-4">
{row.map((item, itemIndex) => (
<div key={item.id} className="min-w-0 flex-1">
<div className="group card relative h-12 overflow-hidden rounded-lg border border-gray-200 shadow-sm transition-all duration-200 hover:border-primary/30 hover:bg-primary/5 hover:shadow-md">
<EditableButton
item={item}
itemIndex={itemIndex}
rowIndex={rowIndex}
setRows={setRows}
/>
</div>
</div>
))}
</div>

<button
type="button"
onClick={() => addItemToRow(rowIndex)}
disabled={row.length >= MAX_COLS}
className={`btn my-auto h-12 w-12 shrink-0 grow-0 rounded-lg border-2 border-dashed btn-soft btn-secondary ${
row.length >= MAX_COLS ?
"cursor-not-allowed border-gray-200 text-gray-400 opacity-50"
: "border-gray-300 hover:border-primary/50"
}`}
title={
row.length >= MAX_COLS ? "Row is full" : "Add Button to Row"
}
>
<Plus size={18} />
</button>
</div>
);
})}
))}

{row.length < MAX_COLS && (
<button
type="button"
onClick={() => addItemToRow(rowIndex)}
className="btn my-auto h-15 shrink-0 grow-0 btn-soft btn-secondary"
>
<Plus size={20} />
</button>
)}
{rows.length < MAX_ROWS && (
<button
type="button"
onClick={addRow}
className="btn mt-2 btn-block rounded-xl btn-soft btn-secondary"
>
<Plus className="mr-2 size-5" />
Add New Row
</button>
)}
</div>
</div>
))}

{rows.length < MAX_ROWS && rows[rows.length - 1].length > 0 && (
<button
type="button"
onClick={addRow}
className="btn mt-2 btn-soft btn-secondary"
>
<Plus />
</button>
)}
}
</div>
);
}
62 changes: 26 additions & 36 deletions src/components/ComponentDetail/EditableButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { X } from "lucide-react";
import { X, Pencil } from "lucide-react";
import { useState } from "react";
import { GridItem } from "../../types/ComponentDetailForm";

Expand All @@ -18,82 +18,72 @@ export default function EditableButton({
const [editingItemId, setEditingItemId] = useState<string | null>(null);
const [editingLabel, setEditingLabel] = useState("");

function trimRows(newRows: GridItem[][], rowIndex: number): GridItem[][] {
if (
newRows[rowIndex].length === 0 &&
rowIndex + 1 < newRows.length &&
newRows[rowIndex + 1].length > 0
) {
newRows[rowIndex].push(newRows[rowIndex + 1].shift()!);
} else {
return newRows;
}

return trimRows(newRows, rowIndex + 1);
}

const saveEdit = (itemId: string) => {
setRows((prev) =>
prev.map((row) =>
const saveEdit = (id: string) => {
setRows((prev) => {
const newRows = prev.map((row) =>
row.map((item) =>
item.id === itemId ? { ...item, label: editingLabel } : item,
item.id === id ? { ...item, label: editingLabel } : item,
),
),
);
);
return newRows;
});
setEditingItemId(null);
setEditingLabel("");
};

const removeItem = (rowIndex: number, itemIndex: number) => {
setRows((prev) => {
let newRows = prev.map((row) => [...row]);
newRows[rowIndex].splice(itemIndex, 1);

newRows = trimRows(newRows, rowIndex);
const newRows = [...prev];
newRows[rowIndex] = newRows[rowIndex].filter(
(_, idx) => idx !== itemIndex,
);

if (newRows[newRows.length - 1].length === 0 && newRows.length > 1) {
newRows.pop();
// Remove empty rows
if (newRows[rowIndex].length === 0) {
newRows.splice(rowIndex, 1);
}

return newRows;
});
};

return (
<>
<div className="relative flex h-full w-full items-center justify-center p-3">
{editingItemId === item.id ?
<input
autoFocus
type="text"
value={editingLabel}
onChange={(e) =>
setEditingLabel(
e.target.value.length > 0 ? e.target.value : "default",
e.target.value.length > 0 ? e.target.value : "New Button",
)
}
onBlur={() => saveEdit(item.id)}
onKeyDown={(e) => {
if (e.key === "Enter") saveEdit(item.id);
}}
className="mx-auto my-auto text-center outline-none input-primary"
className="w-full rounded-lg border border-white/20 bg-white/10 px-4 py-2 text-center text-white placeholder-white/50 backdrop-blur-sm transition-all duration-200 outline-none focus:border-white/40 focus:ring-2 focus:ring-white/20"
placeholder="Enter button text..."
/>
: <div
className="mx-auto my-auto cursor-pointer text-center"
className="group/edit flex h-full w-full cursor-pointer items-center justify-center rounded-lg transition-all duration-200 hover:bg-white/10"
onClick={() => {
setEditingItemId(item.id);
setEditingLabel(item.label);
}}
>
{item.label}
<span className="text-lg font-medium text-white">{item.label}</span>
<Pencil className="ml-2 hidden size-4 text-white/50 transition-all duration-200 group-hover/edit:block" />
</div>
}

<button
className="invisible absolute top-0.5 right-0.5 cursor-pointer rounded-full bg-red-500 opacity-0 transition-opacity group-hover:visible group-hover:opacity-100 hover:bg-red-300"
className="absolute -top-2 -right-2 hidden rounded-full bg-error p-1.5 text-white opacity-0 shadow-lg transition-all duration-200 group-hover:block group-hover:opacity-100 hover:bg-error/80"
onClick={() => removeItem(rowIndex, itemIndex)}
title="Remove Button"
>
<X size={18} />
<X className="size-4" />
</button>
</>
</div>
);
}
38 changes: 20 additions & 18 deletions src/components/ComponentDetail/FormFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,24 +184,26 @@ export default function FormFields({
})}

{canCollapse && (
<button
type="button"
onClick={() => setIsCollapsed(!isCollapsed)}
className="group btn w-full btn-ghost transition-all duration-300 btn-accent"
>
<span className="flex items-center justify-center gap-2">
{isCollapsed ?
<>
<span>Show Optional Fields</span>
<ChevronDown className="size-4 transition-transform duration-300 group-hover:translate-y-0.5" />
</>
: <>
<span>Hide Optional Fields</span>
<ChevronUp className="size-4 transition-transform duration-300 group-hover:-translate-y-0.5" />
</>
}
</span>
</button>
<>
<button
type="button"
onClick={() => setIsCollapsed(!isCollapsed)}
className="group btn w-full btn-ghost transition-all duration-300 btn-accent"
>
<span className="flex items-center justify-center gap-2">
{isCollapsed ?
<>
<span>Show Optional Fields</span>
<ChevronDown className="size-4 transition-transform duration-300 group-hover:translate-y-0.5" />
</>
: <>
<span>Hide Optional Fields</span>
<ChevronUp className="size-4 transition-transform duration-300 group-hover:-translate-y-0.5" />
</>
}
</span>
</button>
</>
)}
</div>
);
Expand Down
Loading
Loading