-
Notifications
You must be signed in to change notification settings - Fork 795
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1444 from mfts/marc/pm-13-add-new-fields-to-acces…
…s-screen feat: add custom fields to links
- Loading branch information
Showing
34 changed files
with
1,196 additions
and
271 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
169 changes: 169 additions & 0 deletions
169
components/links/link-sheet/custom-fields-panel/custom-field.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
import { useEffect, useState } from "react"; | ||
|
||
import { ChevronDown, ChevronUp, Trash2 } from "lucide-react"; | ||
|
||
import { Button } from "@/components/ui/button"; | ||
import { Input } from "@/components/ui/input"; | ||
import { Label } from "@/components/ui/label"; | ||
import { | ||
Select, | ||
SelectContent, | ||
SelectItem, | ||
SelectTrigger, | ||
SelectValue, | ||
} from "@/components/ui/select"; | ||
import { Switch } from "@/components/ui/switch"; | ||
|
||
import { type CustomFieldData } from "."; | ||
|
||
interface CustomFieldProps { | ||
field: CustomFieldData; | ||
onUpdate: (field: CustomFieldData) => void; | ||
onDelete: () => void; | ||
onMoveUp?: () => void; | ||
onMoveDown?: () => void; | ||
isFirst?: boolean; | ||
isLast?: boolean; | ||
} | ||
|
||
export default function CustomField({ | ||
field, | ||
onUpdate, | ||
onDelete, | ||
onMoveUp, | ||
onMoveDown, | ||
isFirst, | ||
isLast, | ||
}: CustomFieldProps) { | ||
const [localField, setLocalField] = useState<CustomFieldData>(field); | ||
|
||
useEffect(() => { | ||
onUpdate(localField); | ||
}, [localField, onUpdate]); | ||
|
||
const handleInputChange = ( | ||
e: React.ChangeEvent<HTMLInputElement>, | ||
key: keyof CustomFieldData, | ||
) => { | ||
if (key === "label") { | ||
setLocalField((prev) => ({ | ||
...prev, | ||
[key]: e.target.value, | ||
identifier: e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, "-"), | ||
})); | ||
} else { | ||
setLocalField((prev) => ({ | ||
...prev, | ||
[key]: e.target.value, | ||
})); | ||
} | ||
}; | ||
|
||
const handleSelectChange = (value: string, key: keyof CustomFieldData) => { | ||
setLocalField((prev) => ({ | ||
...prev, | ||
[key]: value, | ||
})); | ||
}; | ||
|
||
const handleSwitchChange = (checked: boolean, key: keyof CustomFieldData) => { | ||
setLocalField((prev) => ({ | ||
...prev, | ||
[key]: checked, | ||
})); | ||
}; | ||
|
||
return ( | ||
<div className="group relative flex flex-col space-y-4 rounded-lg border border-border bg-card p-4"> | ||
<div className="absolute -right-2 -top-2 hidden group-hover:block"> | ||
<Button | ||
variant="ghost" | ||
size="icon" | ||
className="h-8 w-8 bg-background text-muted-foreground hover:bg-background hover:text-destructive" | ||
onClick={onDelete} | ||
> | ||
<Trash2 className="h-4 w-4" /> | ||
</Button> | ||
</div> | ||
|
||
<div className="grid gap-4"> | ||
<div className="grid gap-2"> | ||
<Label htmlFor="type">Input Type</Label> | ||
<Select | ||
value={localField.type} | ||
onValueChange={(value) => handleSelectChange(value, "type")} | ||
> | ||
<SelectTrigger> | ||
<SelectValue placeholder="Select field type" /> | ||
</SelectTrigger> | ||
<SelectContent> | ||
<SelectItem value="SHORT_TEXT">Short Text</SelectItem> | ||
<SelectItem value="LONG_TEXT">Long Text</SelectItem> | ||
<SelectItem value="NUMBER">Number</SelectItem> | ||
{/* <SelectItem value="PHONE_NUMBER">Phone</SelectItem> */} | ||
<SelectItem value="URL">URL</SelectItem> | ||
</SelectContent> | ||
</Select> | ||
</div> | ||
|
||
<div className="grid gap-2"> | ||
<Label htmlFor="label">Label</Label> | ||
<Input | ||
id="label" | ||
type="text" | ||
required | ||
value={localField.label} | ||
onChange={(e) => handleInputChange(e, "label")} | ||
placeholder="e.g., Company Name" | ||
/> | ||
</div> | ||
|
||
{/* <div className="grid gap-2"> | ||
<Label htmlFor="identifier">Identifier</Label> | ||
<Input | ||
id="identifier" | ||
value={localField.identifier} | ||
type="text" | ||
disabled | ||
onChange={(e) => { | ||
const value = e.target.value | ||
.toLowerCase() | ||
.replace(/[^a-z0-9-]/g, "-"); | ||
handleInputChange( | ||
{ ...e, target: { ...e.target, value } }, | ||
"identifier", | ||
); | ||
}} | ||
placeholder="e.g., company-name" | ||
/> | ||
</div> */} | ||
|
||
<div className="grid gap-2"> | ||
<Label htmlFor="placeholder">Placeholder</Label> | ||
<Input | ||
id="placeholder" | ||
type="text" | ||
value={localField.placeholder || ""} | ||
onChange={(e) => handleInputChange(e, "placeholder")} | ||
placeholder="e.g., Enter your company name" | ||
/> | ||
</div> | ||
|
||
<div className="flex items-center justify-between"> | ||
<div className="flex flex-col space-y-1"> | ||
<Label>Required Field</Label> | ||
<span className="text-sm text-muted-foreground"> | ||
Make this field mandatory | ||
</span> | ||
</div> | ||
<Switch | ||
checked={localField.required} | ||
onCheckedChange={(checked) => | ||
handleSwitchChange(checked, "required") | ||
} | ||
/> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} |
164 changes: 164 additions & 0 deletions
164
components/links/link-sheet/custom-fields-panel/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
import { CustomField, CustomFieldType } from "@prisma/client"; | ||
import { Plus } from "lucide-react"; | ||
import { toast } from "sonner"; | ||
|
||
import { Button } from "@/components/ui/button"; | ||
import { ScrollArea } from "@/components/ui/scroll-area"; | ||
import { Separator } from "@/components/ui/separator"; | ||
import { | ||
Sheet, | ||
SheetContent, | ||
SheetDescription, | ||
SheetHeader, | ||
SheetTitle, | ||
} from "@/components/ui/sheet"; | ||
|
||
import { usePlan } from "@/lib/swr/use-billing"; | ||
|
||
import CustomFieldComponent from "./custom-field"; | ||
|
||
export type CustomFieldData = Omit< | ||
CustomField, | ||
"id" | "createdAt" | "updatedAt" | "linkId" | ||
> & { | ||
type: Omit< | ||
CustomFieldType, | ||
"PHONE_NUMBER" | "CHECKBOX" | "SELECT" | "MULTI_SELECT" | ||
>; | ||
}; | ||
|
||
export default function CustomFieldsPanel({ | ||
fields, | ||
onChange, | ||
isConfigOpen, | ||
setIsConfigOpen, | ||
}: { | ||
fields: CustomFieldData[]; | ||
onChange: (fields: CustomFieldData[]) => void; | ||
isConfigOpen: boolean; | ||
setIsConfigOpen: (open: boolean) => void; | ||
}) { | ||
const { plan } = usePlan(); | ||
|
||
const getFieldLimit = () => { | ||
if (plan === "datarooms") return 3; | ||
if (plan === "business") return 1; | ||
return 0; | ||
}; | ||
|
||
const addField = () => { | ||
const fieldLimit = getFieldLimit(); | ||
if (fields.length >= fieldLimit) { | ||
toast.error( | ||
`You can only add up to ${fieldLimit} custom field${fieldLimit === 1 ? "" : "s"} on the ${plan === "datarooms" ? "Data Rooms" : "Business"} plan`, | ||
); | ||
return; | ||
} | ||
|
||
const newField: CustomFieldData = { | ||
type: "SHORT_TEXT", | ||
identifier: "", | ||
label: "", | ||
placeholder: "", | ||
required: false, | ||
disabled: false, | ||
orderIndex: fields.length, | ||
}; | ||
onChange([...fields, newField]); | ||
}; | ||
|
||
const updateField = (index: number, updatedField: CustomFieldData) => { | ||
const newFields = [...fields]; | ||
newFields[index] = updatedField; | ||
onChange(newFields); | ||
}; | ||
|
||
const removeField = (index: number) => { | ||
const newFields = fields.filter((_, i) => i !== index); | ||
// Update orderIndex for remaining fields | ||
newFields.forEach((field, i) => { | ||
field.orderIndex = i; | ||
}); | ||
onChange(newFields); | ||
}; | ||
|
||
const moveField = (index: number, direction: "up" | "down") => { | ||
if ( | ||
(direction === "up" && index === 0) || | ||
(direction === "down" && index === fields.length - 1) | ||
) | ||
return; | ||
|
||
const newFields = [...fields]; | ||
const newIndex = direction === "up" ? index - 1 : index + 1; | ||
[newFields[index], newFields[newIndex]] = [ | ||
newFields[newIndex], | ||
newFields[index], | ||
]; | ||
|
||
// Update orderIndex for all fields | ||
newFields.forEach((field, i) => { | ||
field.orderIndex = i; | ||
}); | ||
|
||
onChange(newFields); | ||
}; | ||
|
||
const fieldLimit = getFieldLimit(); | ||
|
||
return ( | ||
<Sheet open={isConfigOpen} onOpenChange={setIsConfigOpen}> | ||
<SheetContent className="flex h-full flex-col"> | ||
<SheetHeader> | ||
<SheetTitle>Configure Custom Fields</SheetTitle> | ||
<SheetDescription> | ||
Configure the custom fields that will be shown to viewers. | ||
{fieldLimit > 0 && ( | ||
<span className="mt-1 block text-sm text-muted-foreground"> | ||
You can add up to {fieldLimit} custom field | ||
{fieldLimit === 1 ? "" : "s"} on the{" "} | ||
{plan === "datarooms" ? "Data Rooms" : "Business"} plan. | ||
</span> | ||
)} | ||
</SheetDescription> | ||
</SheetHeader> | ||
|
||
<div className="flex items-center justify-between"> | ||
<div className="text-sm text-muted-foreground"> | ||
{fields.length} of {fieldLimit} custom field | ||
{fields.length === 1 ? "" : "s"} | ||
</div> | ||
<Button | ||
variant="outline" | ||
size="sm" | ||
onClick={addField} | ||
className="flex items-center gap-2" | ||
disabled={fields.length >= fieldLimit} | ||
> | ||
<Plus className="h-4 w-4" /> | ||
Add Field | ||
</Button> | ||
</div> | ||
|
||
<Separator /> | ||
|
||
<ScrollArea className="flex-1"> | ||
<div className="space-y-4"> | ||
{fields.map((field, index) => ( | ||
<CustomFieldComponent | ||
key={index} | ||
field={field} | ||
onUpdate={(updatedField) => updateField(index, updatedField)} | ||
onDelete={() => removeField(index)} | ||
onMoveUp={() => moveField(index, "up")} | ||
onMoveDown={() => moveField(index, "down")} | ||
isFirst={index === 0} | ||
isLast={index === fields.length - 1} | ||
/> | ||
))} | ||
</div> | ||
</ScrollArea> | ||
</SheetContent> | ||
</Sheet> | ||
); | ||
} |
Oops, something went wrong.