Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add custom fields to links #1444

Merged
merged 8 commits into from
Jan 14, 2025
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
10 changes: 2 additions & 8 deletions components/NotionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,7 @@ export const NotionPage = ({
className="cursor-pointer underline underline-offset-4 hover:font-medium"
onClick={() => setSubPageId(null)}
style={{
color:
brand && brand.brandColor
? determineTextColor(brand.brandColor)
: "white",
color: determineTextColor(brand?.brandColor),
}}
>
{title}
Expand All @@ -327,10 +324,7 @@ export const NotionPage = ({
<BreadcrumbPage
className="font-medium"
style={{
color:
brand && brand.brandColor
? determineTextColor(brand.brandColor)
: "white",
color: determineTextColor(brand?.brandColor),
}}
>
{subTitle}
Expand Down
4 changes: 2 additions & 2 deletions components/documents/video-analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ export default function VideoAnalytics({

const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
const uniqueViews = payload[0].value;
const playbackCount = payload[1].value;
const uniqueViews = payload[1].value;
const playbackCount = payload[0].value;
const intensity = playbackCount / uniqueViews || 1;

return (
Expand Down
169 changes: 169 additions & 0 deletions components/links/link-sheet/custom-fields-panel/custom-field.tsx
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 components/links/link-sheet/custom-fields-panel/index.tsx
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>
);
}
Loading
Loading