This commit is contained in:
Luke Vella 2024-10-04 17:43:33 +01:00
parent 9928260436
commit 4c61e70506
No known key found for this signature in database
GPG key ID: 469CAD687F0D784C
21 changed files with 241 additions and 109 deletions

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="Zoom" role="img"
viewBox="0 0 512 512"><rect
width="512" height="512"
rx="15%"
fill="#2D8CFF"/><path fill="#ffffff" d="M428 357c8 2 15-2 19-8 2-3 2-8 2-19V179c0-11 0-15-2-19-3-8-11-11-19-8-21 14-67 55-68 72-.8 3-.8 8-.8 15v38c0 8 0 11 .8 15 1 8 4 15 8 19 12 9 52 45 61 45zM64 187c0-15 0-23 3-27 2-4 8-8 11-11 4-3 11-3 27-3h129c38 0 57 0 72 8 11 8 23 15 30 30 8 15 8 34 8 72v68c0 15 0 23-3 27-2 4-8 8-11 11-4 3-11 3-27 3H174c-38 0-57 0-72-8-11-8-23-15-30-30-8-15-8-34-8-72z"/></svg>

After

Width:  |  Height:  |  Size: 652 B

View file

@ -0,0 +1,48 @@
import { cn } from "@rallly/ui";
import { cva, VariantProps } from "class-variance-authority";
import { PlusIcon } from "lucide-react";
import Link from "next/link";
const variants = cva(
"inline-flex items-start w-48 justify-between gap-2 rounded-lg p-3 text-sm font-medium",
{
variants: {
variant: {
purple:
"bg-purple-50 text-purple-600 hover:bg-purple-100 active:bg-purple-200",
indigo:
"bg-indigo-50 text-indigo-600 hover:bg-indigo-100 active:bg-indigo-200",
pink: "bg-pink-50 text-pink-600 hover:bg-pink-100 active:bg-pink-200",
violet:
"bg-violet-50 text-violet-600 hover:bg-violet-100 active:bg-violet-200",
},
},
defaultVariants: {
variant: "indigo",
},
},
);
export function CreateButton({
href,
icon,
label,
variant,
}: {
href: string;
icon: React.ReactNode;
label: React.ReactNode;
variant?: VariantProps<typeof variants>["variant"];
}) {
return (
<Link href={href} className={cn(variants({ variant }))}>
<span className="flex flex-col gap-4">
{icon}
{label}
</span>
<span>
<PlusIcon className="size-5" />
</span>
</Link>
);
}

View file

