From b75b125981047c9104f5b9c3b4db9e0f28a3437b Mon Sep 17 00:00:00 2001 From: KM Koushik Date: Sat, 28 Sep 2024 20:48:26 +1000 Subject: [PATCH] add new design (#70) * add new design stuff * add more ui things * add more ui changes * more ui changes * add more design * update emoji --- apps/web/package.json | 2 + .../migration.sql | 2 + apps/web/prisma/schema.prisma | 1 + .../(dashboard)/admin/ses-configurations.tsx | 2 +- .../campaigns/[campaignId]/edit/page.tsx | 326 ++++++++++----- .../campaigns/[campaignId]/page.tsx | 47 +-- .../(dashboard)/campaigns/campaign-list.tsx | 213 +++++++++- .../(dashboard)/campaigns/delete-campaign.tsx | 4 +- .../campaigns/duplicate-campaign.tsx | 4 +- .../contacts/[contactBookId]/add-contact.tsx | 2 +- .../contacts/[contactBookId]/contact-list.tsx | 4 +- .../contacts/[contactBookId]/page.tsx | 174 ++++++-- .../(dashboard)/contacts/add-contact-book.tsx | 2 +- .../contacts/contact-books-list.tsx | 107 ++--- .../contacts/delete-contact-book.tsx | 6 +- .../contacts/edit-contact-book.tsx | 12 +- .../web/src/app/(dashboard)/contacts/page.tsx | 2 +- .../src/app/(dashboard)/dasboard-layout.tsx | 18 +- .../(dashboard)/dashboard/dashboard-chart.tsx | 6 +- .../dev-settings/api-keys/api-list.tsx | 2 +- .../(dashboard)/domains/[domainId]/page.tsx | 25 +- .../app/(dashboard)/domains/add-domain.tsx | 2 +- .../app/(dashboard)/domains/domain-badge.tsx | 15 +- .../app/(dashboard)/domains/domain-list.tsx | 2 +- .../(dashboard)/domains/status-indicator.tsx | 2 +- .../app/(dashboard)/emails/email-details.tsx | 38 +- .../src/app/(dashboard)/emails/email-list.tsx | 22 +- .../(dashboard)/emails/email-status-badge.tsx | 42 +- apps/web/src/app/(dashboard)/emails/page.tsx | 3 +- apps/web/src/app/layout.tsx | 2 +- apps/web/src/app/login/login-page.tsx | 69 +++- apps/web/src/app/signup/page.tsx | 16 + .../src/components/theme/ThemeSwitcher.tsx | 45 ++ apps/web/src/server/api/routers/campaign.ts | 23 +- apps/web/src/server/api/routers/contacts.ts | 31 +- apps/web/src/server/api/routers/email.ts | 2 +- .../src/server/service/campaign-service.ts | 15 +- .../email-editor/src/extensions/dragHandle.ts | 390 ++++++++++++++++++ packages/email-editor/src/extensions/index.ts | 2 +- packages/tailwind-config/tailwind.config.ts | 4 +- packages/ui/index.ts | 2 +- packages/ui/package.json | 3 + packages/ui/src/accordion.tsx | 55 +++ packages/ui/src/button.tsx | 2 +- packages/ui/src/charts.tsx | 365 ++++++++++++++++ packages/ui/src/input.tsx | 2 +- packages/ui/src/select.tsx | 2 +- packages/ui/src/text-with-copy.tsx | 2 +- packages/ui/styles/globals.css | 16 +- pnpm-lock.yaml | 193 +++++++-- 50 files changed, 1909 insertions(+), 419 deletions(-) create mode 100644 apps/web/prisma/migrations/20240914003754_add_emoji_for_contact_book/migration.sql create mode 100644 apps/web/src/app/signup/page.tsx create mode 100644 apps/web/src/components/theme/ThemeSwitcher.tsx create mode 100644 packages/email-editor/src/extensions/dragHandle.ts create mode 100644 packages/ui/src/accordion.tsx create mode 100644 packages/ui/src/charts.tsx diff --git a/apps/web/package.json b/apps/web/package.json index 8cd7d0c..8632d90 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -38,6 +38,8 @@ "bullmq": "^5.8.2", "chrono-node": "^2.7.6", "date-fns": "^3.6.0", + "emoji-picker-react": "^4.12.0", + "framer-motion": "^11.0.24", "hono": "^4.2.2", "html-to-text": "^9.0.5", "install": "^0.13.0", diff --git a/apps/web/prisma/migrations/20240914003754_add_emoji_for_contact_book/migration.sql b/apps/web/prisma/migrations/20240914003754_add_emoji_for_contact_book/migration.sql new file mode 100644 index 0000000..5215bca --- /dev/null +++ b/apps/web/prisma/migrations/20240914003754_add_emoji_for_contact_book/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "ContactBook" ADD COLUMN "emoji" TEXT NOT NULL DEFAULT '📙'; diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma index 7ec283e..870085e 100644 --- a/apps/web/prisma/schema.prisma +++ b/apps/web/prisma/schema.prisma @@ -221,6 +221,7 @@ model ContactBook { properties Json createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + emoji String @default("📙") team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) contacts Contact[] diff --git a/apps/web/src/app/(dashboard)/admin/ses-configurations.tsx b/apps/web/src/app/(dashboard)/admin/ses-configurations.tsx index 97f1bbd..42dae43 100644 --- a/apps/web/src/app/(dashboard)/admin/ses-configurations.tsx +++ b/apps/web/src/app/(dashboard)/admin/ses-configurations.tsx @@ -19,7 +19,7 @@ export default function SesConfigurations() { return (
-
+
diff --git a/apps/web/src/app/(dashboard)/campaigns/[campaignId]/edit/page.tsx b/apps/web/src/app/(dashboard)/campaigns/[campaignId]/edit/page.tsx index e3582e9..b158b50 100644 --- a/apps/web/src/app/(dashboard)/campaigns/[campaignId]/edit/page.tsx +++ b/apps/web/src/app/(dashboard)/campaigns/[campaignId]/edit/page.tsx @@ -35,6 +35,12 @@ import { import { toast } from "@unsend/ui/src/toaster"; import { useDebouncedCallback } from "use-debounce"; import { formatDistanceToNow } from "date-fns"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@unsend/ui/src/accordion"; const sendSchema = z.object({ confirmation: z.string(), @@ -97,6 +103,12 @@ function CampaignEditor({ const [subject, setSubject] = useState(campaign.subject); const [from, setFrom] = useState(campaign.from); const [contactBookId, setContactBookId] = useState(campaign.contactBookId); + const [replyTo, setReplyTo] = useState( + campaign.replyTo[0] + ); + const [previewText, setPreviewText] = useState( + campaign.previewText + ); const [openSendDialog, setOpenSendDialog] = useState(false); const updateCampaignMutation = api.campaign.updateCampaign.useMutation({ @@ -179,10 +191,14 @@ function CampaignEditor({ const confirmation = sendForm.watch("confirmation"); + const contactBook = contactBooksQuery.data?.find( + (book) => book.id === contactBookId + ); + return ( -
-
-
+
+
+
-
- - { - setSubject(e.target.value); - }} - onBlur={() => { - if (subject === campaign.subject || !subject) { - return; - } - updateCampaignMutation.mutate( - { - campaignId: campaign.id, - subject, - }, - { - onError: (e) => { - toast.error(`${e.message}. Reverting changes.`); - setSubject(campaign.subject); - }, - } - ); - }} - className="mt-1 block w-full rounded-md shadow-sm" - /> -
-
- - { - setFrom(e.target.value); - }} - className="mt-1 block w-full rounded-md shadow-sm" - placeholder="Friendly name" - onBlur={() => { - if (from === campaign.from) { - return; - } - updateCampaignMutation.mutate( - { - campaignId: campaign.id, - from, - }, - { - onError: (e) => { - toast.error(`${e.message}. Reverting changes.`); - setFrom(campaign.from); - }, - } - ); - }} - /> -
-
- - {contactBooksQuery.isLoading ? ( - - ) : ( - - )} -
- { - setJson(content.getJSON()); - setIsSaving(true); - deboucedUpdateCampaign(); - }} - variables={["email", "firstName", "lastName"]} - uploadImage={ - campaign.imageUploadSupported ? handleFileChange : undefined - } - /> + + +
+
+ + { + setSubject(e.target.value); + }} + onBlur={() => { + if (subject === campaign.subject || !subject) { + return; + } + updateCampaignMutation.mutate( + { + campaignId: campaign.id, + subject, + }, + { + onError: (e) => { + toast.error(`${e.message}. Reverting changes.`); + setSubject(campaign.subject); + }, + } + ); + }} + className="mt-1 py-1 text-sm block w-full outline-none border-b border-transparent focus:border-border bg-transparent" + /> + +
+ + +
+ + { + setFrom(e.target.value); + }} + className="mt-1 py-1 w-full text-sm outline-none border-b border-transparent focus:border-border bg-transparent" + placeholder="Friendly name" + onBlur={() => { + if (from === campaign.from || !from) { + return; + } + updateCampaignMutation.mutate( + { + campaignId: campaign.id, + from, + }, + { + onError: (e) => { + toast.error(`${e.message}. Reverting changes.`); + setFrom(campaign.from); + }, + } + ); + }} + /> +
+
+ + { + setReplyTo(e.target.value); + }} + className="mt-1 py-1 text-sm block w-full outline-none border-b border-transparent bg-transparent focus:border-border" + placeholder="hello@example.com" + onBlur={() => { + if (replyTo === campaign.replyTo[0]) { + return; + } + updateCampaignMutation.mutate( + { + campaignId: campaign.id, + replyTo: replyTo ? [replyTo] : [], + }, + { + onError: (e) => { + toast.error(`${e.message}. Reverting changes.`); + setReplyTo(campaign.replyTo[0]); + }, + } + ); + }} + /> +
+ +
+ + { + setPreviewText(e.target.value); + }} + onBlur={() => { + if ( + previewText === campaign.previewText || + !previewText + ) { + return; + } + updateCampaignMutation.mutate( + { + campaignId: campaign.id, + previewText, + }, + { + onError: (e) => { + toast.error(`${e.message}. Reverting changes.`); + setPreviewText(campaign.previewText ?? ""); + }, + } + ); + }} + className="mt-1 py-1 text-sm block w-full outline-none border-b border-transparent bg-transparent focus:border-border" + /> +
+
+ + {contactBooksQuery.isLoading ? ( + + ) : ( + + )} +
+
+
+
+
+ +
+
+ { + setJson(content.getJSON()); + setIsSaving(true); + deboucedUpdateCampaign(); + }} + variables={["email", "firstName", "lastName"]} + uploadImage={ + campaign.imageUploadSupported ? handleFileChange : undefined + } + /> +
+
); diff --git a/apps/web/src/app/(dashboard)/campaigns/[campaignId]/page.tsx b/apps/web/src/app/(dashboard)/campaigns/[campaignId]/page.tsx index 462fc9d..d6d174c 100644 --- a/apps/web/src/app/(dashboard)/campaigns/[campaignId]/page.tsx +++ b/apps/web/src/app/(dashboard)/campaigns/[campaignId]/page.tsx @@ -60,7 +60,7 @@ export default function CampaignDetailsPage({ ]; return ( -
+
@@ -78,13 +78,13 @@ export default function CampaignDetailsPage({ -
+

Statistics

{statusCards.map((card) => (
{card.status !== "total" ? ( @@ -108,36 +108,33 @@ export default function CampaignDetailsPage({
{campaign.html && ( -
+

Email

-
-
- From - {campaign.from} -
- -
- To - {campaign.contactBookId ? ( +
+
+
+
Subject
+
{campaign.subject}
+
+
+
From
+
{campaign.from}
+
+
+
Contact
- {campaign.contactBook?.name} - +
+ {campaign.contactBook?.emoji}   + {campaign.contactBook?.name} +
- ) : ( -
No one
- )} +
- -
- Subject - {campaign.subject} -
-
+
diff --git a/apps/web/src/app/(dashboard)/campaigns/campaign-list.tsx b/apps/web/src/app/(dashboard)/campaigns/campaign-list.tsx index 1666352..87f4082 100644 --- a/apps/web/src/app/(dashboard)/campaigns/campaign-list.tsx +++ b/apps/web/src/app/(dashboard)/campaigns/campaign-list.tsx @@ -11,19 +11,18 @@ import { import { api } from "~/trpc/react"; import { useUrlState } from "~/hooks/useUrlState"; import { Button } from "@unsend/ui/src/button"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, -} from "@unsend/ui/src/select"; import Spinner from "@unsend/ui/src/spinner"; import { formatDistanceToNow } from "date-fns"; import { CampaignStatus } from "@prisma/client"; import DeleteCampaign from "./delete-campaign"; -import { Edit2 } from "lucide-react"; import Link from "next/link"; import DuplicateCampaign from "./duplicate-campaign"; +import { + Select, + SelectTrigger, + SelectContent, + SelectItem, +} from "@unsend/ui/src/select"; export default function CampaignList() { const [page, setPage] = useUrlState("page", "1"); @@ -33,30 +32,37 @@ export default function CampaignList() { const campaignsQuery = api.campaign.getCampaigns.useQuery({ page: pageNumber, + status: status as CampaignStatus | null, }); return (
- {/* setStatus(val === "all" ? null : val)} > - {status || "All statuses"} + {status ? status.toLowerCase() : "All statuses"} - + All statuses - - Active + + Draft - - Inactive + + Scheduled + + + Sent - */} +
@@ -97,10 +103,10 @@ export default function CampaignList() {
{campaign.status.toLowerCase()} @@ -148,3 +154,170 @@ export default function CampaignList() {
); } + +// "use client"; + +// import { +// Table, +// TableHeader, +// TableRow, +// TableHead, +// TableBody, +// TableCell, +// } from "@unsend/ui/src/table"; +// import { api } from "~/trpc/react"; +// import { useUrlState } from "~/hooks/useUrlState"; +// import { Button } from "@unsend/ui/src/button"; +// import Spinner from "@unsend/ui/src/spinner"; +// import { formatDistanceToNow } from "date-fns"; +// import { CampaignStatus } from "@prisma/client"; +// import DeleteCampaign from "./delete-campaign"; +// import Link from "next/link"; +// import DuplicateCampaign from "./duplicate-campaign"; +// import { motion } from "framer-motion"; +// import { useRouter } from "next/navigation"; +// import { +// Select, +// SelectTrigger, +// SelectContent, +// SelectItem, +// } from "@unsend/ui/src/select"; + +// export default function CampaignList() { +// const [page, setPage] = useUrlState("page", "1"); +// const [status, setStatus] = useUrlState("status"); + +// const pageNumber = Number(page); + +// const campaignsQuery = api.campaign.getCampaigns.useQuery({ +// page: pageNumber, +// status: status as CampaignStatus | null, +// }); + +// const router = useRouter(); + +// return ( +//
+//
+// +//
+ +// {campaignsQuery.isLoading ? ( +//
+// +//
+// ) : ( +//
+// {campaignsQuery.data?.campaigns.map((campaign) => ( +// +//
+// +//
+//
+//
+// + +//
+//
router.push(`/campaigns/${campaign.id}`)} +// > +//
+//
+//
+// {campaign.name} +//
+//
+// {campaign.status.toLowerCase()} +//
+//
+//
+// {formatDistanceToNow(campaign.createdAt, { +// addSuffix: true, +// })} +//
+//
+//
+ +//
+// +// +//
+//
+//
+// +// ))} +//
+// )} + +// {campaignsQuery.data?.totalPage && campaignsQuery.data.totalPage > 1 ? ( +//
+// +// +//
+// ) : null} +//
+// ); +// } diff --git a/apps/web/src/app/(dashboard)/campaigns/delete-campaign.tsx b/apps/web/src/app/(dashboard)/campaigns/delete-campaign.tsx index 8a9aaa3..763a76a 100644 --- a/apps/web/src/app/(dashboard)/campaigns/delete-campaign.tsx +++ b/apps/web/src/app/(dashboard)/campaigns/delete-campaign.tsx @@ -74,8 +74,8 @@ export const DeleteCampaign: React.FC<{ onOpenChange={(_open) => (_open !== open ? setOpen(_open) : null)} > - diff --git a/apps/web/src/app/(dashboard)/campaigns/duplicate-campaign.tsx b/apps/web/src/app/(dashboard)/campaigns/duplicate-campaign.tsx index a5738d9..17079be 100644 --- a/apps/web/src/app/(dashboard)/campaigns/duplicate-campaign.tsx +++ b/apps/web/src/app/(dashboard)/campaigns/duplicate-campaign.tsx @@ -45,8 +45,8 @@ export const DuplicateCampaign: React.FC<{ onOpenChange={(_open) => (_open !== open ? setOpen(_open) : null)} > - diff --git a/apps/web/src/app/(dashboard)/contacts/[contactBookId]/add-contact.tsx b/apps/web/src/app/(dashboard)/contacts/[contactBookId]/add-contact.tsx index dc85a5a..3539f8c 100644 --- a/apps/web/src/app/(dashboard)/contacts/[contactBookId]/add-contact.tsx +++ b/apps/web/src/app/(dashboard)/contacts/[contactBookId]/add-contact.tsx @@ -120,7 +120,7 @@ export default function AddContact({ />
+ + + { + // Handle emoji selection here + // You might want to update the contactBook's emoji + updateContactBookMutation.mutate({ + contactBookId: params.contactBookId, + emoji: emojiObject.emoji, + }); + }} + theme={ + theme === "system" + ? Theme.AUTO + : theme === "dark" + ? Theme.DARK + : Theme.LIGHT + } + /> + + + + + {contactBookDetailQuery.data?.name} + +
@@ -53,38 +122,77 @@ export default function ContactsPage({
-
-
-
Total Contacts
-
- {contactBookDetailQuery.data?.totalContacts !== undefined - ? contactBookDetailQuery.data?.totalContacts - : "--"} +
+
+

Metrics

+
+
+ Total Contacts +
+
+ {contactBookDetailQuery.data?.totalContacts !== undefined + ? contactBookDetailQuery.data?.totalContacts + : "--"} +
+
+
+
+ Unsubscribed +
+
+ {contactBookDetailQuery.data?.unsubscribedContacts !== undefined + ? contactBookDetailQuery.data?.unsubscribedContacts + : "--"} +
-
-
Unsubscribed
-
- {contactBookDetailQuery.data?.unsubscribedContacts !== undefined - ? contactBookDetailQuery.data?.unsubscribedContacts - : "--"} +
+

Details

+
+
+ Contact book ID +
+ +
+
+
+ Created at +
+
+ {contactBookDetailQuery.data?.createdAt + ? formatDistanceToNow(contactBookDetailQuery.data.createdAt, { + addSuffix: true, + }) + : "--"} +
-
-
Created at
-
- {contactBookDetailQuery.data?.createdAt - ? formatDistanceToNow(contactBookDetailQuery.data.createdAt, { +
+

Recent campaigns

+ {!contactBookDetailQuery.isLoading && + contactBookDetailQuery.data?.campaigns.length === 0 ? ( +
+ No campaigns yet. +
+ ) : null} + {contactBookDetailQuery.data?.campaigns.map((campaign) => ( +
+ +
+ {campaign.name} +
+ +
+ {formatDistanceToNow(campaign.createdAt, { addSuffix: true, - }) - : "--"} -
-
-
-
Contact book id
-
- -
+ })} +
+
+ ))}
diff --git a/apps/web/src/app/(dashboard)/contacts/add-contact-book.tsx b/apps/web/src/app/(dashboard)/contacts/add-contact-book.tsx index faab9ea..7c005ee 100644 --- a/apps/web/src/app/(dashboard)/contacts/add-contact-book.tsx +++ b/apps/web/src/app/(dashboard)/contacts/add-contact-book.tsx @@ -106,7 +106,7 @@ export default function AddContactBook() { />
- - - Name - Contacts - Created at - Actions - - - - {contactBooksQuery.isLoading ? ( - - - - - - ) : contactBooksQuery.data?.length === 0 ? ( - - -

No contact books added

-
-
- ) : ( - contactBooksQuery.data?.map((contactBook) => ( - - - - {contactBook.name} - - - {/* {contactBook.name} */} - {contactBook._count.contacts} - - {formatDistanceToNow(contactBook.createdAt, { - addSuffix: true, - })} - - - - - - - )) - )} -
-
+
+ {contactBooksQuery.data?.map((contactBook) => ( + +
+ +
+
+
{contactBook.emoji}
+
{contactBook.name}
+
+
+ + {contactBook._count.contacts} + {" "} + contacts +
+
+ + +
+
router.push(`/contacts/${contactBook.id}`)} + > + {formatDistanceToNow(contactBook.createdAt, { + addSuffix: true, + })} +
+
+ + +
+
+
+
+ ))}
); diff --git a/apps/web/src/app/(dashboard)/contacts/delete-contact-book.tsx b/apps/web/src/app/(dashboard)/contacts/delete-contact-book.tsx index 7facb15..90695cf 100644 --- a/apps/web/src/app/(dashboard)/contacts/delete-contact-book.tsx +++ b/apps/web/src/app/(dashboard)/contacts/delete-contact-book.tsx @@ -77,8 +77,8 @@ export const DeleteContactBook: React.FC<{ onOpenChange={(_open) => (_open !== open ? setOpen(_open) : null)} > - @@ -103,7 +103,7 @@ export const DeleteContactBook: React.FC<{ name="name" render={({ field, formState }) => ( - name + Contact book name diff --git a/apps/web/src/app/(dashboard)/contacts/edit-contact-book.tsx b/apps/web/src/app/(dashboard)/contacts/edit-contact-book.tsx index cdaf020..94278c5 100644 --- a/apps/web/src/app/(dashboard)/contacts/edit-contact-book.tsx +++ b/apps/web/src/app/(dashboard)/contacts/edit-contact-book.tsx @@ -17,7 +17,6 @@ import { FormLabel, FormMessage, } from "@unsend/ui/src/form"; - import { api } from "~/trpc/react"; import { useState } from "react"; import { Edit } from "lucide-react"; @@ -73,8 +72,13 @@ export const EditContactBook: React.FC<{ onOpenChange={(_open) => (_open !== open ? setOpen(_open) : null)} > - @@ -102,7 +106,7 @@ export const EditContactBook: React.FC<{ />