mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-07 22: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";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
|
import { CalendarIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { GridCard, GridCardHeader } from "@/components/grid-card";
|
||||||
import { GroupPollCard } from "@/components/group-poll-card";
|
import { GroupPollCard } from "@/components/group-poll-card";
|
||||||
import { Subheading } from "@/components/heading";
|
import { Subheading } from "@/components/heading";
|
||||||
import { Spinner } from "@/components/spinner";
|
import { Spinner } from "@/components/spinner";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
|
import { useLocalizeTime } from "@/utils/dayjs";
|
||||||
import { trpc } from "@/utils/trpc/client";
|
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() {
|
export default function Dashboard() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
@ -18,7 +25,7 @@ export default function Dashboard() {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<SectionHeading>
|
||||||
<Subheading>
|
<Subheading>
|
||||||
<Trans i18nKey="pending" defaults="Pending" />
|
<Trans i18nKey="pending" defaults="Pending" />
|
||||||
</Subheading>
|
</Subheading>
|
||||||
|
@ -27,9 +34,22 @@ export default function Dashboard() {
|
||||||
<Trans i18nKey="viewAll" defaults="View All" />
|
<Trans i18nKey="viewAll" defaults="View All" />
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</SectionHeading>
|
||||||
<PendingPolls />
|
<PendingPolls />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -44,7 +64,7 @@ function PendingPolls() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-2 md:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
{data.map((poll) => {
|
{data.map((poll) => {
|
||||||
return (
|
return (
|
||||||
<GroupPollCard
|
<GroupPollCard
|
||||||
|
@ -56,9 +76,49 @@ function PendingPolls() {
|
||||||
responseCount={poll.responseCount}
|
responseCount={poll.responseCount}
|
||||||
dateStart={poll.range.start}
|
dateStart={poll.range.start}
|
||||||
dateEnd={poll.range.end}
|
dateEnd={poll.range.end}
|
||||||
|
timeZone={poll.timeZone ?? undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</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;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
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
|
<div
|
||||||
className={cn(
|
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">
|
<div className="flex w-full items-center justify-between gap-4">
|
||||||
|
@ -40,8 +40,8 @@ export default async function Layout({
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-4">
|
||||||
<Button variant="ghost" asChild>
|
<Button variant="primary" asChild>
|
||||||
<Link href="/new">
|
<Link href="/new">
|
||||||
<Icon>
|
<Icon>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
|
|
|
@ -32,3 +32,10 @@ export default async function Page({ params }: { params: Params }) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: { params: Params }) {
|
||||||
|
const { t } = await getTranslation(params.locale);
|
||||||
|
return {
|
||||||
|
title: t("home"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@ import {
|
||||||
HomeIcon,
|
HomeIcon,
|
||||||
LifeBuoyIcon,
|
LifeBuoyIcon,
|
||||||
LogInIcon,
|
LogInIcon,
|
||||||
PlusIcon,
|
|
||||||
Settings2Icon,
|
Settings2Icon,
|
||||||
SparklesIcon,
|
SparklesIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
|
@ -1,13 +1,20 @@
|
||||||
|
import { cn } from "@rallly/ui";
|
||||||
|
|
||||||
export function GridCardFooter({ children }: React.PropsWithChildren) {
|
export function GridCardFooter({ children }: React.PropsWithChildren) {
|
||||||
return <div className="relative z-10 mt-6">{children}</div>;
|
return <div className="relative z-10 mt-6">{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GridCardHeader({ children }: React.PropsWithChildren) {
|
export function GridCardHeader({
|
||||||
return <div className="mb-4 flex items-center gap-2">{children}</div>;
|
children,
|
||||||
|
className,
|
||||||
|
}: React.PropsWithChildren<{ className?: string }>) {
|
||||||
|
return <div className={cn("mb-4", className)}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GridCard = ({ children }: { children: React.ReactNode }) => {
|
export const GridCard = ({ children }: { children: React.ReactNode }) => {
|
||||||
return (
|
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 { GroupPollIcon } from "@/components/group-poll-icon";
|
||||||
import { Pill, PillList } from "@/components/pill";
|
import { Pill, PillList } from "@/components/pill";
|
||||||
import { PollStatusLabel } from "@/components/poll-status";
|
import { PollStatusLabel } from "@/components/poll-status";
|
||||||
|
import { RandomGradientBar } from "@/components/random-gradient-bar";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { useLocalizeTime } from "@/utils/dayjs";
|
import { useLocalizeTime } from "@/utils/dayjs";
|
||||||
import { getRange } from "@/utils/get-range";
|
import { getRange } from "@/utils/get-range";
|
||||||
|
@ -74,6 +75,7 @@ export function GroupPollCard({
|
||||||
responseCount,
|
responseCount,
|
||||||
dateStart,
|
dateStart,
|
||||||
dateEnd,
|
dateEnd,
|
||||||
|
timeZone,
|
||||||
}: {
|
}: {
|
||||||
pollId: string;
|
pollId: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -82,20 +84,18 @@ export function GroupPollCard({
|
||||||
dateStart: Date;
|
dateStart: Date;
|
||||||
dateEnd: Date;
|
dateEnd: Date;
|
||||||
status: PollStatus;
|
status: PollStatus;
|
||||||
|
timeZone?: string;
|
||||||
}) {
|
}) {
|
||||||
const localizeTime = useLocalizeTime();
|
const localizeTime = useLocalizeTime();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridCard key={pollId}>
|
<GridCard key={pollId}>
|
||||||
<GridCardHeader>
|
<GridCardHeader className="flex items-center gap-2">
|
||||||
<div>
|
<div>
|
||||||
<GroupPollIcon size="xs" />
|
<GroupPollIcon size="xs" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="truncate font-medium">
|
<h3 className="truncate font-medium">
|
||||||
<Link
|
<Link className="hover:underline" href={`/poll/${pollId}`}>
|
||||||
className="hover:underline focus:text-gray-900"
|
|
||||||
href={`/poll/${pollId}`}
|
|
||||||
>
|
|
||||||
{title}
|
{title}
|
||||||
</Link>
|
</Link>
|
||||||
</h3>
|
</h3>
|
||||||
|
@ -109,8 +109,8 @@ export function GroupPollCard({
|
||||||
<CalendarIcon />
|
<CalendarIcon />
|
||||||
</Icon>
|
</Icon>
|
||||||
{getRange(
|
{getRange(
|
||||||
localizeTime(dateStart).toDate(),
|
localizeTime(dateStart, !timeZone).toDate(),
|
||||||
localizeTime(dateEnd).toDate(),
|
localizeTime(dateEnd, !timeZone).toDate(),
|
||||||
)}
|
)}
|
||||||
</Pill>
|
</Pill>
|
||||||
<Pill>
|
<Pill>
|
||||||
|
|
|
@ -7,13 +7,7 @@ export function Heading({
|
||||||
className,
|
className,
|
||||||
}: React.PropsWithChildren<{ className?: string }>) {
|
}: React.PropsWithChildren<{ className?: string }>) {
|
||||||
return (
|
return (
|
||||||
<h1
|
<h1 className={cn("text-2xl font-semibold", heading.className, className)}>
|
||||||
className={cn(
|
|
||||||
"text-2xl font-semibold text-gray-900",
|
|
||||||
heading.className,
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</h1>
|
</h1>
|
||||||
);
|
);
|
||||||
|
@ -24,7 +18,7 @@ export function Subheading({
|
||||||
className,
|
className,
|
||||||
}: React.PropsWithChildren<{ className?: string }>) {
|
}: React.PropsWithChildren<{ className?: string }>) {
|
||||||
return (
|
return (
|
||||||
<h2 className={cn("text-lg font-semibold text-gray-900", className)}>
|
<h2 className={cn("text-lg font-semibold", heading.className, className)}>
|
||||||
{children}
|
{children}
|
||||||
</h2>
|
</h2>
|
||||||
);
|
);
|
||||||
|
|
|
@ -50,7 +50,7 @@ const Layout = ({ children }: React.PropsWithChildren) => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
return (
|
return (
|
||||||
<div>
|
<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">
|
<div className="flex min-w-0 items-center gap-x-2.5">
|
||||||
{pathname === pollLink ? (
|
{pathname === pollLink ? (
|
||||||
<Button variant="ghost" asChild>
|
<Button variant="ghost" asChild>
|
||||||
|
|
|
@ -4,7 +4,7 @@ export function PillList({ children }: React.PropsWithChildren) {
|
||||||
|
|
||||||
export function Pill({ children }: React.PropsWithChildren) {
|
export function Pill({ children }: React.PropsWithChildren) {
|
||||||
return (
|
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}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Trans } from "next-i18next";
|
||||||
|
|
||||||
export const ProBadge = ({ className }: { className?: string }) => {
|
export const ProBadge = ({ className }: { className?: string }) => {
|
||||||
return (
|
return (
|
||||||
<Badge variant="primary" className={className}>
|
<Badge variant="secondary" className={className}>
|
||||||
<Trans i18nKey="planPro" />
|
<Trans i18nKey="planPro" />
|
||||||
</Badge>
|
</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"],
|
subsets: ["latin"],
|
||||||
display: "swap",
|
display: "swap",
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply text-foreground bg-gray-50;
|
@apply text-foreground bg-gray-100;
|
||||||
font-feature-settings:
|
font-feature-settings:
|
||||||
"rlig" 1,
|
"rlig" 1,
|
||||||
"calt" 1;
|
"calt" 1;
|
||||||
|
|
|
@ -29,6 +29,7 @@ export const dashboard = router({
|
||||||
title: true,
|
title: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
status: true,
|
status: true,
|
||||||
|
timeZone: true,
|
||||||
options: {
|
options: {
|
||||||
select: {
|
select: {
|
||||||
startTime: true,
|
startTime: true,
|
||||||
|
@ -58,7 +59,48 @@ export const dashboard = router({
|
||||||
status: poll.status,
|
status: poll.status,
|
||||||
responseCount: poll._count.participants,
|
responseCount: poll._count.participants,
|
||||||
inviteLink: shortUrl(`/invite/${poll.id}`),
|
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 = () => {
|
export const useLocalizeTime = () => {
|
||||||
const { timeZone } = useDayjs();
|
const { timeZone } = useDayjs();
|
||||||
return React.useCallback(
|
return React.useCallback(
|
||||||
(date: Date) => dayjs(date).tz(timeZone),
|
(date: Date, keepLocalTime = false) =>
|
||||||
|
keepLocalTime ? dayjs(date).utc() : dayjs(date).tz(timeZone),
|
||||||
[timeZone],
|
[timeZone],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,6 +9,7 @@ const badgeVariants = cva(
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
primary: "border-transparent bg-primary text-primary-50",
|
primary: "border-transparent bg-primary text-primary-50",
|
||||||
|
secondary: "border-transparent bg-primary-50 text-primary",
|
||||||
default: "bg-gray-50 text-secondary-foreground",
|
default: "bg-gray-50 text-secondary-foreground",
|
||||||
destructive:
|
destructive:
|
||||||
"border-transparent bg-destructive text-destructive-foreground",
|
"border-transparent bg-destructive text-destructive-foreground",
|
||||||
|
|
|
@ -8,26 +8,25 @@ import { cn } from "./lib/utils";
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
cn(
|
cn(
|
||||||
"inline-flex border transition-colors font-medium disabled:pointer-events-none select-none disabled:opacity-50 items-center justify-center whitespace-nowrap border",
|
"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-visible:shadow-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-primary-400",
|
||||||
"focus:shadow-none",
|
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
primary:
|
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:
|
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:
|
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:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
ghost:
|
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",
|
link: "underline-offset-4 border-transparent hover:underline text-primary",
|
||||||
},
|
},
|
||||||
size: {
|
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",
|
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",
|
lg: "h-11 text-base gap-x-3 px-4 rounded-md",
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Reference in a new issue