Skip to content
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
88 changes: 88 additions & 0 deletions components/links/link-sheet/allow-list-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Dispatch, SetStateAction, useState, useEffect, ChangeEvent } from "react";
import { Switch } from "@/components/ui/switch";
import { motion } from "framer-motion";
import { FADE_IN_ANIMATION_SETTINGS } from "@/lib/constants";
import { cn, sanitizeAllowDenyList } from "@/lib/utils";
import { DEFAULT_LINK_TYPE } from ".";

export default function AllowListSection({
data,
setData,
}: {
data: DEFAULT_LINK_TYPE;
setData: Dispatch<SetStateAction<DEFAULT_LINK_TYPE>>;
}) {
const { emailAuthenticated, allowList } = data;
// Initialize enabled state based on whether allowList is not null and not empty
const [enabled, setEnabled] = useState<boolean>(!!allowList && allowList.length > 0);
const [allowListInput, setAllowListInput] = useState<string>(allowList?.join("\n") || "");

useEffect(() => {
// Update the allowList in the data state when their inputs change
const newAllowList = sanitizeAllowDenyList(allowListInput);
setEnabled((prevEnabled) => prevEnabled && emailAuthenticated);
setData((prevData) => ({
...prevData,
allowList: emailAuthenticated && enabled ? newAllowList : [],
}));
}, [allowListInput, enabled, emailAuthenticated, setData]);

const handleEnableAllowList = () => {
const updatedEnabled = !enabled;
setEnabled(updatedEnabled);

if (updatedEnabled) {
setData((prevData) => ({
...prevData,
allowList: updatedEnabled ? sanitizeAllowDenyList(allowListInput) : [],
emailAuthenticated: true, // Turn on email authentication
emailProtected: true // Turn on email protection
}));
} else {
setData((prevData) => ({
...prevData,
allowList: [],
}));
}
};

const handleAllowListChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setAllowListInput(event.target.value);
};


return (
<div className="pb-5">
<div className="flex flex-col space-y-4">
<div className="flex items-center justify-between">
<h2
className={cn(
"text-sm font-medium leading-6",
enabled ? "text-foreground" : "text-muted-foreground",
)}
>
Allow specified viewers
</h2>
<Switch
checked={enabled}
onCheckedChange={handleEnableAllowList}
/>
</div>
{enabled && (
<motion.div
className="mt-1 block w-full"
{...FADE_IN_ANIMATION_SETTINGS}
>
<textarea
className="form-textarea w-full rounded-md border-0 py-1.5 text-foreground bg-background shadow-sm ring-1 ring-inset ring-input placeholder:text-muted-foreground focus:ring-2 focus:ring-inset focus:ring-gray-400 text-sm leading-6"
rows={5}
placeholder="Enter allowed emails/domains, one per line, e.g. [email protected] @example.org"
value={allowListInput}
onChange={handleAllowListChange}
/>
</motion.div>
)}
</div>
</div>
);
}
89 changes: 89 additions & 0 deletions components/links/link-sheet/deny-list-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Dispatch, SetStateAction, useState, useEffect, ChangeEvent } from "react";
import { Switch } from "@/components/ui/switch";
import { motion } from "framer-motion";
import { FADE_IN_ANIMATION_SETTINGS } from "@/lib/constants";
import { cn, sanitizeAllowDenyList } from "@/lib/utils";
import { DEFAULT_LINK_TYPE } from ".";

export default function DenyListSection({
data,
setData,
}: {
data: DEFAULT_LINK_TYPE;
setData: Dispatch<SetStateAction<DEFAULT_LINK_TYPE>>;
}) {
const { emailAuthenticated, denyList } = data;
// Initialize enabled state based on whether denyList is not null and not empty
const [enabled, setEnabled] = useState<boolean>(!!denyList && denyList.length > 0);
const [denyListInput, setDenyListInput] = useState<string>(denyList?.join("\n") || "");


useEffect(() => {
// Update the denyList in the data state when their inputs change
const newDenyList = sanitizeAllowDenyList(denyListInput);
setEnabled((prevEnabled) => prevEnabled && emailAuthenticated);
setData((prevData) => ({
...prevData,
denyList: emailAuthenticated && enabled ? newDenyList : [],
}));
}, [denyListInput, enabled, emailAuthenticated, setData]);

const handleEnableDenyList = () => {
const updatedEnabled = !enabled;
setEnabled(updatedEnabled);

if (updatedEnabled) {
setData((prevData) => ({
...prevData,
denyList: updatedEnabled ? sanitizeAllowDenyList(denyListInput) : [],
emailAuthenticated: true, // Turn on email authentication
emailProtected: true // Turn on email protection
}));
} else {
setData((prevData) => ({
...prevData,
denyList: [],
}));
}
};

const handleDenyListChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setDenyListInput(event.target.value);
};