@ -1,9 +1,18 @@
"use client";
import { Button } from "@rallly/ui/button";
import { CalendarIcon } from "lucide-react";
import { Icon } from "@rallly/ui/icon";
import {
BarChart2Icon,
BookIcon,
CalendarIcon,
PlusIcon,
TableIcon,
UserIcon,
} from "lucide-react";
import Link from "next/link";
import { CreateButton } from "@/app/[locale]/(admin)/create-button";
import { GridCard, GridCardHeader } from "@/components/grid-card";
import { GroupPollCard } from "@/components/group-poll-card";
import { Subheading } from "@/components/heading";
@ -19,6 +28,37 @@ const SectionHeading = ({ children }: React.PropsWithChildren) => {
export default function Dashboard() {
return (
<div className="space-y-6">
<div className="space-y-4">
<Subheading>
<Trans i18nKey="create" defaults="Create" />
</Subheading>
<div className="scrollbar-none -mx-6 flex gap-2 overflow-x-auto whitespace-nowrap px-6">
<CreateButton
variant="purple"
href="/new"
icon={<BarChart2Icon className="size-5" />}
label={<Trans i18nKey="groupPoll" defaults="Group Poll" />}
/>
<CreateButton
variant="violet"
href="/new"
icon={<UserIcon className="size-5" />}
label={<Trans i18nKey="oneOnOne" defaults="One-on-One" />}
/>
<CreateButton
variant="indigo"
href="/new"
icon={<BookIcon className="size-5" />}
label={<Trans i18nKey="bookingPage" defaults="Booking Page" />}
/>
<CreateButton
variant="pink"
href="/new"
icon={<TableIcon className="size-5" />}
label={<Trans i18nKey="signUpSheet" defaults="Sign Up Sheet" />}
/>
</div>
</div>
<div className="space-y-4">
<SectionHeading>
<Subheading>
@ -54,12 +94,8 @@ function PendingPolls() {
suspense: true,
});
if (!data) {
return <Spinner />;
}
return (
<div className="grid gap-4 md:grid-cols-2">
<div className="grid gap-2 md:grid-cols-3">
{data.map((poll) => {
return (
<GroupPollCard
@ -90,20 +126,23 @@ function UpcomingEvents() {
return <Spinner />;
}
if (data.length === 0) {
return (
<div className="text-muted-foreground">
<Trans i18nKey="noUpcomingEvents" defaults="No upcoming events" />
</div>
);
}
return (
<div className="grid gap-4 md:grid-cols-2">
{data.length === 0 ? (
<div className="text-muted-foreground">
<Trans i18nKey="noUpcomingEvents" defaults="No upcoming events" />
</div>
) : null}
<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" />
<CalendarIcon className="size-5" />
</div>
</div>
<div className="min-w-0">

View file

@ -30,15 +30,18 @@ export default async function Layout({
<Sidebar />
</div>
<div className={cn("pb-16 lg:min-w-0 lg:pb-0 lg:pl-72")}>
<div className="mx-auto max-w-7xl p-6 xl:pr-12">
<div className="mx-auto max-w-7xl p-3 sm:p-6 xl:pr-12">
<div className="mb-6 flex justify-end gap-2">
<div className="flex items-center gap-4">
<Button variant="primary" asChild>
<Button
className="hidden sm:inline-flex"
variant="primary"
asChild
>
<Link href="/new">
<Icon>
<PlusIcon />
</Icon>
Create
</Link>
</Button>
<UserDropdown />

View file

@ -1,13 +1,12 @@
import { HomeIcon } from "lucide-react";
import { Trans } from "react-i18next/TransWithoutContext";
import Dashboard from "@/app/[locale]/(admin)/dashboard";
import WelcomeMessage from "@/app/[locale]/(admin)/welcome-message";
import { Params } from "@/app/[locale]/types";
import {
PageContainer,
PageContent,
PageHeader,
PageIcon,
PageTitle,
} from "@/app/components/page-layout";
import { getTranslation } from "@/app/i18n";
@ -17,13 +16,13 @@ export default async function Page({ params }: { params: Params }) {
return (
<div>
<PageContainer>
<PageHeader>
<PageIcon>
<HomeIcon />
</PageIcon>
<PageHeader className="space-y-2">
<PageTitle>
<Trans t={t} i18nKey="home" defaults="Home" />
</PageTitle>
<p className="text-muted-foreground">
<WelcomeMessage />
</p>
</PageHeader>
<PageContent>
<Dashboard />

View file

@ -59,7 +59,7 @@ function FilteredPolls({ status }: { status: PollStatus }) {
<ol className="space-y-4">
{data.pages.map((page, i) => (
<li key={i}>
<div className="grid gap-4 md:grid-cols-2">
<div className="grid gap-2 sm:gap-4 md:grid-cols-3">
{page.polls.map((poll) => (
<GroupPollCard
key={poll.id}
@ -141,7 +141,7 @@ export function UserPolls() {
const parsedPollStatus = pollStatusSchema.parse(pollStatus);
return (
<div className="space-y-4">
<div className="space-y-6">
<PollStatusMenu
status={parsedPollStatus}
onStatusChange={setPollStatus}

View file

@ -22,10 +22,8 @@ export default async function ProfileLayout({
<PageHeader>
<PageTitle>{t("settings")}</PageTitle>
</PageHeader>
<PageContent className="space-y-3 sm:space-y-6">
<div className="scrollbar-none -mx-3 overflow-auto px-3 sm:mx-0 sm:px-0">
<SettingsMenu />
</div>
<PageContent className="space-y-6">
<SettingsMenu />
<div>{children}</div>
</PageContent>
</PageContainer>

View file

@ -1,6 +1,5 @@
"use client";
import { Icon } from "@rallly/ui/icon";
import { CreditCardIcon, Settings2Icon, UserIcon } from "lucide-react";
import { Trans } from "react-i18next";
@ -11,22 +10,16 @@ export function SettingsMenu() {
return (
<TabMenu>
<TabMenuItem href="/settings/profile">
<Icon>
<UserIcon />
</Icon>
<UserIcon className="size-4" />
<Trans i18nKey="profile" />
</TabMenuItem>
<TabMenuItem href="/settings/preferences">
<Icon>
<Settings2Icon />
</Icon>
<Settings2Icon className="size-4" />
<Trans i18nKey="preferences" />
</TabMenuItem>
<IfCloudHosted>
<TabMenuItem href="/settings/billing">
<Icon>
<CreditCardIcon />
</Icon>
<CreditCardIcon className="size-4" />
<Trans i18nKey="billing" />
</TabMenuItem>
</IfCloudHosted>

View file

@ -46,8 +46,8 @@ function NavItem({
target={target}
className={cn(
current
? "text-foreground bg-gray-200"
: "text-muted-foreground border-transparent hover:bg-gray-200 focus:bg-gray-300",
? "text-foreground bg-gray-100"
: "text-muted-foreground hover:text-foreground border-transparent hover:bg-gray-100 focus:bg-gray-200",
"group flex items-center gap-x-3 rounded-md px-3 py-2 text-sm font-semibold leading-6",
)}
>
@ -102,15 +102,15 @@ export function Sidebar() {
}
asChild
>
<button className="mb-4 flex w-full flex-col rounded-md border bg-gray-50 px-4 py-3 focus:border-gray-300 focus:bg-gray-200">
<button className="bg-primary-50 text-primary hover:bg-primary-100 focus:bg-primary-200 mb-4 flex w-full flex-col rounded-lg px-4 py-3">
<span className="mb-2 flex items-center gap-x-2">
<SparklesIcon className="size-5 text-gray-400" />
<SparklesIcon className="size-5 opacity-50" />
<span className="text-sm font-bold">
<Trans i18nKey="upgrade" />
</span>
<ProBadge />
</span>
<span className="text-sm leading-relaxed text-gray-500">
<span className="text-sm leading-relaxed opacity-75">
<Trans
i18nKey="unlockFeatures"
defaults="Unlock all Pro features."

View file

@ -0,0 +1,47 @@
"use client";
import { Trans } from "@/components/trans";
import { useUser } from "@/components/user-provider";
function getTimeOfDay(): "morning" | "afternoon" | "evening" {
const hours = new Date().getHours();
if (hours < 12) {
return "morning";
} else if (hours < 18) {
return "afternoon";
} else {
return "evening";
}
}
export default function WelcomeMessage() {
const { user } = useUser();
const timeOfDay = getTimeOfDay();
switch (timeOfDay) {
case "morning":
return (
<Trans
i18nKey="goodMorning"
defaults="Good morning, {name}!"
values={{ name: user.name }}
/>
);
case "afternoon":
return (
<Trans
i18nKey="goodAfternoon"
defaults="Good afternoon, {name}!"
values={{ name: user.name }}
/>
);
case "evening":
return (
<Trans
i18nKey="goodEvening"
defaults="Good evening, {name}!"
values={{ name: user.name }}
/>
);
}
}

View file

@ -11,7 +11,7 @@ export default async function Page({ params }: { params: Params }) {
const { t } = await getTranslation(params.locale);
return (
<div>
<div className="sticky top-0 z-20 flex items-center justify-between border-b bg-gray-100/90 p-3 backdrop-blur-md sm:grid-cols-3">
<div className="sticky top-0 z-20 flex items-center justify-between p-3 backdrop-blur-md sm:grid-cols-3">
<div className="flex items-center justify-center gap-x-4">
<BackButton />
<GroupPollIcon size="xs" />

View file

@ -43,9 +43,7 @@ export function PageHeader({
className?: string;
variant?: "default" | "ghost";
}) {
return (
<div className={cn("flex items-center gap-x-4", className)}>{children}</div>
);
return <div className={cn(className)}>{children}</div>;
}
export function PageSection({ children }: { children?: React.ReactNode }) {

View file

@ -18,8 +18,8 @@ export function TabMenuItem({
className={cn(
"flex h-9 min-w-0 grow items-center gap-x-2.5 rounded-md px-2.5 text-sm font-medium",
pathname === href
? "text-foreground bg-gray-200"
: "hover:text-foreground focus:text-foreground border-transparent text-gray-500 focus:bg-gray-200",
? "text-foreground bg-gray-100"
: "hover:text-foreground focus:text-foreground border-transparent text-gray-500 hover:bg-gray-100 focus:bg-gray-200",
)}
href={href}
>
@ -30,5 +30,7 @@ export function TabMenuItem({
}
export function TabMenu({ children }: { children: React.ReactNode }) {
return <ul className="flex gap-2.5">{children}</ul>;
return (
<ul className="scrollbar-none flex gap-1 overflow-auto">{children}</ul>
);
}

View file

@ -16,7 +16,7 @@ export function GridCardHeader({
export const GridCard = ({ children }: { children: React.ReactNode }) => {
return (
<div className="relative rounded-lg border bg-white p-3 shadow-sm">
<div className="relative flex h-48 flex-col rounded-lg bg-gray-100 p-4">
{children}
</div>
);

View file

@ -12,8 +12,6 @@ import {
import { Icon } from "@rallly/ui/icon";
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
import {
BarChart2Icon,
CalendarSearchIcon,
CheckIcon,
Link2Icon,
MoreHorizontalIcon,
@ -25,7 +23,11 @@ import Link from "next/link";
import React from "react";
import { useCopyToClipboard } from "react-use";
import { GridCard, GridCardHeader } from "@/components/grid-card";
import {
GridCard,
GridCardFooter,
GridCardHeader,
} from "@/components/grid-card";
import { GroupPollIcon } from "@/components/group-poll-icon";
import { Pill, PillList } from "@/components/pill";
import { Trans } from "@/components/trans";
@ -40,6 +42,7 @@ function CopyLinkButton({ link, ...forwardProps }: { link: string }) {
<Tooltip open={isCopied ? true : undefined}>
<TooltipTrigger onMouseLeave={() => setIsCopied(false)} asChild>
<Button
variant="ghost"
size="sm"
{...forwardProps}
onClick={() => {
@ -94,36 +97,15 @@ export function GroupPollCard({
return (
<GridCard key={pollId}>
<GridCardHeader className="flex flex-col justify-between gap-4 sm:flex-row">
<div className="flex items-center gap-2">
<div>
<GroupPollIcon size="xs" />
</div>
<h3 className="font-medium">
<Link className="truncate hover:underline" href={`/poll/${pollId}`}>
{title}
</Link>
</h3>
<GridCardHeader className="flex items-start justify-between gap-4">
<div>
<GroupPollIcon size="xs" />
</div>
<div className="flex items-center gap-2">
<CopyLinkButton link={inviteLink} />
<Tooltip>
<TooltipTrigger asChild>
<Button size="sm" asChild>
<Link href={`/poll/${pollId}`}>
<Icon>
<BarChart2Icon />
</Icon>
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>
<Trans i18nKey="viewResults" defaults="View results" />
</TooltipContent>
</Tooltip>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm">
<Button variant="ghost" size="sm">
<Icon>
<MoreHorizontalIcon />
</Icon>
@ -147,27 +129,34 @@ export function GroupPollCard({
</DropdownMenu>
</div>
</GridCardHeader>
<PillList>
<Pill>
<Icon>
<CalendarSearchIcon />
</Icon>
<div className="grow space-y-1">
<h3 className="font-medium">
<Link className="truncate hover:underline" href={`/poll/${pollId}`}>
{title}
</Link>
</h3>
<p className="text-muted-foreground text-sm">
{getRange(
localizeTime(dateStart, !timeZone).toDate(),
localizeTime(dateEnd, !timeZone).toDate(),
)}
</Pill>
<Pill>
<Icon>
<User2Icon />
</Icon>
<Trans
i18nKey="participantCount"
defaults="{count, plural, one {# participant} other {# participants}}"
values={{ count: responseCount }}
/>
</Pill>
</PillList>
</p>
</div>
<GridCardFooter>
<PillList>
<Pill></Pill>
<Pill>
<Icon>
<User2Icon />
</Icon>
<Trans
i18nKey="participantCount"
defaults="{count, plural, one {# participant} other {# participants}}"
values={{ count: responseCount }}
/>
</Pill>
</PillList>
</GridCardFooter>
</GridCard>
);
}

View file

@ -11,7 +11,7 @@ export function GroupPollIcon({
role="img"
aria-label="Group Poll Icon"
className={cn(
"inline-flex items-center justify-center bg-gradient-to-br from-purple-500 to-violet-500 text-purple-100",
"inline-flex items-center justify-center bg-purple-600 text-purple-50",
{
"size-6 rounded": size === "xs",
"size-8 rounded-md": size === "sm",

View file

@ -1,10 +1,10 @@
export function PillList({ children }: React.PropsWithChildren) {
return <ul className="flex gap-2">{children}</ul>;
return <ul className="flex flex-col gap-2">{children}</ul>;
}
export function Pill({ children }: React.PropsWithChildren) {
return (
<span className="text-muted-foreground inline-flex items-center gap-2 rounded-md border bg-gray-50 p-1 text-sm">
<span className="inline-flex items-center gap-2 rounded-md text-sm">
{children}
</span>
);

View file

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

View file

@ -42,7 +42,7 @@ export const dashboard = router({
},
},
},
take: 4,
take: 3,
});
return polls.map((poll) => {

View file

@ -3,15 +3,24 @@ import dayjs from "dayjs";
/**
* Get a range of dates in a human readable format
* If the start and end date are the same, return the start date
* If either the end date is in a different year, include the year
* @param start The start date
* @param end The end date
* @returns A human readable range of dates
*/
export function getRange(start: Date, end: Date) {
const startDay = dayjs(start).format("DD MMM");
const endDay = dayjs(end).format("DD MMM");
const startDay = dayjs(start).format("D MMM");
const endDay = dayjs(end).format("D MMM");
const startYear = dayjs(start).format("YYYY");
const endYear = dayjs(end).format("YYYY");
if (startDay === endDay) {
return startDay;
return `${startDay} ${startYear}`;
}
return `${startDay} - ${endDay}`;
if (startYear !== endYear) {
return `${startDay} ${startYear} - ${endDay} ${endYear}`;
}
return `${startDay} - ${endDay} ${startYear}`;
}

View file

@ -7,15 +7,15 @@ 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",
"inline-flex border active:shadow-none hover:shadow-sm transition-colors font-medium disabled:pointer-events-none select-none disabled:opacity-50 items-center justify-center whitespace-nowrap border",
),
{
variants: {
variant: {
primary:
"bg-primary disabled:bg-gray-400 disabled:border-transparent text-primary-foreground hover:bg-primary-700 active:bg-primary-800 shadow-sm",
"bg-primary disabled:bg-gray-400 disabled:border-transparent text-primary-foreground hover:bg-primary-700 active:bg-primary-800",
destructive:
"bg-destructive shadow-sm text-destructive-foreground active:bg-destructive border-destructive hover:bg-destructive/90",
"bg-destructive text-destructive-foreground active:bg-destructive border-destructive hover:bg-destructive/90",
default:
"data-[state=open]:bg-gray-100 hover:bg-gray-100 active:bg-gray-200 bg-white",
secondary:
@ -27,7 +27,7 @@ const buttonVariants = cva(
size: {
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",
lg: "h-11 text-base gap-x-3 px-4 rounded-xl",
},
},
defaultVariants: {