Skip to content

Commit

Permalink
improved Accordion UI, added UTM builder
Browse files Browse the repository at this point in the history
  • Loading branch information
steven-tey committed Oct 20, 2022
1 parent 45e0f67 commit 0bda075
Show file tree
Hide file tree
Showing 15 changed files with 395 additions and 84 deletions.
184 changes: 122 additions & 62 deletions components/app/modals/add-edit-link-modal/advanced-settings.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { Dispatch, SetStateAction, useCallback, useState } from "react";
import TextareaAutosize from "react-textarea-autosize";
import BlurImage from "@/components/shared/blur-image";
import {
ChevronRight,
LoadingCircle,
UploadCloud,
} from "@/components/shared/icons";
import { Dispatch, SetStateAction, useMemo, useState } from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { motion } from "framer-motion";
import { CheckCircleFill, ChevronRight } from "@/components/shared/icons";
import { LinkProps } from "@/lib/types";
import { getDateTimeLocal } from "@/lib/utils";
import { getParamsFromURL } from "@/lib/utils";
import ExpirationSection from "./expiration-section";
import OGSection from "./og-section";
import PasswordSection from "./password-section";
import UTMSection from "./utm-section";

export const AnimationSettings = {
initial: { height: 0 },
animate: { height: "auto" },
exit: { height: 0 },
transition: { duration: 0.2, bounce: 0 },
};