return (
<div className="pb-5">
<div className="flex flex-col space-y-4">
<div className="flex items-center justify-between">
<h2
className={cn(
"text-sm font-medium leading-6",
enabled ? "text-foreground" : "text-muted-foreground",
)}
>
Block specified viewers
</h2>
<Switch
checked={enabled}
onCheckedChange={handleEnableDenyList}
/>
</div>
{enabled && (
<motion.div
className="mt-1 block w-full"
{...FADE_IN_ANIMATION_SETTINGS}
>
<textarea
className="form-textarea w-full rounded-md border-0 py-1.5 text-foreground bg-background shadow-sm ring-1 ring-inset ring-input placeholder:text-muted-foreground focus:ring-2 focus:ring-inset focus:ring-gray-400 text-sm leading-6"
rows={5}
placeholder="Enter blocked emails/domains, one per line, e.g. [email protected] @example.org"
value={denyListInput}
onChange={handleDenyListChange}
/>
</motion.div>
)}
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function EmailAuthenticationSection({
setData: Dispatch<SetStateAction<DEFAULT_LINK_TYPE>>;
}) {
const { emailProtected, emailAuthenticated } = data;
const [enabled, setEnabled] = useState<boolean>(true);
const [enabled, setEnabled] = useState<boolean>(emailAuthenticated);

useEffect(() => {
setEnabled(emailAuthenticated);
Expand All @@ -23,6 +23,8 @@ export default function EmailAuthenticationSection({
...data,
emailProtected: updatedEmailAuthentication ? true : emailProtected,
emailAuthenticated: updatedEmailAuthentication,
allowList: updatedEmailAuthentication ? data.allowList : [],
denyList: updatedEmailAuthentication ? data.denyList : [],
});
setEnabled(updatedEmailAuthentication);
};
Expand All @@ -37,7 +39,7 @@ export default function EmailAuthenticationSection({
enabled ? "text-foreground" : "text-muted-foreground",
)}
>
Require email authentication to view
Require email verification
</h2>
</div>
<Switch
Expand Down
4 changes: 2 additions & 2 deletions components/links/link-sheet/email-protection-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export default function EmailProtectionSection({
data: DEFAULT_LINK_TYPE;
setData: Dispatch<SetStateAction<DEFAULT_LINK_TYPE>>;
}) {
const { emailProtected, emailAuthenticated } = data;
const [enabled, setEnabled] = useState<boolean>(true);
const { emailProtected } = data;
const [enabled, setEnabled] = useState<boolean>(emailProtected);

useEffect(() => {
setEnabled(emailProtected);
Expand Down
33 changes: 27 additions & 6 deletions components/links/link-sheet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import AllowNotificationSection from "./allow-notification-section";
import FeedbackSection from "./feedback-section";
import OGSection from "./og-section";
import { ScrollArea } from "@/components/ui/scroll-area";
import AllowListSection from "./allow-list-section";
import DenyListSection from "./deny-list-section";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";

export const DEFAULT_LINK_PROPS = {
id: null,
Expand All @@ -38,6 +41,8 @@ export const DEFAULT_LINK_PROPS = {
emailProtected: true,
emailAuthenticated: false,
allowDownload: false,
allowList: [],
denyList: [],
enableNotification: true,
enableFeedback: true,
enableCustomMetatag: false,
Expand All @@ -56,6 +61,8 @@ export type DEFAULT_LINK_TYPE = {
emailProtected: boolean;
emailAuthenticated: boolean;
allowDownload: boolean;
allowList: string[];
denyList: string[];
enableNotification: boolean;
enableFeedback: boolean;
enableCustomMetatag: boolean; // metatags
Expand Down Expand Up @@ -209,20 +216,34 @@ export default function LinkSheet({
<Separator className="bg-muted-foreground absolute" />
<div className="relative mx-auto">
<span className="px-2 bg-background text-muted-foreground text-sm">
Optional
Link Options
</span>
</div>
</div>

<div>
<EmailProtectionSection {...{ data, setData }} />
<EmailAuthenticationSection {...{ data, setData }} />
<AllowNotificationSection {...{ data, setData }} />
<AllowDownloadSection {...{ data, setData }} />
<PasswordSection {...{ data, setData }} />
<ExpirationSection {...{ data, setData }} />
<OGSection {...{ data, setData }} />
<AllowNotificationSection {...{ data, setData }} />
<FeedbackSection {...{ data, setData }} />

<Accordion type="single" collapsible>
<AccordionItem value="item-1" className="border-none">
<AccordionTrigger className="py-0 rounded-lg space-x-2">
<span className="text-sm font-medium leading-6 text-foreground">
Advanced Link Access Options
</span>
</AccordionTrigger>
<AccordionContent className="first:pt-5">
<EmailAuthenticationSection {...{ data, setData }} />
<AllowListSection {...{ data, setData }} />
<DenyListSection {...{ data, setData }} />
<PasswordSection {...{ data, setData }} />
<OGSection {...{ data, setData }} />
<FeedbackSection {...{ data, setData }} />
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions components/links/links-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export default function LinksTable({
emailProtected: link.emailProtected,
emailAuthenticated: link.emailAuthenticated,
allowDownload: link.allowDownload ? link.allowDownload : false,
allowList: link.allowList,
denyList: link.denyList,
enableNotification: link.enableNotification
? link.enableNotification
: false,
Expand Down
56 changes: 56 additions & 0 deletions components/ui/accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"

import { cn } from "@/lib/utils"

const Accordion = AccordionPrimitive.Root

const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"

const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName

const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))

AccordionContent.displayName = AccordionPrimitive.Content.displayName

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
2 changes: 1 addition & 1 deletion components/ui/scroll-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const ScrollArea = React.forwardRef<
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollBar className="hidden" />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
Expand Down
11 changes: 11 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,14 @@ export const generateGravatarHash = (email: string | null): string => {

return hash;
};

export const sanitizeAllowDenyList = (list: string): string[] => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const domainRegex = /^@[^\s@]+\.[^\s@]+$/;

return list
.split("\n")
.map(item => item.trim().replace(/,$/, '')) // Trim whitespace and remove trailing commas
.filter(item => item !== "") // Remove empty items
.filter(item => emailRegex.test(item) || domainRegex.test(item)); // Remove items that don't match email or domain regex
};
Loading