Skip to content

Commit

Permalink
Merge pull request #1444 from mfts/marc/pm-13-add-new-fields-to-acces…
Browse files Browse the repository at this point in the history
…s-screen

feat: add custom fields to links
  • Loading branch information
mfts authored Jan 14, 2025
2 parents 1804147 + ffaf168 commit 7a648fd
Show file tree
Hide file tree
Showing 34 changed files with 1,196 additions and 271 deletions.
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

0 comments on commit 7a648fd

Please sign in to comment.