mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-03 20:26:03 +02:00
uncommit
This commit is contained in:
parent
dc9940b390
commit
331c7fd9ad
16 changed files with 150 additions and 40 deletions
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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"),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
HomeIcon,
|
||||
LifeBuoyIcon,
|
||||
LogInIcon,
|
||||
PlusIcon,
|
||||
Settings2Icon,
|
||||
SparklesIcon,
|
||||
} from "lucide-react";
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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",
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
}));
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -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],
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
|
|
Loading…
Add table
Reference in a new issue