Skip to content

Commit

Permalink
make email list better looking
Browse files Browse the repository at this point in the history
  • Loading branch information
RiskyMH committed Oct 5, 2024
1 parent 0fcbe9b commit 89ebebc
Show file tree
Hide file tree
Showing 13 changed files with 144 additions and 65 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Publish `emailthing` Package to npmjs
name: Publish npm packages
on:
push:
paths: packages/emailthing/package.json
Expand Down
85 changes: 63 additions & 22 deletions app/(email)/mail/[mailbox]/(email-list)/email-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import { cn } from "@/utils/tw";
import { ExternalLink, ForwardIcon, ReplyAllIcon, ReplyIcon, TagIcon } from "lucide-react";
import Link from "next/link";
import { deleteEmail, updateEmail as updateEmailAction } from "../actions";
import { deleteEmail, nothing, updateEmail as updateEmailAction } from "../actions";
import { ClientStar, ContextMenuAction } from "../components.client";
import type { mailboxCategories } from "../tools";
import type { getDraftJustEmailsList, getJustEmailsList } from "./tools";
Expand All @@ -38,52 +38,93 @@ export function EmailItem({ email, mailboxId, type, categories }: EmailItemProps
<Link
href={link}
className={cn(
"inline-flex h-16 gap-4 rounded px-5 py-1.5 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
"group inline-flex h-12 gap-3 rounded-md px-4 py-1.5 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
email.isRead ? "hover:bg-card/60" : "bg-card text-card-foreground shadow-sm hover:bg-card/60",
)}
>
{/* //todo: use the icon here, and find some other place for category */}
<TooltipText text={category?.name ?? "No category"}>
<span
className="m-2 mx-auto inline-block size-3 shrink-0 self-center rounded-full"
className="inline-block size-4 shrink-0 self-center rounded-full"
style={{
backgroundColor: category?.color ?? "grey",
}}
/>
</TooltipText>

<TooltipText
<ClientStar
enabled={!!email.isStarred}
// todo: drafts can be starred
action={type !== "drafts" ? updateEmail.bind(null, { isStarred: !email.isStarred }) : nothing}
className="hidden shrink-0 self-center text-muted-foreground hover:text-foreground sm:inline-block"
/>

{/* <TooltipText
text={email.from?.name || email.from?.address || "There should be an email here"}
subtext={email.from?.name && email.from?.address ? `(${email.from?.address})` : ""}
> */}
<span
className={cn(
"w-1/4 shrink-0 self-center truncate text-sm max-sm:block sm:w-32 md:w-56",
!email.isRead ? "font-bold" : "text-foreground/80",
)}
title={email.from?.address ?? "uh?"}
>
<span className="w-56 self-center truncate">{email.from?.name || email.from?.address}</span>
</TooltipText>
{email.from?.name || email.from?.address}
</span>
{/* </TooltipText> */}

<TooltipText text={email.subject || "No subject was provided"}>
<span className={cn("w-80 self-center truncate font-bold", !email.subject && "italic")}>
{email.subject || "(no subject)"}
</span>
</TooltipText>
{/* <TooltipText text={email.subject || "No subject was provided"}> */}
<span
className={cn(
"self-center truncate text-sm",
!email.subject && "italic",
!email.isRead ? "font-bold" : "text-foreground/80",
)}
title={email.subject || "No subject was provided"}
>
{email.subject || "(no subject)"}
</span>
{/* </TooltipText> */}

<span className="hidden w-full shrink-[2] gap-4 self-center sm:inline-flex">
{/* <span className="hidden w-full shrink-[2] gap-4 self-center sm:inline-flex">
{!email.isRead && (
<span className="inline h-6 select-none self-center rounded bg-red px-3 py-1 font-bold text-white text-xs">
NEW
</span>
)}
<span className="line-clamp-2 break-all text-muted-foreground text-sm">{email.snippet}</span>
</span>
<ClientStar
enabled={!!email.isStarred}
action={updateEmail.bind(null, {
isStarred: !email.isStarred,
})}
className="-me-2 ms-auto hidden shrink-0 self-center text-muted-foreground hover:text-foreground sm:inline-block"
/>
</span> */}
<LocalTime
type="hour-min/date"
type="smart"
time={email.createdAt}
className="float-right w-16 shrink-0 self-center text-right text-muted-foreground text-sm"
className="float-right ms-auto w-auto shrink-0 self-center text-right text-muted-foreground text-xs group-hover:sm:hidden"
/>
<div className="float-right ms-auto me-1.5 hidden w-auto shrink-0 gap-4 self-center text-right text-muted-foreground text-xs group-hover:sm:flex">
<ContextMenuAction
icon={email.isRead ? "BellDotIcon" : "MailOpenIcon"}
action={updateEmail.bind(null, {
isRead: !email.isRead,
})}
tooltip={email.isRead ? "Mark as unread" : "Mark as read"}
size="small"
/>
{!["drafts", "temp"].includes(type) ? (
<ContextMenuAction
icon={!email.binnedAt ? "Trash2Icon" : "ArchiveRestoreIcon"}
action={updateEmail.bind(null, { binned: !email.binnedAt })}
tooltip={!email.binnedAt ? "Delete" : "Restore to inbox"}
size="small"
/>
) : (
<ContextMenuAction
icon="Trash2Icon"
action={deleteEmail.bind(null, mailboxId, emailId, type)}
tooltip="Delete forever"
size="small"
/>
)}
</div>
</Link>
</ContextMenuTrigger>
<ContextMenuContent>
Expand Down
22 changes: 11 additions & 11 deletions app/(email)/mail/[mailbox]/(email-list)/email-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default async function EmailList({
}: EmailListProps) {
const baseUrl = `/mail/${mailboxId}${type === "inbox" ? "" : `/${type}`}`;

const take = initialTake ? Number.parseInt(initialTake) : 10;
const take = initialTake ? Number.parseInt(initialTake) : 25;
const emailFetchOptions = {
isBinned: type === "trash",
isSender: type === "sent",
Expand Down Expand Up @@ -80,18 +80,18 @@ export default async function EmailList({
{
...emailFetchOptions,
selectCategories: false,
take: 10,
take: 25,
},
curser,
)
: await getDraftJustEmailsList(mailboxId, { take: 10, search }, curser);
: await getDraftJustEmailsList(mailboxId, { take: 25, search }, curser);

if (emails.length === 0) {
console.error("No more emails");
return [[], null] as [JSX.Element[], null];
}

const nextPageEmail = emails.length >= 11 ? emails.pop() : null;
const nextPageEmail = emails.length >= 26 ? emails.pop() : null;
const categories = type === "temp" ? undefined : await mailboxCategories(mailboxId);

return [
Expand All @@ -109,10 +109,10 @@ export default async function EmailList({

return (
<>
<div className="flex w-full min-w-0 flex-col gap-2 p-5">
<div className="-mt-4 sticky top-0 z-10 flex w-full min-w-0 flex-row gap-6 overflow-y-auto border-b-2 bg-background pt-3 pb-3">
<input type="checkbox" disabled id="select" className="mt-1 mr-2 size-4 shrink-0 self-start" />
<div className="-mb-3 flex w-full min-w-0 flex-row gap-6 overflow-y-auto pb-3">
<div className="flex w-full min-w-0 flex-col gap-2 p-5 px-3 pt-0">
<div className="overflow sticky top-0 z-10 flex h-12 w-full min-w-0 flex-row items-center justify-center gap-3 overflow-y-hidden border-b-2 bg-background px-2">
<input type="checkbox" disabled id="select" className="my-auto mr-2 size-4 shrink-0 self-start" />
<div className="flex h-6 w-full min-w-0 flex-row gap-6 overflow-y-hidden">
<CategoryItem
circleColor={null}
name={type === "drafts" ? "Drafts" : search ? "Search results" : "All"}
Expand Down Expand Up @@ -153,8 +153,8 @@ export default async function EmailList({
</SmartDrawerContent>
</SmartDrawer>
)}
<div className="ms-auto me-2 shrink-0">
<RefreshButton />
<div className="ms-auto flex h-6 shrink-0 items-center justify-center">
<RefreshButton className="shrink-0" />
</div>
</div>
{type === "trash" && (
Expand Down Expand Up @@ -245,7 +245,7 @@ export function CategoryItem({
)}
>
{circleColor && <div className="mr-1 size-2.5 rounded-full" style={{ backgroundColor: circleColor }} />}
<span className="font-medium group-hover:text-muted-foreground">{name}</span>
<span className="font-medium text-base group-hover:text-muted-foreground">{name}</span>
<span className="text-muted-foreground text-sm group-hover:text-muted-foreground/50">({count})</span>
</Link>
);
Expand Down
38 changes: 23 additions & 15 deletions app/(email)/mail/[mailbox]/(email-list)/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
import { StarIcon } from "lucide-react";

export default function Loading() {
return (
<div className="flex w-full min-w-0 flex-col gap-2 p-5">
<div className="-mt-1 flex w-full min-w-0 animate-pulse flex-row gap-6 border-b-2 pb-3">
<input type="checkbox" disabled id="select" className="mt-1 mr-2 size-4 self-start" />
<div className="inline-flex w-auto max-w-fit items-center gap-1 border-transparent border-b-3 px-1 font-bold">
<span className="h-6 w-36 self-center rounded bg-muted-foreground/25" />
<div className="flex w-full min-w-0 flex-col gap-2 p-5 px-3 pt-0">
<div className="sticky top-0 z-10 flex h-12 w-full min-w-0 animate-pulse flex-row items-center justify-center gap-3 border-b-2 bg-background px-2">
<input type="checkbox" disabled id="select" className="my-auto mr-2 size-4 self-start" />
<div className="inline-flex h-6 w-auto max-w-fit items-center justify-center gap-1 border-transparent border-b-3 px-1 font-bold">
<span className="h-5 w-36 self-center rounded bg-muted-foreground/25" />
</div>

<div className="ms-auto me-2 h-5">
<span className="inline-block size-5 rounded-full bg-muted-foreground/25 p-2" />
<div className="ms-auto me-2 flex h-6 shrink-0 items-center justify-center">
<span className="-m-2 size-5 rounded-full bg-muted-foreground/25 p-2" />
</div>
</div>

{Array.from({ length: 10 }).map((_, i) => (
<div key={i} className="flex h-16 w-full animate-pulse gap-4 rounded bg-card py-2 pr-5 pl-5 shadow-sm">
{Array.from({ length: 25 }).map((_, i) => (
<div
key={i}
className="//bg-card flex h-12 w-full animate-pulse gap-3 rounded-md px-4 py-1.5 shadow-sm"
>
<span
className="m-2 mx-auto inline-block size-3 shrink-0 self-center rounded-full"
className="m-2 mx-auto inline-block size-4 shrink-0 self-center rounded-full"
style={{ backgroundColor: "grey" }}
/>
<span className="h-5 w-56 self-center rounded bg-muted-foreground/50" />
<span className="h-5 w-80 self-center rounded bg-muted-foreground/50" />
<span className="hidden h-7 w-full shrink-[2] self-center rounded-lg bg-muted-foreground/25 sm:flex" />
<span className="-me-2 mx-auto hidden size-5 shrink-0 self-center truncate rounded-full bg-muted-foreground/25 sm:inline-block" />
<span className="float-right h-4 w-16 shrink-0 self-center rounded bg-muted-foreground/25 text-right text-sm" />
<StarIcon className="hidden size-4 shrink-0 self-center text-muted-foreground sm:inline-block" />
<div className="h-4 w-1/4 shrink-0 self-center sm:w-32 md:w-56">
<span className="me-auto block h-4 w-full max-w-20 rounded bg-muted-foreground/50" />
</div>
<div className="h-4 w-full self-center ">
<span className="me-auto block h-4 w-full max-w-56 rounded bg-muted-foreground/50 md:max-w-72 lg:max-w-[30rem]" />
</div>
<span className="float-right h-4 w-10 shrink-0 self-center rounded bg-muted-foreground/25 text-right text-sm sm:w-16" />
</div>
))}
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/(email)/mail/[mailbox]/[email]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export default async function EmailPage({
<TopButtons mailboxId={params.mailbox} emailId={params.email} />
{!email.isRead && <MarkRead action={markRead} />}

<h1 className="mt-3 break-words font-bold text-3xl">{email.subject}</h1>
<h1 className="mt-3 break-words font-bold text-2xl sm:text-3xl">{email.subject}</h1>
<div className="flex flex-col gap-3 rounded-md bg-card p-3">
{/* from info and gravatar */}
<div className="flex gap-2">
Expand Down
2 changes: 2 additions & 0 deletions app/(email)/mail/[mailbox]/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,5 @@ export async function deleteEmail(mailboxId: string, emailId: string, type: Emai

return revalidatePath(baseUrl);
}

export async function nothing() {}
22 changes: 16 additions & 6 deletions app/(email)/mail/[mailbox]/components.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,16 @@ export function ClientStar({
size="auto"
onClick={onClick as any}
aria-disabled={isPending}
className={cn(className, "rounded-full ring-offset-5 hover:bg-transparent", enabled && "text-blue/80")}
className={cn(
className,
"rounded-full ring-offset-5 hover:bg-transparent hover:text-amber-500",
enabled && "text-amber-500 hover:text-foreground",
)}
>
{isPending ? (
<Loader2 className="size-5 animate-spin text-muted-foreground" />
<Loader2 className="size-4 animate-spin text-muted-foreground" />
) : (
<StarIcon fill={enabled ? "currentColor" : "transparent"} className="size-5" />
<StarIcon fill={enabled ? "currentColor" : "transparent"} className="size-4" />
)}
</Button>
);
Expand All @@ -68,6 +72,7 @@ interface ContextMenuActionProps {
icon: keyof typeof iconMap | "EmptyIcon";
fillIcon?: boolean | null;
tooltip?: string;
size?: "small" | "normal";
}

export function ContextMenuAction({
Expand All @@ -76,6 +81,7 @@ export function ContextMenuAction({
icon,
fillIcon,
tooltip,
size,
...props
}: PropsWithChildren<ContextMenuActionProps>) {
const Icon: LucideIcon | null = iconMap[icon] ?? null;
Expand All @@ -93,12 +99,16 @@ export function ContextMenuAction({
<button {...props} onClick={onClick}>
{Icon && !isPending && (
<Icon
className={cn("size-5", children && "text-muted-foreground")}
className={cn("size-5", size === "small" && "size-4", children && "text-muted-foreground")}
fill={fillIcon ? "currentColor" : "transparent"}
/>
)}
{icon === "EmptyIcon" && !isPending && <EmptyIcon className="size-5 text-muted-foreground" />}
{isPending && <Loader2 className="size-5 animate-spin text-muted-foreground" />}
{icon === "EmptyIcon" && !isPending && (
<EmptyIcon className={cn("size-5 text-muted-foreground", size === "small" && "size-4")} />
)}
{isPending && (
<Loader2 className={cn("size-5 animate-spin text-muted-foreground", size === "small" && "size-4")} />
)}
{children}
</button>
);
Expand Down
5 changes: 4 additions & 1 deletion app/(emailthing.me)/emailme/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ export const metadata = {
export default function DocsLayout({ children }: DocsLayoutProps) {
return (
<>
<Header className="container sticky top-0 z-40 flex h-20 w-full items-center justify-between bg-background py-6 transition-[height]" vaul-drawer-wrapper="">
<Header
className="container sticky top-0 z-40 flex h-20 w-full items-center justify-between bg-background py-6 transition-[height]"
vaul-drawer-wrapper=""
>
<div className="flex gap-6 md:gap-10">
<Link href="/" className="group flex items-center gap-1">
<Logo className="flex size-7 shrink-0" />
Expand Down
2 changes: 1 addition & 1 deletion app/(home)/home/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default async function Home() {
{"n "}
<span className="inline-block whitespace-nowrap bg-gradient-to-br from-[#FF9797] to-[#6D6AFF] bg-clip-text text-transparent">
email app
</span>
</span>{" "}
where you can receive and send emails!
</h1>
{/* eslint-disable-next-line @next/next/no-img-element */}
Expand Down
15 changes: 9 additions & 6 deletions app/components/loadmore.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,14 @@ const LoadMore = <T extends string | number | Record<string, any> = any>({

const element = ref.current;

const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting && element?.disabled === false) {
loadMore(signal);
}
});
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && element?.disabled === false) {
loadMore(signal);
}
},
{ rootMargin: "150px" },
);

if (element) {
observer.observe(element);
Expand All @@ -73,7 +76,7 @@ const LoadMore = <T extends string | number | Record<string, any> = any>({

useEffect(() => {
const num = loadMoreNodes.length + initialLength;
if (num === 0 || (initialLength === 10 && loadMoreNodes.length === 0)) return;
if (num === 0 || (initialLength === 25 && loadMoreNodes.length === 0)) return;

const urlparams = new URLSearchParams(window.location.search);
urlparams.set("take", num.toString());
Expand Down
Loading

0 comments on commit 89ebebc

Please sign in to comment.