This commit is contained in:
Luke Vella 2024-09-30 08:51:05 +01:00
parent dc9940b390
commit 331c7fd9ad
No known key found for this signature in database
GPG key ID: 469CAD687F0D784C
16 changed files with 150 additions and 40 deletions

View file

@ -1,14 +1,21 @@
"use client";
import { Button } from "@rallly/ui/button";
import { CalendarIcon } from "lucide-react";
import Link from "next/link";
import { GridCard, GridCardHeader } from "@/components/grid-card";
import { GroupPollCard } from "@/components/group-poll-card";
import { Subheading } from "@/components/heading";
import { Spinner } from "@/components/spinner";
import { Trans } from "@/components/trans";
import { useLocalizeTime } from "@/utils/dayjs";
import { trpc } from "@/utils/trpc/client";
const SectionHeading = ({ children }: React.PropsWithChildren) => {
return <div className="flex items-center justify-between">{children}</div>;
};
export default function Dashboard() {
return (
<div className="space-y-6">
@ -18,7 +25,7 @@ export default function Dashboard() {
</Button>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<SectionHeading>
<Subheading>
<Trans i18nKey="pending" defaults="Pending" />
</Subheading>
@ -27,9 +34,22 @@ export default function Dashboard() {
<Trans i18nKey="viewAll" defaults="View All" />
</Link>
</Button>
</div>
</SectionHeading>
<PendingPolls />
</div>
<div className="space-y-4">
<SectionHeading>
<Subheading>
<Trans i18nKey="upcoming" defaults="Upcoming" />
</Subheading>
<Button asChild>
<Link href="/events">
<Trans i18nKey="viewAll" defaults="View All" />
</Link>
</Button>
</SectionHeading>
<UpcomingEvents />
</div>
</div>
);
}
@ -44,7 +64,7 @@ function PendingPolls() {
}
return (
<div className="grid gap-2 md:grid-cols-3">
<div className="grid gap-4 md:grid-cols-3">
{data.map((poll) => {
return (
<GroupPollCard
@ -56,9 +76,49 @@ function PendingPolls() {
responseCount={poll.responseCount}
dateStart={poll.range.start}
dateEnd={poll.range.end}
timeZone={poll.timeZone ?? undefined}
/>
);
})}
</div>
);
}
function UpcomingEvents() {
const { data } = trpc.dashboard.getUpcoming.useQuery(undefined, {
suspense: true,
});
const localizeTime = useLocalizeTime();
if (!data) {
return <Spinner />;
}
return (
<div className="grid gap-4 md:grid-cols-3">
{data.map((event) => {
return (
<GridCard key={event.id}>
<GridCardHeader className="flex gap-2">
<div>
<div className="bg-primary-600 text-primary-100 inline-flex items-center justify-center rounded-md p-1.5">
<CalendarIcon className="size-4" />
</div>
</div>
<div className="min-w-0">
<h3 className="truncate font-semibold">{event.title}</h3>
<time
className="text-muted-foreground whitespace-nowrap"
dateTime={event.start.toISOString()}
>
{localizeTime(event.start, !event.timeZone).format("DD MMM")}
</time>
</div>
</GridCardHeader>
</GridCard>
);
})}
</div>
);
}

View file

@ -17,10 +17,10 @@ export default async function Layout({
children: React.ReactNode;
}) {
return (
<div className="flex h-screen flex-col bg-gray-50 pb-16 md:pb-0">
<div className="flex h-screen flex-col pb-16 md:pb-0">
<div
className={cn(
"fixed inset-y-0 z-50 hidden w-72 shrink-0 flex-col gap-y-6 overflow-y-auto p-6 lg:flex",
"fixed inset-y-0 z-50 hidden w-72 shrink-0 flex-col gap-y-10 overflow-y-auto p-6 lg:flex",
)}
>
<div className="flex w-full items-center justify-between gap-4">
@ -40,8 +40,8 @@ export default async function Layout({
</Button>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" asChild>
<div className="flex items-center gap-4">
<Button variant="primary" asChild>
<Link href="/new">
<Icon>
<PlusIcon />

View file

@ -32,3 +32,10 @@ export default async function Page({ params }: { params: Params }) {
</div>
);
}
export async function generateMetadata({ params }: { params: Params }) {
const { t } = await getTranslation(params.locale);
return {
title: t("home"),
};
}

View file

@ -12,7 +12,6 @@ import {
HomeIcon,
LifeBuoyIcon,
LogInIcon,
PlusIcon,
Settings2Icon,
SparklesIcon,
} from "lucide-react";

View file

@ -1,13 +1,20 @@
import { cn } from "@rallly/ui";
export function GridCardFooter({ children }: React.PropsWithChildren) {
return <div className="relative z-10 mt-6">{children}</div>;
}
export function GridCardHeader({ children }: React.PropsWithChildren) {
return <div className="mb-4 flex items-center gap-2">{children}</div>;
export function GridCardHeader({
children,
className,
}: React.PropsWithChildren<{ className?: string }>) {
return <div className={cn("mb-4", className)}>{children}</div>;
}
export const GridCard = ({ children }: { children: React.ReactNode }) => {
return (
<div className="relative rounded-xl border bg-white p-4">{children}</div>
<div className="relative rounded-lg border bg-white p-4 shadow-sm">
{children}
</div>
);
};

View file

@ -23,6 +23,7 @@ import {
import { GroupPollIcon } from "@/components/group-poll-icon";
import { Pill, PillList } from "@/components/pill";
import { PollStatusLabel } from "@/components/poll-status";
import { RandomGradientBar } from "@/components/random-gradient-bar";
import { Trans } from "@/components/trans";
import { useLocalizeTime } from "@/utils/dayjs";
import { getRange } from "@/utils/get-range";
@ -74,6 +75,7 @@ export function GroupPollCard({
responseCount,
dateStart,
dateEnd,
timeZone,
}: {
pollId: string;
title: string;
@ -82,20 +84,18 @@ export function GroupPollCard({
dateStart: Date;
dateEnd: Date;
status: PollStatus;
timeZone?: string;
}) {
const localizeTime = useLocalizeTime();
return (
<GridCard key={pollId}>
<GridCardHeader>
<GridCardHeader className="flex items-center gap-2">
<div>
<GroupPollIcon size="xs" />
</div>
<h3 className="truncate font-medium">
<Link
className="hover:underline focus:text-gray-900"
href={`/poll/${pollId}`}
>
<Link className="hover:underline" href={`/poll/${pollId}`}>
{title}
</Link>
</h3>
@ -109,8 +109,8 @@ export function GroupPollCard({
<CalendarIcon />
</Icon>
{getRange(
localizeTime(dateStart).toDate(),
localizeTime(dateEnd).toDate(),
localizeTime(dateStart, !timeZone).toDate(),
localizeTime(dateEnd, !timeZone).toDate(),
)}
</Pill>
<Pill>

View file

@ -7,13 +7,7 @@ export function Heading({
className,
}: React.PropsWithChildren<{ className?: string }>) {
return (
<h1
className={cn(
"text-2xl font-semibold text-gray-900",
heading.className,
className,
)}
>
<h1 className={cn("text-2xl font-semibold", heading.className, className)}>
{children}
</h1>
);
@ -24,7 +18,7 @@ export function Subheading({
className,
}: React.PropsWithChildren<{ className?: string }>) {
return (
<h2 className={cn("text-lg font-semibold text-gray-900", className)}>
<h2 className={cn("text-lg font-semibold", heading.className, className)}>
{children}
</h2>
);

View file

@ -50,7 +50,7 @@ const Layout = ({ children }: React.PropsWithChildren) => {
const pathname = usePathname();
return (
<div>
<div className="sticky top-0 z-30 flex flex-col justify-between gap-x-4 gap-y-2.5 border-b bg-gray-100 p-3 sm:flex-row lg:items-center lg:px-5">
<div className="sticky top-0 z-30 flex flex-col justify-between gap-x-4 gap-y-2.5 p-3 sm:flex-row lg:items-center lg:px-5">
<div className="flex min-w-0 items-center gap-x-2.5">
{pathname === pollLink ? (
<Button variant="ghost" asChild>

View file

@ -4,7 +4,7 @@ export function PillList({ children }: React.PropsWithChildren) {
export function Pill({ children }: React.PropsWithChildren) {
return (
<span className="text-muted-foreground inline-flex items-center gap-2 rounded-md border px-1.5 py-0.5 text-sm">
<span className="text-muted-foreground inline-flex items-center gap-2 rounded-md border bg-gray-50 px-1.5 py-0.5 text-sm">
{children}
</span>
);

View file

@ -3,7 +3,7 @@ import { Trans } from "next-i18next";
export const ProBadge = ({ className }: { className?: string }) => {
return (
<Badge variant="primary" className={className}>
<Badge variant="secondary" className={className}>
<Trans i18nKey="planPro" />
</Badge>
);

View file

@ -1,6 +1,6 @@
import { Archivo } from "next/font/google";
import { Inter_Tight } from "next/font/google";
export const heading = Archivo({
export const heading = Inter_Tight({
subsets: ["latin"],
display: "swap",
});

View file

@ -7,7 +7,7 @@
@apply border-border;
}
body {
@apply text-foreground bg-gray-50;
@apply text-foreground bg-gray-100;
font-feature-settings:
"rlig" 1,
"calt" 1;

View file

@ -29,6 +29,7 @@ export const dashboard = router({
title: true,
createdAt: true,
status: true,
timeZone: true,
options: {
select: {
startTime: true,
@ -58,7 +59,48 @@ export const dashboard = router({
status: poll.status,
responseCount: poll._count.participants,
inviteLink: shortUrl(`/invite/${poll.id}`),
timeZone: poll.timeZone,
};
});
}),
getUpcoming: possiblyPublicProcedure.query(async ({ ctx }) => {
const events = await prisma.event.findMany({
select: {
id: true,
title: true,
start: true,
duration: true,
poll: {
select: {
timeZone: true,
location: true,
participants: {
include: {
_count: true,
},
},
},
},
},
where: {
userId: ctx.user.id,
start: { gte: new Date() },
},
orderBy: [
{
start: "desc",
},
{
title: "asc",
},
],
take: 3,
});
return events.map(({ poll, ...event }) => ({
...event,
timeZone: poll?.timeZone || null,
location: poll?.location || null,
}));
}),
});

View file

@ -198,7 +198,8 @@ export const useDayjs = () => {
export const useLocalizeTime = () => {
const { timeZone } = useDayjs();
return React.useCallback(
(date: Date) => dayjs(date).tz(timeZone),
(date: Date, keepLocalTime = false) =>
keepLocalTime ? dayjs(date).utc() : dayjs(date).tz(timeZone),
[timeZone],
);
};

View file

@ -9,6 +9,7 @@ const badgeVariants = cva(
variants: {
variant: {
primary: "border-transparent bg-primary text-primary-50",
secondary: "border-transparent bg-primary-50 text-primary",
default: "bg-gray-50 text-secondary-foreground",
destructive:
"border-transparent bg-destructive text-destructive-foreground",

View file

@ -8,26 +8,25 @@ import { cn } from "./lib/utils";
const buttonVariants = cva(
cn(
"inline-flex border transition-colors font-medium disabled:pointer-events-none select-none disabled:opacity-50 items-center justify-center whitespace-nowrap border",
"focus-visible:ring-offset-input-background",
"focus:shadow-none",
"focus-visible:shadow-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-primary-400",
),
{
variants: {
variant: {
primary:
"border-primary-700 bg-primary disabled:bg-gray-400 disabled:border-transparent text-primary-foreground shadow-sm focus:bg-primary-500",
"bg-primary disabled:bg-gray-400 disabled:border-transparent text-primary-foreground hover:bg-primary-700 active:bg-primary-800 shadow-sm",
destructive:
"bg-destructive shadow-sm text-destructive-foreground focus-visible:ring-offset-1 active:bg-destructive border-destructive hover:bg-destructive/90",
"bg-destructive shadow-sm text-destructive-foreground active:bg-destructive border-destructive hover:bg-destructive/90",
default:
"ring-1 ring-inset ring-white/25 data-[state=open]:bg-gray-100 focus:border-gray-300 focus:bg-gray-200 hover:bg-gray-100 bg-gray-50",
"data-[state=open]:bg-gray-100 hover:bg-gray-100 active:bg-gray-200 bg-gray-50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"border-transparent bg-transparent text-gray-800 hover:bg-gray-100 focus:bg-gray-200",
"border-transparent bg-transparent bg-gray-50 text-gray-800 hover:bg-gray-100 active:bg-gray-200",
link: "underline-offset-4 border-transparent hover:underline text-primary",
},
size: {
default: "h-9 px-2.5 pr-3 gap-x-2 text-sm rounded-md",
default: "h-8 px-2 gap-x-1.5 text-sm rounded-md",
sm: "h-7 text-sm px-1.5 gap-x-1.5 rounded-md",
lg: "h-11 text-base gap-x-3 px-4 rounded-md",
},