Added template creator (doesn't work without db)

This commit is contained in:
2026-03-10 19:15:03 -05:00
parent 5a8b20a827
commit 39c5021e2f
10 changed files with 322 additions and 8 deletions
@@ -0,0 +1,49 @@
import type { Component } from 'solid-js';
import { For } from 'solid-js';
import { Plus } from 'lucide-solid';
import type { TemplateItem } from '../../types';
import TemplateItemRow from './TemplateItemRow';
interface TemplateItemListProps {
items: TemplateItem[];
onUpdateItem: (id: string, updates: Partial<TemplateItem>) => void;
onDeleteItem: (id: string) => void;
onAddItem: () => void;
}
const TemplateItemList: Component<TemplateItemListProps> = (props) => {
return (
<div class="mt-8 space-y-4">
<div class="flex items-center justify-between pb-2 border-b border-border/60">
<h3 class="text-lg font-semibold text-foreground/90">Template Items</h3>
<button
onClick={props.onAddItem}
class="flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors shadow-sm"
>
<Plus class="w-4 h-4" />
Add Item
</button>
</div>
<div class="space-y-2">
<For each={props.items}>
{(item) => (
<TemplateItemRow
item={item}
onUpdate={props.onUpdateItem}
onDelete={props.onDeleteItem}
/>
)}
</For>
{props.items.length === 0 && (
<div class="text-center py-12 text-muted-foreground/60 border-2 border-dashed border-border/60 rounded-xl bg-card">
<p>No items added yet. Click "Add Item" to start building this template.</p>
</div>
)}
</div>
</div>
);
};
export default TemplateItemList;
@@ -0,0 +1,97 @@
import type { Component } from 'solid-js';
import { createSignal, createEffect } from 'solid-js';
import { Trash2, GripVertical } from 'lucide-solid';
import type { TemplateItem } from '../../types';
interface TemplateItemRowProps {
item: TemplateItem;
onUpdate: (id: string, updates: Partial<TemplateItem>) => void;
onDelete: (id: string) => void;
}
const TemplateItemRow: Component<TemplateItemRowProps> = (props) => {
let descriptionRef: HTMLTextAreaElement | undefined;
const [isEditing, setIsEditing] = createSignal(false);
const [localDescription, setLocalDescription] = createSignal(props.item.description);
createEffect(() => {
if (!isEditing()) {
setLocalDescription(props.item.description);
}
});
const handleBlur = () => {
const currentDesc = localDescription();
if (currentDesc !== props.item.description) {
props.onUpdate(props.item.id, { description: currentDesc });
}
setIsEditing(false);
};
return (
<div class={`group flex items-start gap-4 p-3 border transition-all animate-in fade-in slide-in-from-left-2 rounded-2xl bg-card border-border hover:shadow-premium-hover hover:border-border/80 ${isEditing() ? 'ring-1 ring-primary/20 shadow-premium border-primary/30' : ''}`}>
<div class="cursor-grab transition-colors shrink-0 w-4 mt-2 p-1 -m-1 rounded hover:bg-muted text-muted-foreground/30 hover:text-muted-foreground/60" title="Drag to reorder (Coming soon)">
<GripVertical class="w-4 h-4" />
</div>
<div class="flex-1 relative">
<textarea
ref={descriptionRef}
value={localDescription()}
onFocus={() => setIsEditing(true)}
onBlur={handleBlur}
onInput={(e) => {
setLocalDescription(e.currentTarget.value);
if (!('fieldSizing' in document.documentElement.style)) {
e.currentTarget.style.height = 'auto';
e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px';
}
}}
rows="1"
class="w-full bg-transparent border-b border-transparent hover:border-border/40 focus:border-primary outline-none py-1 transition-all resize-none block overflow-hidden align-top leading-normal text-base font-medium text-foreground/90"
style={{ "field-sizing": "content", "min-height": "1.5em" }}
placeholder="Item description..."
/>
</div>
<div class="w-24 shrink-0 mt-1">
<input
type="number"
value={props.item.quantity}
onInput={(e) => props.onUpdate(props.item.id, { quantity: parseFloat(e.currentTarget.value) || 0 })}
class="w-full bg-muted/30 border border-border/60 rounded-xl px-2 py-1.5 text-xs text-right focus:bg-background focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none font-bold text-foreground/80 transition-all"
placeholder="Qty"
/>
</div>
<div class="w-32 shrink-0 mt-1">
<div class="relative">
<span class="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground/50 text-[10px]">$</span>
<input
type="number"
step="0.1"
value={props.item.unitPrice}
onInput={(e) => props.onUpdate(props.item.id, { unitPrice: parseFloat(e.currentTarget.value) || 0 })}
class="w-full bg-muted/30 border border-border/60 rounded-xl pl-5 pr-2 py-1.5 text-xs text-right focus:bg-background focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none font-bold text-foreground/80 transition-all"
placeholder="Price"
/>
</div>
</div>
<div class="w-10 shrink-0 flex items-center justify-end gap-1.5 mt-1.5 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => props.onDelete(props.item.id)}
class="text-muted-foreground/40 hover:text-destructive transition-colors"
title="Delete Item"
>
<div class="p-1 px-1.5 bg-muted rounded-lg border border-border/40 hover:bg-destructive/10">
<Trash2 class="w-3 h-3" />
</div>
</button>
</div>
</div>
);
};
export default TemplateItemRow;