UI: Responsiveness of Settings Menu

UI: Responsive of Settings Menu
This commit is contained in:
Johannes
2023-08-22 11:14:34 +02:00
committed by GitHub
11 changed files with 212 additions and 102 deletions

View File

@@ -237,8 +237,7 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
<div className="flex space-x-4 py-2">
<Link
href={`/environments/${environmentId}/surveys/`}
className=" flex items-center justify-center rounded-md bg-gradient-to-b text-white transition-all ease-in-out hover:scale-105">
{/* <PlusIcon className="h-6 w-6" /> */}
className="flex items-center justify-center rounded-md bg-gradient-to-b text-white transition-all ease-in-out hover:scale-105">
<Image src={FaveIcon} width={30} height={30} alt="faveicon" />
</Link>
@@ -253,7 +252,7 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
item.current
? "bg-slate-100 text-slate-900"
: "text-slate-900 hover:bg-slate-50 hover:text-slate-900",
"hidden items-center rounded-md px-2 py-1 text-sm font-medium sm:inline-flex"
"hidden items-center rounded-md px-2 py-1 text-sm font-medium lg:inline-flex"
)}
aria-current={item.current ? "page" : undefined}>
<IconComponent className="mr-3 h-5 w-5" />
@@ -263,7 +262,8 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
})}
</div>
<div className="flex items-center sm:hidden">
{/* Mobile Menu */}
<div className="flex items-center lg:hidden">
<Popover open={mobileNavMenuOpen} onOpenChange={setMobileNavMenuOpen}>
<PopoverTrigger onClick={() => setMobileNavMenuOpen(!mobileNavMenuOpen)}>
<span>
@@ -290,7 +290,8 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
</Popover>
</div>
<div className="hidden sm:ml-6 sm:flex sm:items-center">
{/* User Dropdown */}
<div className="hidden lg:ml-6 lg:flex lg:items-center">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="flex cursor-pointer flex-row items-center space-x-5">

View File

