Skip to content

Commit 83d45b8

Browse files
a lot (#199)
1 parent d28934e commit 83d45b8

File tree

8 files changed

+299
-220
lines changed

8 files changed

+299
-220
lines changed

frontend/app/expense-tracker/expense-table/category-options.tsx

Lines changed: 144 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,16 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
22
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command";
33
import { X } from "lucide-react";
44
import { useState } from "react";
5-
import { Input } from "@/components/ui/input";
5+
import React from "react";
6+
7+
const CATEGORY_MAX_CHARS = 20;
68

79
interface CategoryLabelProps {
810
category: string;
911
updateCategory?: (category: string, lineItems: string[], removeCategory: boolean) => void;
1012
lineItemIds: string[];
1113
editableTags: boolean;
12-
}
13-
14-
interface CategoryBadgeProps extends CategoryLabelProps {
15-
allCategories: string[];
14+
allCategories?: string[];
1615
}
1716

1817
interface CreateCategoryProps {
@@ -22,7 +21,45 @@ interface CreateCategoryProps {
2221
lineItemIds: string[];
2322
}
2423

25-
export default function CategoryLabel({ category, updateCategory, lineItemIds, editableTags }: CategoryLabelProps) {
24+
interface CategoryBadgeSpanProps {
25+
category: string;
26+
variant?: "default" | "flex";
27+
clickable?: boolean;
28+
children?: React.ReactNode;
29+
}
30+
31+
export const CategoryBadgeSpan = React.forwardRef<HTMLSpanElement, CategoryBadgeSpanProps>(
32+
({ category, variant = "default", clickable = false, children, ...props }, ref) => {
33+
const baseClasses = "px-2 py-1 rounded text-xs font-bold h-6 select-none";
34+
const variantClasses = variant === "flex" ? "flex items-center gap-1 flex-shrink-0" : "inline-block";
35+
const clickableClass = clickable ? "cursor-pointer" : "";
36+
37+
const displayText =
38+
category.length > CATEGORY_MAX_CHARS ? `${category.substring(0, CATEGORY_MAX_CHARS)}...` : category;
39+
40+
return (
41+
<span
42+
ref={ref}
43+
{...props}
44+
className={`${baseClasses} ${variantClasses} ${clickableClass} text-black`}
45+
style={{ backgroundColor: getTagColor(category).backgroundColor }}
46+
>
47+
{displayText}
48+
{children}
49+
</span>
50+
);
51+
}
52+
);
53+
54+
CategoryBadgeSpan.displayName = "CategoryBadgeSpan";
55+
56+
export default function CategoryLabel({
57+
category,
58+
updateCategory,
59+
lineItemIds,
60+
editableTags,
61+
allCategories,
62+
}: CategoryLabelProps) {
2663
const categories = category.length > 0 ? category.split(",") : [];
2764

2865
if (editableTags && updateCategory && categories.length === 0) {
@@ -43,7 +80,7 @@ export default function CategoryLabel({ category, updateCategory, lineItemIds, e
4380
<CategoryBadge
4481
key={index}
4582
category={cat}
46-
allCategories={categories}
83+
allCategories={allCategories}
4784
updateCategory={updateCategory}
4885
lineItemIds={lineItemIds}
4986
editableTags={editableTags}
@@ -60,27 +97,14 @@ export default function CategoryLabel({ category, updateCategory, lineItemIds, e
6097
<PopoverTrigger asChild>
6198
<button className="text-gray-400 text-sm hover:text-gray-600 underline">+ Add category</button>
6299
</PopoverTrigger>
63-
<PopoverContent className="w-[200px] p-4">
64-
<div className="space-y-2">
65-
<Input
66-
type="text"
67-
placeholder="Enter category name..."
68-
value={searchValue}
69-
onChange={(e) => setSearchValue(e.target.value)}
70-
maxLength={100}
71-
inputMode="text"
72-
className="h-8 w-full rounded-full bg-muted text-black text-sm border border-border/40 px-3
73-
focus-visible:ring-1 focus-visible:ring-ring/40
74-
placeholder:text-xs placeholder:text-black
75-
[&::placeholder]:text-xs [&::placeholder]:text-black"
76-
/>
77-
<Create
78-
searchValue={searchValue}
79-
setSearchValue={setSearchValue}
80-
updateCategory={updateCategory!}
81-
lineItemIds={lineItemIds}
82-
/>
83-
</div>
100+
<PopoverContent className="w-64 p-0">
101+
<CategoryCommand
102+
searchValue={searchValue}
103+
setSearchValue={setSearchValue}
104+
updateCategory={updateCategory!}
105+
lineItemIds={lineItemIds}
106+
allCategories={allCategories ?? []}
107+
/>
84108
</PopoverContent>
85109
</Popover>
86110
);
@@ -93,108 +117,119 @@ export function CategoryBadge({
93117
updateCategory,
94118
lineItemIds,
95119
editableTags,
96-
}: CategoryBadgeProps) {
120+
}: CategoryLabelProps) {
97121
const [searchValue, setSearchValue] = useState("");
98-
const displayCategory = category.length > 20 ? `${category.substring(0, 20)}...` : category;
99122

100-
if (!editableTags || !updateCategory) {
101-
return (
102-
<span
103-
className="px-[8px] py-[4px] rounded-[4px] text-[12px] font-bold h-[24px] inline-block"
104-
style={{ backgroundColor: getTagColor(category).backgroundColor }}
105-
>
106-
{displayCategory}
107-
</span>
108-
);
123+
if (!editableTags || !updateCategory || !allCategories) {
124+
return <CategoryBadgeSpan category={category} />;
109125
}
110126

111127
return (
112128
<Popover>
113129
<PopoverTrigger asChild>
114-
<span
115-
className="px-[8px] py-[4px] rounded-[4px] text-[12px] h-[24px] font-bold cursor-pointer inline-block"
116-
style={{ backgroundColor: getTagColor(category).backgroundColor }}
117-
>
118-
{displayCategory}
119-
</span>
130+
<CategoryBadgeSpan category={category} clickable={true} />
120131
</PopoverTrigger>
121-
<PopoverContent className="w-[200px] p-0">
122-
<Command>
123-
<div className="flex items-center gap-2 border-b bg-muted/60 px-2 py-0" cmdk-input-wrapper="">
124-
<span
125-
className="flex items-center gap-1 px-[8px] py-[4px] h-[24px] rounded-[4px] text-[12px] font-bold text-black flex-shrink-0"
126-
style={getTagColor(category)}
127-
>
128-
{category}
132+
<PopoverContent className="w-64 p-0">
133+
<CategoryCommand
134+
searchValue={searchValue}
135+
setSearchValue={setSearchValue}
136+
updateCategory={updateCategory}
137+
lineItemIds={lineItemIds}
138+
allCategories={allCategories}
139+
headerContent={
140+
<CategoryBadgeSpan category={category} variant="flex">
129141
<button
130142
onClick={(e) => {
131143
e.stopPropagation();
132144
updateCategory(category, lineItemIds, true);
133145
}}
134-
className="hover:bg-gray-100 hover:bg-opacity-20 rounded-full p-0.5"
146+
className="hover:bg-gray-100 hover:bg-opacity-20 rounded-full p-0.5 ml-1"
135147
>
136-
<X className="h-3 w-3" />
148+
<X className="h-4 w-4" strokeWidth={1.5} />
137149
</button>
138-
</span>
139-
<div
140-
className="flex-1
141-
[&_[data-slot=command-input-wrapper]]:border-0
142-
[&_[data-slot=command-input-wrapper]]:px-0
143-
[&_[data-slot=command-input-wrapper]]:h-auto
144-
[&_svg]:hidden"
145-
>
146-
<CommandInput
147-
value={searchValue}
148-
onValueChange={(value) => {
149-
if (value.length <= 100) {
150-
setSearchValue(value);
151-
}
152-
}}
153-
className="overflow-hidden border-0 px-0 flex-1"
154-
inputMode="text"
155-
/>
156-
</div>
157-
</div>
158-
<div className="px-3 py-0.5 text-xs text-muted-foreground border-b">
159-
Select an option or create one
160-
</div>
161-
<CommandEmpty>
162-
<Create
163-
searchValue={searchValue}
164-
setSearchValue={setSearchValue}
165-
updateCategory={updateCategory}
166-
lineItemIds={lineItemIds}
167-
/>
168-
</CommandEmpty>
169-
<CommandGroup>
170-
{allCategories.map((cat) => (
171-
<CommandItem
172-
key={cat}
173-
onSelect={() => updateCategory(cat, lineItemIds, false)}
174-
className="flex items-center gap-2"
175-
>
176-
<span
177-
className="px-[8px] py-[4px] h-[24px] rounded-[4px] text-[12px] font-bold text-black"
178-
style={getTagColor(cat)}
179-
>
180-
{cat}
181-
</span>
182-
</CommandItem>
183-
))}
184-
</CommandGroup>
185-
</Command>
150+
</CategoryBadgeSpan>
151+
}
152+
/>
186153
</PopoverContent>
187154
</Popover>
188155
);
189156
}
190157

158+
interface CategoryCommandProps {
159+
searchValue: string;
160+
setSearchValue: (value: string) => void;
161+
updateCategory: (category: string, lineItems: string[], removeCategory: boolean) => void;
162+
lineItemIds: string[];
163+
allCategories: string[];
164+
headerContent?: React.ReactNode;
165+
}
166+
167+
function CategoryCommand({
168+
searchValue,
169+
setSearchValue,
170+
updateCategory,
171+
lineItemIds,
172+
allCategories,
173+
headerContent,
174+
}: CategoryCommandProps) {
175+
return (
176+
<Command>
177+
<div className="flex items-center gap-2 border-b bg-muted/60 px-2 py-0" cmdk-input-wrapper="">
178+
{headerContent}
179+
<div
180+
className="flex-1 [&_[data-slot=command-input-wrapper]]:border-0
181+
[&_[data-slot=command-input-wrapper]]:px-0 [&_[data-slot=command-input-wrapper]]:h-auto [&_svg]:hidden"
182+
>
183+
<CommandInput
184+
value={searchValue}
185+
onValueChange={(value) => {
186+
if (value.length <= 100) {
187+
setSearchValue(value);
188+
}
189+
}}
190+
className="overflow-hidden border-0 px-0 flex-1"
191+
inputMode="text"
192+
/>
193+
</div>
194+
</div>
195+
<div className="px-3 py-2 text-sm text-black">Select an option or create one</div>
196+
<CommandEmpty className="py-0 pb-0.5">
197+
<Create
198+
searchValue={searchValue}
199+
setSearchValue={setSearchValue}
200+
updateCategory={updateCategory}
201+
lineItemIds={lineItemIds}
202+
/>
203+
</CommandEmpty>
204+
<CommandGroup>
205+
{allCategories.map((cat) => (
206+
<CommandItem
207+
key={cat}
208+
onSelect={() => updateCategory(cat, lineItemIds, false)}
209+
className="flex items-center gap-2"
210+
>
211+
<CategoryBadgeSpan category={cat} />
212+
</CommandItem>
213+
))}
214+
</CommandGroup>
215+
</Command>
216+
);
217+
}
218+
191219
function Create({ searchValue, updateCategory, lineItemIds, setSearchValue }: CreateCategoryProps) {
192-
const displayText = searchValue.length > 15 ? `${searchValue.substring(0, 15)}...` : searchValue;
220+
const previewColor = {
221+
backgroundColor: "hsl(0, 0%, 85%)",
222+
color: "#000000",
223+
};
224+
225+
const displayText =
226+
searchValue.length > CATEGORY_MAX_CHARS ? `${searchValue.substring(0, CATEGORY_MAX_CHARS)}...` : searchValue;
193227

194228
return (
195229
<button
196230
type="button"
197-
className="relative mt-1 flex cursor-pointer select-none items-center rounded-sm px-2 py-0.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground"
231+
className="relative w-full flex cursor-pointer select-none items-center gap-2 rounded-sm px-3 py-1.5 text-sm
232+
outline-none hover:bg-accent hover:text-accent-foreground"
198233
onClick={() => {
199234
const name = searchValue.trim();
200235
if (name) {
@@ -203,7 +238,10 @@ function Create({ searchValue, updateCategory, lineItemIds, setSearchValue }: Cr
203238
}
204239
}}
205240
>
206-
Create {displayText}
241+
<span className="text-sm text-black">Create</span>
242+
<span className="px-2 py-1 rounded text-xs font-bold h-6 inline-block text-black" style={previewColor}>
243+
{displayText}
244+
</span>
207245
</button>
208246
);
209247
}

0 commit comments

Comments
 (0)