export default function AdvancedSettings({
data,
Expand All @@ -19,14 +25,18 @@ export default function AdvancedSettings({
}) {
const [expanded, setExpanded] = useState(false);

const { password, expiresAt } = data;
const { url, title, description, image, password, expiresAt } = data;

const { utm_source, utm_medium, utm_campaign } = useMemo(() => {
return getParamsFromURL(url);
}, [url]);

return (
<div>
<div className="sm:px-16 px-4">
<button
type="button"
className="flex items-center"
className="flex items-center space-x-2"
onClick={() => setExpanded(!expanded)}
>
<ChevronRight
Expand All @@ -39,61 +49,111 @@ export default function AdvancedSettings({
</div>

{expanded && (
<div className="mt-4 grid gap-5 bg-white border-t border-b border-gray-200 sm:px-16 px-4 py-8">
{/* OG Tags Section */}
<OGSection {...{ data, setData }} />
<motion.div key="accordion-root" {...AnimationSettings}>
<AccordionPrimitive.Root
type="single"
collapsible={true}
className="mt-4 grid bg-white border-t border-b border-gray-200 px-2 sm:px-8 py-8"
>
{/* UTM Builder Section */}
<AccordionPrimitive.Item
value="utm"
className="border border-gray-200 rounded-t-lg py-3"
>
<AccordionPrimitive.Header className="px-5">
<AccordionPrimitive.Trigger className="group focus:outline-black flex w-full items-center justify-between space-x-2 bg-white py-2 text-left dark:bg-gray-800">
<div className="flex items-center justify-start space-x-2 h-6">
<ChevronRight className="h-5 w-5 shrink-0 text-gray-700 ease-in-out dark:text-gray-400 group-radix-state-open:rotate-90 transition-all" />
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
UTM Builder
</span>
</div>
{utm_source && utm_medium && utm_campaign && (
<CheckCircleFill className="h-6 w-6 text-black" />
)}
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
<AccordionPrimitive.Content>
<UTMSection {...{ data, setData }} />
</AccordionPrimitive.Content>
</AccordionPrimitive.Item>

{/* Password Protection */}
<AccordionPrimitive.Item
value="password"
className="border border-gray-200 border-t-0 px-5 py-3"
>
<AccordionPrimitive.Header>
<AccordionPrimitive.Trigger className="group focus:outline-black flex w-full items-center justify-between space-x-2 bg-white py-2 text-left dark:bg-gray-800">
<div className="flex items-center justify-start space-x-2 h-6">
<ChevronRight className="h-5 w-5 shrink-0 text-gray-700 ease-in-out dark:text-gray-400 group-radix-state-open:rotate-90 transition-all" />
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
Password Protection
</span>
</div>
{password && (
<CheckCircleFill className="h-6 w-6 text-black" />
)}
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
<AccordionPrimitive.Content>
<PasswordSection {...{ data, setData }} />
</AccordionPrimitive.Content>
</AccordionPrimitive.Item>

{/* Password Protection */}
<div>
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700"
{/* Expiration Date */}
<AccordionPrimitive.Item
value="expire"
className="border border-gray-200 border-t-0 border-b-0 px-5 py-3"
>
Password Protection
</label>
<div className="flex mt-1 rounded-md shadow-sm">
<input
name="password"
id="password"
type="password"
className="border-gray-300 text-gray-900 placeholder-gray-300 focus:border-gray-500 focus:ring-gray-500 block w-full rounded-md focus:outline-none sm:text-sm"
value={password}
onChange={(e) => {
setData({ ...data, password: e.target.value });
}}
aria-invalid="true"
/>
</div>
</div>
<AccordionPrimitive.Header>
<AccordionPrimitive.Trigger className="group focus:outline-black flex w-full items-center justify-between space-x-2 bg-white py-2 text-left dark:bg-gray-800">
<div className="flex items-center justify-start space-x-2 h-6">
<ChevronRight className="h-5 w-5 shrink-0 text-gray-700 ease-in-out dark:text-gray-400 group-radix-state-open:rotate-90 transition-all" />
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
Expiration Date
</span>
{expiresAt &&
new Date().getTime() > new Date(expiresAt).getTime() && (
<span className="bg-amber-500 px-2 py-0.5 text-xs text-white uppercase">
Expired
</span>
)}
</div>
{expiresAt && (
<CheckCircleFill className="h-6 w-6 text-black" />
)}
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
<AccordionPrimitive.Content>
<ExpirationSection {...{ data, setData }} />
</AccordionPrimitive.Content>
</AccordionPrimitive.Item>

{/* Expire Link */}
<div>
<label
htmlFor="expiresAt"
className="flex justify-between text-sm font-medium text-gray-700"
{/* OG Tags Section */}
<AccordionPrimitive.Item
value="og"
className="border border-gray-200 rounded-b-lg px-5 py-3"
>
<p>Auto-expire Link</p>
{expiresAt &&
new Date().getTime() > new Date(expiresAt).getTime() && (
<span className="bg-amber-500 px-2 py-0.5 text-xs text-white uppercase">
Expired
</span>
)}
</label>
<input
type="datetime-local"
id="expiresAt"
name="expiresAt"
min={getDateTimeLocal()}
value={expiresAt ? getDateTimeLocal(expiresAt) : ""}
step="60" // need to add step to prevent weird date bug (https://stackoverflow.com/q/19284193/10639526)
onChange={(e) => {
setData({ ...data, expiresAt: new Date(e.target.value) });
}}
className="flex space-x-2 justify-center items-center mt-1 rounded-md shadow-sm border border-gray-300 text-gray-500 hover:border-gray-800 px-3 py-2 w-full focus:outline-none sm:text-sm transition-all"
/>
</div>
</div>
<AccordionPrimitive.Header>
<AccordionPrimitive.Trigger className="group focus:outline-black flex w-full items-center justify-between space-x-2 bg-white py-2 text-left dark:bg-gray-800">
<div className="flex items-center justify-start space-x-2 h-6">
<ChevronRight className="h-5 w-5 shrink-0 text-gray-700 ease-in-out dark:text-gray-400 group-radix-state-open:rotate-90 transition-all" />
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
Custom OG Tags
</span>
</div>
{title && description && image && (
<CheckCircleFill className="h-6 w-6 text-black" />
)}
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
<AccordionPrimitive.Content>
<OGSection {...{ data, setData }} />
</AccordionPrimitive.Content>
</AccordionPrimitive.Item>
</AccordionPrimitive.Root>
</motion.div>
)}
</div>
);
Expand Down
47 changes: 47 additions & 0 deletions components/app/modals/add-edit-link-modal/expiration-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Dispatch, SetStateAction } from "react";
import { motion } from "framer-motion";
import { X } from "@/components/shared/icons";
import { LinkProps } from "@/lib/types";
import { getDateTimeLocal } from "@/lib/utils";
import { AnimationSettings } from "./advanced-settings";

export default function ExpireSection({
data,
setData,
}: {
data: LinkProps;
setData: Dispatch<SetStateAction<LinkProps>>;
}) {
const { expiresAt } = data;
return (
<motion.div key="expire" {...AnimationSettings}>
<div>
<label htmlFor="expiresAt" className="block my-2 text-sm text-gray-500">
Automatically expires your link at a given date and time. Your link
will be disabled but the data will still be kept.
</label>
<div className="flex space-x-2 mb-3">
<input
type="datetime-local"
id="expiresAt"
name="expiresAt"
min={getDateTimeLocal()}
value={expiresAt ? getDateTimeLocal(expiresAt) : ""}
step="60" // need to add step to prevent weird date bug (https://stackoverflow.com/q/19284193/10639526)
onChange={(e) => {
setData({ ...data, expiresAt: new Date(e.target.value) });
}}
className="flex space-x-2 justify-center items-center rounded-md shadow-sm border border-gray-300 text-gray-500 hover:border-gray-800 px-3 py-2 w-full focus:outline-none sm:text-sm transition-all"
/>
<button
onClick={() => setData({ ...data, expiresAt: null })}
type="button"
className="group rounded-md border w-10 h-10 flex justify-center items-center text-gray-500 hover:text-gray-800 hover:border-gray-800 focus:outline-none transition-all"
>
<X className="text-gray-400 w-4 h-4 group-hover:text-black transition-all" />
</button>
</div>
</div>
</motion.div>
);
}
6 changes: 0 additions & 6 deletions components/app/modals/add-edit-link-modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,6 @@ function AddEditLinkModal({
description: null,
image: null,

utm_source: null,
utm_medium: null,
utm_campaign: null,
utm_term: null,
utm_content: null,

clicks: 0,
userId: "",
createdAt: new Date(),
Expand Down
23 changes: 16 additions & 7 deletions components/app/modals/add-edit-link-modal/og-section.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Dispatch, SetStateAction, useCallback, useState } from "react";
import { motion } from "framer-motion";
import TextareaAutosize from "react-textarea-autosize";
import BlurImage from "@/components/shared/blur-image";
import { LoadingCircle, UploadCloud } from "@/components/shared/icons";
import { LinkProps } from "@/lib/types";
import { AnimationSettings } from "./advanced-settings";

export default function OGSection({
data,
Expand All @@ -11,7 +13,6 @@ export default function OGSection({
data: LinkProps;
setData: Dispatch<SetStateAction<LinkProps>>;
}) {
const [expanded, setExpanded] = useState(false);
const [generatingTitle, setGeneratingTitle] = useState(false);
const [generatingDescription, setGeneratingDescription] = useState(false);

Expand Down Expand Up @@ -51,8 +52,15 @@ export default function OGSection({
[setData],
);
return (
<>
<div>
<motion.div key="og" className="grid gap-5" {...AnimationSettings}>
<p className="block mt-2 text-sm text-gray-500 px-5">
If you use custom OG tags,{" "}
<span className="font-semibold text-black">
be sure to set all 3 tags
</span>
, or the default tags of the target URL will be used.
</p>
<div className="border-t border-gray-200 px-5 pt-5 pb-2.5">
<div className="flex justify-between items-center">
<label
htmlFor="title"
Expand Down Expand Up @@ -80,7 +88,7 @@ export default function OGSection({
id="title"
minRows={3}
className="border-gray-300 text-gray-900 placeholder-gray-300 focus:border-gray-500 focus:ring-gray-500 pr-10 block w-full rounded-md focus:outline-none sm:text-sm"
placeholder="Dub - an open-source link shortener SaaS with built-in analytics + free custom domains."
placeholder="Dub - Open Source Bitly Alternative"
value={title}
onChange={(e) => {
setData({ ...data, title: e.target.value });
Expand All @@ -89,7 +97,8 @@ export default function OGSection({
/>
</div>
</div>
<div>

<div className="border-t border-gray-200 px-5 pt-5 pb-2.5">
<div className="flex justify-between items-center">
<label
htmlFor="description"
Expand Down Expand Up @@ -131,7 +140,7 @@ export default function OGSection({
</div>
</div>

<div>
<div className="border-t border-gray-200 px-5 pt-5 pb-2.5">
<p className="block text-sm font-medium text-gray-700">OG Image</p>
<label
htmlFor="image"
Expand Down Expand Up @@ -173,6 +182,6 @@ export default function OGSection({
/>
</div>
</div>
</>
</motion.div>
);
}
35 changes: 35 additions & 0 deletions components/app/modals/add-edit-link-modal/password-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Dispatch, SetStateAction } from "react";
import { motion } from "framer-motion";
import { LinkProps } from "@/lib/types";
import { AnimationSettings } from "./advanced-settings";

export default function PasswordSection({
data,
setData,
}: {
data: LinkProps;
setData: Dispatch<SetStateAction<LinkProps>>;
}) {
const { password } = data;
return (
<motion.div key="password" {...AnimationSettings}>
<label htmlFor="password" className="block my-2 text-sm text-gray-500">
Protect your links with a password. Users will need to enter the
password to access the link.
</label>
<div className="flex rounded-md shadow-sm mb-3">
<input
name="password"
id="password"
type="password"
className="border-gray-300 text-gray-900 placeholder-gray-300 focus:border-gray-500 focus:ring-gray-500 block w-full rounded-md focus:outline-none sm:text-sm"
value={password}
onChange={(e) => {
setData({ ...data, password: e.target.value });
}}
aria-invalid="true"
/>
</div>
</motion.div>
);
}
Loading

0 comments on commit 0bda075

Please sign in to comment.