@@ -18,7 +18,7 @@ export default function SettingsCard({
}) {
return (
<div className="my-4 w-full bg-white shadow sm:rounded-lg">
<div className="rounded-t-lg border-b border-slate-200 bg-slate-100 px-6 py-5">
<div className="border-b border-slate-200 bg-slate-100 px-6 py-5">
<div className="flex">
<h3 className="text-lg font-medium leading-6 text-slate-900">{title}</h3>
<div className="ml-2">

View File

@@ -4,6 +4,8 @@ import { useProduct } from "@/lib/products/products";
import { useTeam } from "@/lib/teams/teams";
import { truncate } from "@/lib/utils";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui";
import { ChevronDownIcon } from "@heroicons/react/20/solid";
import {
AdjustmentsVerticalIcon,
BellAlertIcon,
@@ -11,22 +13,24 @@ import {
CreditCardIcon,
DocumentCheckIcon,
DocumentMagnifyingGlassIcon,
HashtagIcon,
KeyIcon,
LinkIcon,
PaintBrushIcon,
HashtagIcon,
UserCircleIcon,
UsersIcon,
} from "@heroicons/react/24/solid";
import clsx from "clsx";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useMemo } from "react";
import { useMemo, useState } from "react";
export default function SettingsNavbar({ environmentId }: { environmentId: string }) {
const pathname = usePathname();
const { team } = useTeam(environmentId);
const { product } = useProduct(environmentId);
const [mobileNavMenuOpen, setMobileNavMenuOpen] = useState(false);
interface NavigationLink {
name: string;
href: string;
@@ -41,7 +45,6 @@ export default function SettingsNavbar({ environmentId }: { environmentId: strin
links: NavigationLink[];
}
// Then, specify the type of the navigation array
const navigation: NavigationSection[] = useMemo(
() => [
{
@@ -181,42 +184,96 @@ export default function SettingsNavbar({ environmentId }: { environmentId: strin
if (!navigation) return null;
return (
<div className="fixed h-full bg-white py-2 pl-4 pr-10">
<nav className="flex-1 space-y-1 bg-white px-2">
{navigation.map((item) => (
<div key={item.title}>
<p className="mt-6 pl-3 pr-1 text-xs font-semibold uppercase tracking-wider text-slate-500">
{item.title}{" "}
{item.title === "Product" && product?.name && (
<span className="font-normal capitalize">({truncate(product?.name, 10)})</span>
)}
{item.title === "Team" && team?.name && (
<span className="font-normal capitalize">({truncate(team?.name, 14)})</span>
)}
</p>
<div className="ml-2 mt-1 space-y-1">
{item.links
.filter((l) => !l.hidden)
.map((link) => (
<Link
key={link.name}
href={link.href}
target={link.target}
className={clsx(
link.current ? "bg-slate-100 text-slate-900" : "text-slate-900 hover:bg-slate-50 ",
"group flex items-center whitespace-nowrap rounded-md px-1 py-1 pl-2 text-sm font-medium "
)}>
<link.icon
className="mr-3 h-4 w-4 flex-shrink-0 text-slate-400 group-hover:text-slate-500"
aria-hidden="true"
/>
{link.name}
</Link>
))}
<>
<div className="fixed hidden h-full bg-white py-2 pl-4 pr-10 md:block ">
<nav className="flex-1 space-y-1 bg-white px-2">
{navigation.map((item) => (
<div key={item.title}>
<p className="mt-6 pl-3 pr-1 text-xs font-semibold uppercase tracking-wider text-slate-500">
{item.title}{" "}
{item.title === "Product" && product?.name && (
<span className="font-normal capitalize">({truncate(product?.name, 10)})</span>
)}
{item.title === "Team" && team?.name && (
<span className="font-normal capitalize">({truncate(team?.name, 14)})</span>
)}
</p>
<div className="ml-2 mt-1 space-y-1">
{item.links
.filter((l) => !l.hidden)
.map((link) => (
<Link
key={link.name}
href={link.href}
target={link.target}
className={clsx(
link.current ? "bg-slate-100 text-slate-900" : "text-slate-900 hover:bg-slate-50 ",
"group flex items-center whitespace-nowrap rounded-md px-1 py-1 pl-2 text-sm font-medium "
)}>
<link.icon
className="mr-3 h-4 w-4 flex-shrink-0 text-slate-400 group-hover:text-slate-500"
aria-hidden="true"
/>
{link.name}
</Link>
))}
</div>
</div>
</div>
))}
</nav>
</div>
))}
</nav>
</div>
{/* Mobile Menu */}
<div className="fixed z-10 flex h-14 w-full items-center justify-between border-b border-slate-200 bg-white px-4 sm:px-6 md:hidden">
<Popover open={mobileNavMenuOpen} onOpenChange={setMobileNavMenuOpen}>
<PopoverTrigger onClick={() => setMobileNavMenuOpen(!mobileNavMenuOpen)}>
<span className="flex items-center">
<span className="mr-1">Settings</span>
<ChevronDownIcon className="h-5 w-5 text-slate-500" aria-hidden="true" />
</span>
</PopoverTrigger>
<PopoverContent className="shadow">
<div className="flex flex-col">
{navigation.map((item) => (
<div key={item.title}>
<p className="mt-3 pl-3 pr-1 text-xs font-semibold uppercase tracking-wider text-slate-500">
{item.title}{" "}
{item.title === "Product" && product?.name && (
<span className="font-normal capitalize">({truncate(product?.name, 10)})</span>
)}
{item.title === "Team" && team?.name && (
<span className="font-normal capitalize">({truncate(team?.name, 14)})</span>
)}
</p>
<div className="ml-2 mt-1 space-y-1">
{item.links
.filter((l) => !l.hidden)
.map((link) => (
<Link
key={link.name}
href={link.href}
target={link.target}
onClick={() => setMobileNavMenuOpen(false)}
className={clsx(
link.current
? "bg-slate-100 text-slate-900"
: "text-slate-900 hover:bg-slate-50 ",
"group flex items-center whitespace-nowrap rounded-md px-1 py-1 pl-2 text-sm font-medium "
)}>
<link.icon
className="mr-3 h-4 w-4 flex-shrink-0 text-slate-400 group-hover:text-slate-500"
aria-hidden="true"
/>
{link.name}
</Link>
))}
</div>
</div>
))}
</div>
</PopoverContent>
</Popover>
</div>
</>
);
}

View File

@@ -60,10 +60,10 @@ export default function EditAPIKeys({
</div>
<div className="rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-9 content-center rounded-t-lg bg-slate-100 px-6 text-left text-sm font-semibold text-slate-900">
<div className="col-span-2">Label</div>
<div className="col-span-2">API Key</div>
<div className="col-span-2">Last used</div>
<div className="col-span-2">Created at</div>
<div className="col-span-4 sm:col-span-2">Label</div>
<div className="col-span-4 hidden sm:col-span-2 sm:block">API Key</div>
<div className="col-span-4 hidden sm:col-span-2 sm:block">Last used</div>
<div className="col-span-4 sm:col-span-2">Created at</div>
<div className=""></div>
</div>
<div className="grid-cols-9">
@@ -77,12 +77,14 @@ export default function EditAPIKeys({
<div
className="grid h-12 w-full grid-cols-9 content-center rounded-lg px-6 text-left text-sm text-slate-900"
key={apiKey.hashedKey}>
<div className="col-span-2 font-semibold">{apiKey.label}</div>
<div className="col-span-2">{apiKey.apiKey || <span className="italic">secret</span>}</div>
<div className="col-span-2">
<div className="col-span-4 font-semibold sm:col-span-2">{apiKey.label}</div>
<div className="col-span-4 hidden sm:col-span-2 sm:block">
{apiKey.apiKey || <span className="italic">secret</span>}
</div>
<div className="col-span-4 hidden sm:col-span-2 sm:block">
{apiKey.lastUsedAt && timeSince(apiKey.lastUsedAt.toString())}
</div>
<div className="col-span-2">{timeSince(apiKey.createdAt.toString())}</div>
<div className="col-span-4 sm:col-span-2">{timeSince(apiKey.createdAt.toString())}</div>
<div className="col-span-1 text-center">
<button onClick={(e) => handleOpenDeleteKeyModal(e, apiKey)}>
<TrashIcon className="h-5 w-5 text-slate-700 hover:text-slate-500" />

View File

@@ -1,5 +1,5 @@
import SettingsNavbar from "./SettingsNavbar";
import { Metadata } from "next";
import SettingsNavbar from "./SettingsNavbar";
export const metadata: Metadata = {
title: "Settings",
@@ -8,10 +8,10 @@ export const metadata: Metadata = {
export default function SettingsLayout({ children, params }) {
return (
<>
<div className="flex">
<div className="sm:flex">
<SettingsNavbar environmentId={params.environmentId} />
<div className="ml-64 w-full">
<div className="max-w-4xl p-6">
<div className="w-full md:ml-64">
<div className="max-w-4xl px-6 pb-6 pt-14 md:pt-6">
<div>{children}</div>
</div>
</div>

View File

@@ -1,11 +1,11 @@
"use client";
import { cn } from "@formbricks/lib/cn";
import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
import { TProduct, TProductUpdateInput } from "@formbricks/types/v1/product";
import { Button, ColorPicker, Label, Switch } from "@formbricks/ui";
import { useState } from "react";
import toast from "react-hot-toast";
import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
import { TProduct, TProductUpdateInput } from "@formbricks/types/v1/product";
import { updateProductAction } from "./actions";
interface EditHighlightBorderProps {
@@ -46,7 +46,7 @@ export const EditHighlightBorder = ({ product }: EditHighlightBorderProps) => {
}
};
return (
/* return (
<div className="flex min-h-full w-full">
<div className="flex w-1/2 flex-col px-6 py-5">
<div className="mb-6 flex items-center space-x-2">
@@ -92,4 +92,54 @@ export const EditHighlightBorder = ({ product }: EditHighlightBorderProps) => {
</div>
</div>
);
}; */
return (
<div className="flex min-h-full w-full flex-col md:flex-row">
<div className="flex w-full flex-col px-6 py-5 md:w-1/2">
<div className="mb-6 flex items-center space-x-2">
<Switch id="highlightBorder" checked={showHighlightBorder} onCheckedChange={handleSwitch} />
<h2 className="text-sm font-medium text-slate-800">Show highlight border</h2>
</div>
{showHighlightBorder && color ? (
<>
<Label htmlFor="brandcolor">Color (HEX)</Label>
<ColorPicker color={color} onChange={setColor} />
</>
) : null}
<Button
variant="darkCTA"
className="mt-4 flex max-w-[80px] items-center justify-center"
loading={updatingBorder}
onClick={handleUpdateHighlightBorder}>
Save
</Button>
</div>
<div className="mt-4 flex w-full flex-col items-center justify-center gap-4 bg-slate-200 px-6 py-5 md:mt-0 md:w-1/2">
<h3 className="text-slate-500">Preview</h3>
<div
className={cn("flex flex-col gap-4 rounded-lg border-2 bg-white p-5")}
{...(showHighlightBorder &&
color && {
style: {
borderColor: color,
},
})}>
<h3 className="text-sm font-semibold text-slate-800">How easy was it for you to do this?</h3>
<div className="grid grid-cols-5 rounded-xl border border-slate-400">
{[1, 2, 3, 4, 5].map((num) => (
<div
key={num}
className="flex justify-center border-r border-slate-400 px-3 py-2 last:border-r-0 lg:px-6 lg:py-5">
<span className="text-sm font-medium">{num}</span>
</div>
))}
</div>
</div>
</div>
</div>
);
};

View File

@@ -2,6 +2,7 @@
import ShareInviteModal from "@/app/(app)/environments/[environmentId]/settings/members/ShareInviteModal";
import TransferOwnershipModal from "@/app/(app)/environments/[environmentId]/settings/members/TransferOwnershipModal";
import CustomDialog from "@/components/shared/CustomDialog";
import DeleteDialog from "@/components/shared/DeleteDialog";
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import CreateTeamModal from "@/components/team/CreateTeamModal";
@@ -17,6 +18,7 @@ import {
updateMemberRole,
useMembers,
} from "@/lib/members";
import { useMemberships } from "@/lib/memberships";
import { useProfile } from "@/lib/profile";
import { capitalizeFirstLetter } from "@/lib/utils";
import {
@@ -35,12 +37,10 @@ import {
} from "@formbricks/ui";
import { ChevronDownIcon } from "@heroicons/react/20/solid";
import { PaperAirplaneIcon, ShareIcon, TrashIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";
import AddMemberModal from "./AddMemberModal";
import { useRouter } from "next/navigation";
import { useMemberships } from "@/lib/memberships";
import CustomDialog from "@/components/shared/CustomDialog";
type EditMembershipsProps = {
environmentId: string;
@@ -273,7 +273,7 @@ export function EditMemberships({ environmentId }: EditMembershipsProps) {
)}
<Button
variant="secondary"
className="mr-2"
className="mr-2 hidden sm:inline-flex"
onClick={() => {
setCreateTeamModalOpen(true);
}}>
@@ -294,24 +294,26 @@ export function EditMemberships({ environmentId }: EditMembershipsProps) {
<div className="col-span-2"></div>
<div className="col-span-5">Fullname</div>
<div className="col-span-5">Email</div>
<div className="col-span-3">Role</div>
<div className="col-span-5"></div>
<div className="hidden sm:col-span-3 sm:block">Role</div>
<div className="hidden sm:col-span-5 sm:block"></div>
</div>
<div className="grid-cols-20">
{[...team.members, ...team.invitees].map((member) => (
<div
className="grid-cols-20 grid h-auto w-full content-center rounded-lg p-0.5 py-2 text-left text-sm text-slate-900"
key={member.email}>
<div className="h-58 col-span-2 pl-4">
<ProfileAvatar userId={member.userId || member.email} />
<div className="h-58 col-span-2 pl-4 ">
<div className="hidden sm:block">
<ProfileAvatar userId={member.userId || member.email} />
</div>
</div>
<div className="ph-no-capture col-span-5 flex flex-col justify-center break-all">
<p>{member.name}</p>
</div>
<div className="ph-no-capture col-span-5 flex flex-col justify-center break-all">
<div className="ph-no-capture col-span-5 flex flex-col justify-center break-all">
{member.email}
</div>
<div className="ph-no-capture col-span-3 flex flex-col items-start justify-center break-all">
<div className="ph-no-capture col-span-3 hidden flex-col items-start justify-center break-all sm:flex">
<RoleElement
isAdminOrOwner={isAdminOrOwner}
memberRole={member.role}
@@ -325,7 +327,7 @@ export function EditMemberships({ environmentId }: EditMembershipsProps) {
currentUserRole={role}
/>
</div>
<div className="col-span-5 flex items-center justify-end gap-x-4 pr-4">
<div className="col-span-5 ml-48 hidden items-center justify-end gap-x-2 pr-4 sm:ml-0 sm:gap-x-4 lg:flex">
{!member.accepted &&
(isExpired(member) ? (
<Badge className="mr-2" type="gray" text="Expired" size="tiny" />

View File

@@ -22,20 +22,21 @@ export default function EditAlerts({ memberships, user, environmentId }: EditAle
<p className="text-slate-800">{membership.team.name}</p>
</div>
<div className="mb-6 rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-4 content-center rounded-t-lg bg-slate-100 px-4 text-left text-sm font-semibold text-slate-900">
<div className="col-span-2">Survey</div>
<div className="col-span-1">Product</div>
<div className="grid h-12 grid-cols-3 content-center rounded-t-lg bg-slate-100 px-4 text-left text-sm font-semibold text-slate-900">
<div className="col-span-2 flex items-center">Survey</div>
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger>
<div className="col-span-1 cursor-default text-center">
Every Response <QuestionMarkCircleIcon className="mb-1 inline h-4 w-4 text-slate-500" />
<div className="col-span-1 flex cursor-default items-center justify-center">
<span className="">Every Response</span>
<QuestionMarkCircleIcon className="h-4 w-4 flex-shrink-0 text-slate-500" />
</div>
</TooltipTrigger>
<TooltipContent>Sends complete responses, no partials.</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{membership.team.products.some((product) =>
product.environments.some((environment) => environment.surveys.length > 0)
) ? (
@@ -46,13 +47,11 @@ export default function EditAlerts({ memberships, user, environmentId }: EditAle
<div key={environment.id}>
{environment.surveys.map((survey) => (
<div
className="grid h-auto w-full cursor-pointer grid-cols-4 place-content-center rounded-lg px-2 py-2 text-left text-sm text-slate-900 hover:bg-slate-50"
className="grid h-auto w-full cursor-pointer grid-cols-3 place-content-center rounded-lg px-2 py-2 text-left text-sm text-slate-900 hover:bg-slate-50"
key={survey.name}>
<div className=" col-span-2 flex items-center ">
<p className="text-slate-800">{survey.name}</p>
</div>
<div className="col-span-1 flex flex-col justify-center break-all">
{product?.name}
<div className="col-span-2 text-left">
<div className="font-medium text-slate-900">{survey.name}</div>
<div className="text-xs text-slate-400">{product.name}</div>
</div>
<div className="col-span-1 text-center">
<NotificationSwitch

View File

@@ -21,17 +21,17 @@ export default function EditWeeklySummary({ memberships, user, environmentId }:
<p className="text-slate-800">{membership.team.name}</p>
</div>
<div className="mb-6 rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-2 content-center rounded-t-lg bg-slate-100 px-4 text-left text-sm font-semibold text-slate-900">
<div>Product</div>
<div className="cursor-default pr-12 text-right">Weekly Summary</div>
<div className="grid h-12 grid-cols-3 content-center rounded-t-lg bg-slate-100 px-4 text-left text-sm font-semibold text-slate-900">
<div className="col-span-2">Product</div>
<div className="col-span-1 text-center">Weekly Summary</div>
</div>
<div className="grid-cols-8 space-y-1 p-2">
<div className="space-y-1 p-2">
{membership.team.products.map((product) => (
<div
className="grid h-auto w-full cursor-pointer grid-cols-2 place-content-center rounded-lg px-2 py-2 text-left text-sm text-slate-900 hover:bg-slate-50"
className="grid h-auto w-full cursor-pointer grid-cols-3 place-content-center justify-center rounded-lg px-2 py-2 text-left text-sm text-slate-900 hover:bg-slate-50"
key={product.id}>
<div>{product?.name}</div>
<div className="mr-20 flex justify-end">
<div className="col-span-2">{product?.name}</div>
<div className="col-span-1 flex items-center justify-center">
<NotificationSwitch
surveyOrProductId={product.id}
userId={user.id}

View File

@@ -3,7 +3,7 @@
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import { useEnvironment } from "@/lib/environments/environments";
import { ErrorComponent } from "@formbricks/ui";
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
import { LightBulbIcon } from "@heroicons/react/24/solid";
import { useRouter } from "next/navigation";
export default function EnvironmentNotice({ environmentId }: { environmentId: string }) {
@@ -26,14 +26,14 @@ export default function EnvironmentNotice({ environmentId }: { environmentId: st
return (
<div>
{environment.type === "production" && !environment.widgetSetupCompleted && (
<div className="flex items-center rounded-lg border border-amber-100 bg-amber-50 p-4 text-slate-900 shadow-sm">
<ExclamationTriangleIcon className="mr-3 h-6 w-6 text-amber-700" />
You&apos;re currently in the Production environment.
<a
onClick={() => changeEnvironment("development")}
className="ml-1 cursor-pointer font-medium underline">
Set up Development environment?
</a>
<div className="flex items-center space-y-3 rounded-lg border border-blue-100 bg-blue-50 p-4 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base">
<LightBulbIcon className="mr-3 h-6 w-6 text-blue-400" />
<p>
You&apos;re currently in the Production environment.
<a onClick={() => changeEnvironment("development")} className="ml-1 cursor-pointer underline">
Switch to Development environment.
</a>
</p>
</div>
)}
</div>

View File

@@ -1,16 +1,15 @@
"use client";
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import { Confetti } from "@formbricks/ui";
import { useEnvironment } from "@/lib/environments/environments";
import { useEnvironmentMutation } from "@/lib/environments/mutateEnvironments";
import { useEvents } from "@/lib/events/events";
import { timeSince } from "@formbricks/lib/time";
import { Confetti, ErrorComponent } from "@formbricks/ui";
import { ArrowDownIcon, CheckIcon, ExclamationTriangleIcon } from "@heroicons/react/24/solid";
import clsx from "clsx";
import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import { ErrorComponent } from "@formbricks/ui";
interface WidgetStatusIndicatorProps {
environmentId: string;
@@ -81,7 +80,7 @@ export default function WidgetStatusIndicator({ environmentId, type }: WidgetSta
return (
<div
className={clsx(
"flex flex-col items-center justify-center space-y-2 rounded-lg py-6",
"flex flex-col items-center justify-center space-y-2 rounded-lg py-6 text-center",
status === "notImplemented" && "bg-slate-100",
status === "running" && "bg-green-100",
status === "issue" && "bg-amber-100"
@@ -95,7 +94,7 @@ export default function WidgetStatusIndicator({ environmentId, type }: WidgetSta
)}>
<currentStatus.icon />
</div>
<p className="text-xl font-bold text-slate-800">{currentStatus.title}</p>
<p className="text-md font-bold text-slate-800 md:text-xl">{currentStatus.title}</p>
<p className="text-sm text-slate-700">
{currentStatus.subtitle}{" "}
{status !== "notImplemented" && <span>{timeSince(events[0].createdAt)}</span>}