Add an away mode to disable your booking page (#1418)
* Add away column and status circle * Add away status toggle * Show message on booking page when away * Update common.json Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
parent
a61cb690af
commit
7739994f4e
8 changed files with 86 additions and 19 deletions
|
@ -8,6 +8,7 @@ import {
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
LogoutIcon,
|
LogoutIcon,
|
||||||
PuzzleIcon,
|
PuzzleIcon,
|
||||||
|
MoonIcon,
|
||||||
} from "@heroicons/react/solid";
|
} from "@heroicons/react/solid";
|
||||||
import { signOut, useSession } from "next-auth/react";
|
import { signOut, useSession } from "next-auth/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
@ -356,14 +357,25 @@ function UserDropdown({ small }: { small?: boolean }) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const query = useMeQuery();
|
const query = useMeQuery();
|
||||||
const user = query.data;
|
const user = query.data;
|
||||||
|
const mutation = trpc.useMutation("viewer.away");
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<div className="flex items-center w-full space-x-2 cursor-pointer group">
|
<div className="flex items-center w-full space-x-2 cursor-pointer group">
|
||||||
<span
|
<span
|
||||||
className={classNames(small ? "w-8 h-8" : "w-10 h-10", "bg-gray-300 rounded-full flex-shrink-0")}>
|
className={classNames(
|
||||||
|
small ? "w-8 h-8" : "w-10 h-10",
|
||||||
|
"bg-gray-300 rounded-full flex-shrink-0 relative"
|
||||||
|
)}>
|
||||||
<Avatar imageSrc={user?.avatar || ""} alt={user?.username || "Nameless User"} />
|
<Avatar imageSrc={user?.avatar || ""} alt={user?.username || "Nameless User"} />
|
||||||
|
{!user?.away && (
|
||||||
|
<div className="absolute bottom-0 right-0 w-3 h-3 bg-green-500 border-2 border-white rounded-full"></div>
|
||||||
|
)}
|
||||||
|
{user?.away && (
|
||||||
|
<div className="absolute bottom-0 right-0 w-3 h-3 bg-yellow-500 border-2 border-white rounded-full"></div>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
{!small && (
|
{!small && (
|
||||||
<span className="flex items-center flex-grow truncate">
|
<span className="flex items-center flex-grow truncate">
|
||||||
|
@ -384,6 +396,26 @@ function UserDropdown({ small }: { small?: boolean }) {
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
mutation.mutate({ away: !user?.away });
|
||||||
|
utils.invalidateQueries("viewer.me");
|
||||||
|
}}
|
||||||
|
className="flex px-4 py-2 text-sm cursor-pointer hover:bg-gray-100 hover:text-gray-900">
|
||||||
|
<MoonIcon
|
||||||
|
className={classNames(
|
||||||
|
user?.away
|
||||||
|
? "text-purple-500 group-hover:text-purple-700"
|
||||||
|
: "text-gray-500 group-hover:text-gray-700",
|
||||||
|
"mr-2 flex-shrink-0 h-5 w-5"
|
||||||
|
)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{user?.away ? t("set_as_free") : t("set_as_away")}
|
||||||
|
</a>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||||
{user?.username && (
|
{user?.username && (
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<a
|
<a
|
||||||
|
|
|
@ -26,7 +26,7 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
{...props}
|
{...props}
|
||||||
className="z-10 mt-1 text-sm origin-top-right bg-white rounded-sm shadow-lg w-44 ring-1 ring-black ring-opacity-5 focus:outline-none"
|
className="z-10 w-48 mt-1 text-sm origin-top-right bg-white rounded-sm shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||||
ref={forwardedRef}>
|
ref={forwardedRef}>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.Content>
|
</DropdownMenuPrimitive.Content>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||||
|
import { MoonIcon } from "@heroicons/react/solid";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
@ -49,23 +50,31 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
||||||
<p className="text-neutral-500 dark:text-white">{user.bio}</p>
|
<p className="text-neutral-500 dark:text-white">{user.bio}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-6" data-testid="event-types">
|
<div className="space-y-6" data-testid="event-types">
|
||||||
{eventTypes.map((type) => (
|
{user.away && (
|
||||||
<div
|
<div className="relative px-6 py-4 bg-white border rounded-sm group dark:bg-neutral-900 dark:border-0 border-neutral-200">
|
||||||
key={type.id}
|
<MoonIcon className="w-8 h-8 mb-4 text-neutral-800" />
|
||||||
className="relative bg-white border rounded-sm group dark:bg-neutral-900 dark:border-0 dark:hover:border-neutral-600 hover:bg-gray-50 border-neutral-200 hover:border-brand">
|
<h2 className="font-semibold text-neutral-900 dark:text-white">{t("user_away")}</h2>
|
||||||
<ArrowRightIcon className="absolute w-4 h-4 text-black transition-opacity opacity-0 right-3 top-3 dark:text-white group-hover:opacity-100" />
|
<p className="text-neutral-500 dark:text-white">{t("user_away_description")}</p>
|
||||||
<Link
|
|
||||||
href={{
|
|
||||||
pathname: `/${user.username}/${type.slug}`,
|
|
||||||
query,
|
|
||||||
}}>
|
|
||||||
<a className="block px-6 py-4" data-testid="event-type-link">
|
|
||||||
<h2 className="font-semibold text-neutral-900 dark:text-white">{type.title}</h2>
|
|
||||||
<EventTypeDescription eventType={type} />
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
|
{!user.away &&
|
||||||
|
eventTypes.map((type) => (
|
||||||
|
<div
|
||||||
|
key={type.id}
|
||||||
|
className="relative bg-white border rounded-sm group dark:bg-neutral-900 dark:border-0 dark:hover:border-neutral-600 hover:bg-gray-50 border-neutral-200 hover:border-brand">
|
||||||
|
<ArrowRightIcon className="absolute w-4 h-4 text-black transition-opacity opacity-0 right-3 top-3 dark:text-white group-hover:opacity-100" />
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname: `/${user.username}/${type.slug}`,
|
||||||
|
query,
|
||||||
|
}}>
|
||||||
|
<a className="block px-6 py-4" data-testid="event-type-link">
|
||||||
|
<h2 className="font-semibold text-neutral-900 dark:text-white">{type.title}</h2>
|
||||||
|
<EventTypeDescription eventType={type} />
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
{eventTypes.length === 0 && (
|
{eventTypes.length === 0 && (
|
||||||
<div className="overflow-hidden rounded-sm shadow">
|
<div className="overflow-hidden rounded-sm shadow">
|
||||||
|
@ -102,6 +111,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
avatar: true,
|
avatar: true,
|
||||||
theme: true,
|
theme: true,
|
||||||
plan: true,
|
plan: true,
|
||||||
|
away: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "users" ADD COLUMN "away" BOOLEAN NOT NULL DEFAULT false;
|
|
@ -118,6 +118,7 @@ model User {
|
||||||
brandColor String @default("#292929")
|
brandColor String @default("#292929")
|
||||||
// the location where the events will end up
|
// the location where the events will end up
|
||||||
destinationCalendar DestinationCalendar?
|
destinationCalendar DestinationCalendar?
|
||||||
|
away Boolean @default(false)
|
||||||
metadata Json?
|
metadata Json?
|
||||||
|
|
||||||
@@map(name: "users")
|
@@map(name: "users")
|
||||||
|
|
|
@ -570,5 +570,9 @@
|
||||||
"error_required_field": "This field is required.",
|
"error_required_field": "This field is required.",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"team_view_user_availability": "View user availability",
|
"team_view_user_availability": "View user availability",
|
||||||
"team_view_user_availability_disabled": "User needs to accept invite to view availability"
|
"team_view_user_availability_disabled": "User needs to accept invite to view availability",
|
||||||
|
"set_as_away": "Set yourself as away",
|
||||||
|
"set_as_free": "Disable away status",
|
||||||
|
"user_away": "This user is currently away.",
|
||||||
|
"user_away_description": "The person you are trying to book has set themselves to away, and therefore is not accepting new bookings."
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ async function getUserFromSession({
|
||||||
twoFactorEnabled: true,
|
twoFactorEnabled: true,
|
||||||
brandColor: true,
|
brandColor: true,
|
||||||
plan: true,
|
plan: true,
|
||||||
|
away: true,
|
||||||
credentials: {
|
credentials: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
|
|
@ -57,6 +57,7 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||||
twoFactorEnabled,
|
twoFactorEnabled,
|
||||||
brandColor,
|
brandColor,
|
||||||
plan,
|
plan,
|
||||||
|
away,
|
||||||
} = ctx.user;
|
} = ctx.user;
|
||||||
const me = {
|
const me = {
|
||||||
id,
|
id,
|
||||||
|
@ -73,10 +74,26 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||||
twoFactorEnabled,
|
twoFactorEnabled,
|
||||||
brandColor,
|
brandColor,
|
||||||
plan,
|
plan,
|
||||||
|
away,
|
||||||
};
|
};
|
||||||
return me;
|
return me;
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
.mutation("away", {
|
||||||
|
input: z.object({
|
||||||
|
away: z.boolean(),
|
||||||
|
}),
|
||||||
|
async resolve({ input, ctx }) {
|
||||||
|
await ctx.prisma.user.update({
|
||||||
|
where: {
|
||||||
|
email: ctx.user.email,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
away: input.away,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
.query("eventTypes", {
|
.query("eventTypes", {
|
||||||
async resolve({ ctx }) {
|
async resolve({ ctx }) {
|
||||||
const { prisma } = ctx;
|
const { prisma } = ctx;
|
||||||
|
|
Loading…
Reference in a new issue