feat: Migrating App Store to v2 (#3911)
* Fix breadcrumb colors * HorizontalTabs * Team List Item WIP * Horizontal Tabs * Cards * Remove team list item WIP * Login Page * Add welcome back i118n * EventType page work * Update EventType Icons * WIP Availability * Horizontal Tab Work * Add build command for in root * Update build DIr/command * Add Edit Button + change buttons to v2 * Availablitiy page * Fix IPAD * Make mobile look a little nicer * WIP bookingshell * Remove list items from breaking build * Mian bulk of Booking Page. * Few updates to components * Fix chormatic feedback * Fix banner * Fix Empty Screen * Text area + embded window fixes * Semi fix avatar * Troubleshoot container + Active on count * Improve mobile * NITS * Fix padding on input * Fix icons * Starting to move event types settings to tabs * Begin migration to single page form * Single page tabs * Limits Page * Advanced tab * Add RHF to dependancies * Most of advanced tab * Solved RHF mismtach * Build fixes * RHF conditionals fixes * Improved legibility * Major refactor/organisation into optional V2 UI * Portal EditLocationModal * Fix dialoug form * Update imports * Auto Animate + custom inputs WIP * Custom Inputs * WIP Apps * Fixing stories imports * Stripe app * Remove duplicate dialog * Remove duplicate dialog * Fix embed URL * Fix app toggles + number of active apps * Fix container padding on disabledBorder prop * Removes strict * EventType Team page WIP * Fix embed * NIT * Add Darkmode gray color * V2 Shell WIP * Fix headings on shell V2 * Fix mobile layout with V2 shell * V2 create event type button * Checked Team Select * Hidden to happen on save - not on toggle * Team Attendee Select animation * Fix scheduling type and remove multi select label * Fix overflow on teams url * Even Type move order handles * Fix Embed TS errors * Fix TS errors * Fix Eslint errors * Fix TS errors for UI * Fix ESLINT error * Starting migration * added SidebarCard for promo to v2 and storybook (#3906) Co-authored-by: Julian Benegas <julianbenegas99@gmail.com> Co-authored-by: Alan <alannnc@gmail.com> Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> * Tooltip Provider - Wrapper due to dep upgrade * public event type list darkmode * V2 Color changes to public booking * Remove unused component * Fix typecheck * Explore all apps section WIP * Update packages/ui/v2/core/layouts/AppsLayout.tsx Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> * Interactive category list * Moving stuff to pages/v2 * Latest changes * Restoring uneeded changes * Making v1 apps work * Removing duplicated css util * Update apps/web/middleware.ts Co-authored-by: Omar López <zomars@me.com> * Hiding categories for now * Accomodating badges for app card * Default for global apps, filtering tweaks * Fixing types * Reimplementing fixing types * i18n + type fix * Removing unneeded eslint exception * Fixing tabs in app layout * Commiting yarn.lock in case it helps * Routing enabled category filter + responsive appcard * Feat: v2 app store category/categories (#4126) * Single category page * Latest changes * Applying feedback * Migrates UpgradeToProDialog Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: zomars <zomars@me.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-authored-by: Julian Benegas <julianbenegas99@gmail.com> Co-authored-by: Alan <alannnc@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
26 files changed
+763
-97
No files matched your search
@@ -11,16 +11,7 @@ export default function AllApps({ apps }: { apps: App[] }) {
|
||||
<h2 className="mb-2 text-lg font-semibold text-gray-900">{t("all_apps")}</h2>
|
||||
<div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
{apps.map((app) => (
|
||||
<AppCard
|
||||
key={app.name}
|
||||
name={app.name}
|
||||
slug={app.slug}
|
||||
description={app.description}
|
||||
logo={app.logo}
|
||||
rating={app.rating}
|
||||
reviews={app.reviews}
|
||||
isProOnly={app.isProOnly}
|
||||
/>
|
||||
<AppCard key={app.name} app={app} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { App } from "@calcom/types/App";
|
||||
import Badge from "@calcom/ui/Badge";
|
||||
import Button from "@calcom/ui/Button";
|
||||
|
||||
interface AppCardProps {
|
||||
logo: string;
|
||||
name: string;
|
||||
slug?: string;
|
||||
category?: string;
|
||||
description: string;
|
||||
rating: number;
|
||||
reviews?: number;
|
||||
isProOnly?: boolean;
|
||||
app: App;
|
||||
}
|
||||
|
||||
export default function AppCard(props: AppCardProps) {
|
||||
const { data: user } = trpc.useQuery(["viewer.me"]);
|
||||
return (
|
||||
<Link href={"/apps/" + props.slug}>
|
||||
<Link href={"/apps/" + props.app.slug}>
|
||||
<a
|
||||
className="block h-full rounded-sm border border-gray-300 p-5 hover:bg-neutral-50"
|
||||
data-testid={`app-store-app-card-${props.slug}`}>
|
||||
data-testid={`app-store-app-card-${props.app.slug}`}>
|
||||
<div className="flex">
|
||||
{
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={props.logo} alt={props.name + " Logo"} className="mb-4 h-12 w-12 rounded-sm" />
|
||||
<img src={props.app.logo} alt={props.app.name + " Logo"} className="mb-4 h-12 w-12 rounded-sm" />
|
||||
}
|
||||
<Button
|
||||
color="secondary"
|
||||
@@ -37,8 +31,8 @@ export default function AppCard(props: AppCardProps) {
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<h3 className="font-medium">{props.name}</h3>
|
||||
{props.isProOnly && user?.plan === "FREE" ? (
|
||||
<h3 className="font-medium">{props.app.name}</h3>
|
||||
{props.app.isProOnly && user?.plan === "FREE" ? (
|
||||
<Badge className="ml-2" variant="default">
|
||||
PRO
|
||||
</Badge>
|
||||
@@ -48,7 +42,7 @@ export default function AppCard(props: AppCardProps) {
|
||||
<span>{props.rating} stars</span> <StarIcon className="ml-1 mt-0.5 h-4 w-4 text-yellow-600" />
|
||||
<span className="pl-1 text-gray-500">{props.reviews} reviews</span>
|
||||
</div> */}
|
||||
<p className="mt-2 truncate text-sm text-gray-500">{props.description}</p>
|
||||
<p className="mt-2 truncate text-sm text-gray-500">{props.app.description}</p>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -21,20 +21,10 @@ const TrendingAppsSlider = <T extends App>({ items }: { items: T[] }) => {
|
||||
},
|
||||
},
|
||||
}}
|
||||
renderItem={(app) => (
|
||||
<AppCard
|
||||
key={app.name}
|
||||
name={app.name}
|
||||
slug={app.slug}
|
||||
description={app.description}
|
||||
logo={app.logo}
|
||||
rating={app.rating}
|
||||
reviews={app.reviews}
|
||||
isProOnly={app.isProOnly}
|
||||
/>
|
||||
)}
|
||||
renderItem={(app) => <AppCard app={app} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/** @deprecated use v2 instead, located at packages/ui/v2/core/apps */
|
||||
export default TrendingAppsSlider;
|
||||
@@ -132,9 +132,7 @@ const Component = ({
|
||||
isGlobal || (installedAppCount > 0 && allowedMultipleInstalls) ? (
|
||||
<div className="flex space-x-3">
|
||||
<Button StartIcon={Icon.FiCheck} color="secondary" disabled>
|
||||
{installedAppCount > 0
|
||||
? t("active_install", { count: installedAppCount })
|
||||
: t("globally_install")}
|
||||
{installedAppCount > 0 ? t("active_install", { count: installedAppCount }) : t("default")}
|
||||
</Button>
|
||||
{!isGlobal && (
|
||||
<InstallAppButton
|
||||
@@ -158,7 +156,7 @@ const Component = ({
|
||||
color="primary"
|
||||
size="base"
|
||||
data-testid="install-app-button">
|
||||
{t("add_another")}
|
||||
{t("install_another")}
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -13,9 +13,7 @@ const V2_WHITELIST = [
|
||||
"/bookings",
|
||||
"/event-types",
|
||||
"/workflows",
|
||||
// Apps contains trailing slash to prevent app overview from being rendered as v2,
|
||||
// since it doesn't exist yet.
|
||||
"/apps/",
|
||||
"/apps",
|
||||
"/success",
|
||||
];
|
||||
const V2_BLACKLIST = ["/apps/routing_forms"];
|
||||
|
||||
@@ -31,17 +31,7 @@ export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticP
|
||||
<h2 className="mb-2 text-lg font-semibold text-gray-900">All {router.query.category} apps</h2>
|
||||
<div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
{apps.map((app) => {
|
||||
return (
|
||||
<AppCard
|
||||
key={app.name}
|
||||
slug={app.slug}
|
||||
name={app.name}
|
||||
description={app.description}
|
||||
logo={app.logo}
|
||||
rating={app.rating}
|
||||
isProOnly={app.isProOnly}
|
||||
/>
|
||||
);
|
||||
return <AppCard key={app.name} app={app} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,6 +36,7 @@ import { Icon } from "@calcom/ui/Icon";
|
||||
import Shell from "@calcom/ui/Shell";
|
||||
import Switch from "@calcom/ui/Switch";
|
||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
||||
import { UpgradeToProDialog } from "@calcom/ui/UpgradeToProDialog";
|
||||
import { Form } from "@calcom/ui/form/fields";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
@@ -50,7 +51,6 @@ import { ClientSuspense } from "@components/ClientSuspense";
|
||||
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
|
||||
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||
import Loader from "@components/Loader";
|
||||
import { UpgradeToProDialog } from "@components/UpgradeToProDialog";
|
||||
import { AvailabilitySelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||
import RecurringEventController from "@components/eventtype/RecurringEventController";
|
||||
|
||||
@@ -27,6 +27,7 @@ import Button from "@calcom/ui/Button";
|
||||
import ConfirmationDialogContent from "@calcom/ui/ConfirmationDialogContent";
|
||||
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import { UpgradeToProDialog } from "@calcom/ui/UpgradeToProDialog";
|
||||
import { Form, PasswordField } from "@calcom/ui/form/fields";
|
||||
import { Label } from "@calcom/ui/form/fields";
|
||||
|
||||
@@ -46,8 +47,6 @@ import { UsernameAvailability } from "@components/ui/UsernameAvailability";
|
||||
import ColorPicker from "@components/ui/colorpicker";
|
||||
import Select from "@components/ui/form/Select";
|
||||
|
||||
import { UpgradeToProDialog } from "../../components/UpgradeToProDialog";
|
||||
|
||||
type Props = inferSSRProps<typeof getServerSideProps>;
|
||||
|
||||
function HideBrandingInput(props: { hideBrandingRef: RefObject<HTMLInputElement>; user: Props["user"] }) {
|
||||
|
||||
@@ -37,7 +37,7 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
||||
isGlobal={data.isGlobal}
|
||||
type={data.type}
|
||||
logo={data.logo}
|
||||
categories={[data.category]}
|
||||
categories={data.categories ?? [data.category]}
|
||||
author={data.publisher}
|
||||
feeType={data.feeType || "usage-based"}
|
||||
price={data.price || 0}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { AppCategories } from "@prisma/client";
|
||||
import { GetStaticPropsContext, InferGetStaticPropsType } from "next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import Shell from "@calcom/ui/v2/core/Shell";
|
||||
import AppCard from "@calcom/ui/v2/core/apps/AppCard";
|
||||
|
||||
export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const { category } = router.query;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Shell isPublic large>
|
||||
<div className="text-md flex items-center gap-1 px-4 pb-3 pt-3 font-normal md:px-8 lg:px-0 lg:pt-0">
|
||||
<Link href="/apps">
|
||||
<a className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900">
|
||||
<Icon.FiArrowLeft className="h-4 w-4" />
|
||||
{t("app_store")}{" "}
|
||||
</a>
|
||||
</Link>
|
||||
{category && (
|
||||
<span className="flex gap-1 text-gray-600">
|
||||
<span> / </span>
|
||||
{t("category_apps", { category: category[0].toUpperCase() + category?.slice(1) })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-16">
|
||||
<div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
{apps.map((app) => {
|
||||
return <AppCard key={app.slug} app={app} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Shell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
const appStore = await getAppRegistry();
|
||||
const paths = appStore.reduce((categories, app) => {
|
||||
if (!categories.includes(app.category)) {
|
||||
categories.push(app.category);
|
||||
}
|
||||
return categories;
|
||||
}, [] as string[]);
|
||||
|
||||
return {
|
||||
paths: paths.map((category) => ({ params: { category } })),
|
||||
fallback: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticProps = async (context: GetStaticPropsContext) => {
|
||||
const category = context.params?.category as AppCategories;
|
||||
|
||||
const appQuery = await prisma.app.findMany({
|
||||
where: {
|
||||
categories: {
|
||||
has: category,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
});
|
||||
|
||||
const dbAppsSlugs = appQuery.map((category) => category.slug);
|
||||
|
||||
const appStore = await getAppRegistry();
|
||||
|
||||
const apps = appStore.filter((app) => dbAppsSlugs.includes(app.slug));
|
||||
return {
|
||||
props: {
|
||||
apps,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { InferGetStaticPropsType } from "next";
|
||||
import Link from "next/link";
|
||||
|
||||
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import Shell from "@calcom/ui/Shell";
|
||||
|
||||
export default function Apps({ categories }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Shell isPublic large>
|
||||
<div className="text-md flex items-center gap-1 px-4 pb-3 pt-3 font-normal md:px-8 lg:px-0 lg:pt-0">
|
||||
<Link href="/apps">
|
||||
<a className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900">
|
||||
<Icon.FiArrowLeft className="h-4 w-4" />
|
||||
{t("app_store")}{" "}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mb-16">
|
||||
<div className="grid h-auto w-full grid-cols-5 gap-3">
|
||||
{categories.map((category) => (
|
||||
<Link key={category.name} href={"/apps/categories/" + category.name}>
|
||||
<a
|
||||
data-testid={`app-store-category-${category.name}`}
|
||||
className="relative flex rounded-sm bg-gray-100 px-6 py-4 sm:block">
|
||||
<div className="self-center">
|
||||
<h3 className="font-medium capitalize">{category.name}</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
{t("number_apps", { count: category.count })}{" "}
|
||||
<Icon.FiArrowRight className="inline-block h-4 w-4" />
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticProps = async () => {
|
||||
const appStore = await getAppRegistry();
|
||||
const categories = appStore.reduce((c, app) => {
|
||||
c[app.category] = c[app.category] ? c[app.category] + 1 : 1;
|
||||
return c;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
return {
|
||||
props: {
|
||||
categories: Object.entries(categories).map(([name, count]) => ({ name, count })),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { InferGetStaticPropsType, NextPageContext } from "next";
|
||||
|
||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { AppCategories } from "@calcom/prisma/client";
|
||||
import AllApps from "@calcom/ui/v2/core/apps/AllApps";
|
||||
import AppStoreCategories from "@calcom/ui/v2/core/apps/Categories";
|
||||
import TrendingAppsSlider from "@calcom/ui/v2/core/apps/TrendingAppsSlider";
|
||||
import AppsLayout from "@calcom/ui/v2/core/layouts/AppsLayout";
|
||||
|
||||
export default function Apps({ appStore, categories }: InferGetStaticPropsType<typeof getServerSideProps>) {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<AppsLayout isPublic heading={t("app_store")} subtitle={t("app_store_description")}>
|
||||
<AppStoreCategories categories={categories} />
|
||||
<TrendingAppsSlider items={appStore} />
|
||||
<AllApps apps={appStore} />
|
||||
</AppsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (context: NextPageContext) => {
|
||||
const session = await getSession(context);
|
||||
|
||||
let appStore;
|
||||
if (session?.user?.id) {
|
||||
appStore = await getAppRegistryWithCredentials(session.user.id);
|
||||
} else {
|
||||
appStore = await getAppRegistry();
|
||||
}
|
||||
|
||||
const categoryQuery = await prisma.app.findMany({
|
||||
select: {
|
||||
categories: true,
|
||||
},
|
||||
});
|
||||
const categories = categoryQuery.reduce((c, app) => {
|
||||
for (const category of app.categories) {
|
||||
c[category] = c[category] ? c[category] + 1 : 1;
|
||||
}
|
||||
return c;
|
||||
}, {} as Record<string, number>);
|
||||
return {
|
||||
props: {
|
||||
categories: Object.entries(categories)
|
||||
.map(([name, count]): { name: AppCategories; count: number } => ({
|
||||
name: name as AppCategories,
|
||||
count,
|
||||
}))
|
||||
.sort(function (a, b) {
|
||||
return b.count - a.count;
|
||||
}),
|
||||
appStore,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -143,6 +143,7 @@
|
||||
"view_public_page": "View public page",
|
||||
"sign_out": "Sign out",
|
||||
"add_another": "Add another",
|
||||
"install_another": "Install another",
|
||||
"until": "until",
|
||||
"powered_by": "powered by",
|
||||
"unavailable": "Unavailable",
|
||||
@@ -215,6 +216,7 @@
|
||||
"forgot": "Forgot?",
|
||||
"done": "Done",
|
||||
"all_done": "All done!",
|
||||
"all_apps": "All",
|
||||
"check_email_reset_password": "Check your email. We sent you a link to reset your password.",
|
||||
"finish": "Finish",
|
||||
"few_sentences_about_yourself": "A few sentences about yourself. This will appear on your personal url page.",
|
||||
@@ -227,6 +229,7 @@
|
||||
"connect_your_calendar_instructions": "Connect your calendar to automatically check for busy times and new events as they’re scheduled.",
|
||||
"set_up_later": "Set up later",
|
||||
"current_time": "Current time",
|
||||
"details": "Details",
|
||||
"welcome": "Welcome",
|
||||
"welcome_back": "Welcome back",
|
||||
"welcome_to_calcom": "Welcome to Cal.com",
|
||||
@@ -698,16 +701,20 @@
|
||||
"delete_account_confirmation_message": "Are you sure you want to delete your Cal.com account? Anyone who you've shared your account link with will no longer be able to book using it and any preferences you have saved will be lost.",
|
||||
"integrations": "Integrations",
|
||||
"apps": "Apps",
|
||||
"category_apps": "{{category}} apps",
|
||||
"app_store": "App Store",
|
||||
"app_store_description": "Connecting people, technology and the workplace.",
|
||||
"settings": "Settings",
|
||||
"event_type_moved_successfully": "Event type has been moved successfully",
|
||||
"next_step": "Skip step",
|
||||
"prev_step": "Prev step",
|
||||
"install": "Install",
|
||||
"installed": "Installed",
|
||||
"active_install_one": "{{count}} active install",
|
||||
"active_install_other": "{{count}} active installs",
|
||||
"globally_install": "Globally installed",
|
||||
"app_successfully_installed": "App successfully installed",
|
||||
"app_could_not_be_installed": "App could not be installed",
|
||||
"disconnect": "Disconnect",
|
||||
"embed_your_calendar": "Embed your calendar within your webpage",
|
||||
"connect_your_favourite_apps": "Connect your favourite apps.",
|
||||
@@ -751,9 +758,12 @@
|
||||
"import_from": "Import from",
|
||||
"access_token": "Access token",
|
||||
"visit_roadmap": "Roadmap",
|
||||
"featured_categories": "Featured Categories",
|
||||
"popular_categories": "Popular Categories",
|
||||
"number_apps_one": "{{count}} App",
|
||||
"number_apps_other": "{{count}} Apps",
|
||||
"trending_apps": "Trending Apps",
|
||||
"all_apps": "All Apps",
|
||||
"explore_apps": "{{category}} apps",
|
||||
"installed_apps": "Installed Apps",
|
||||
"empty_installed_apps_headline": "No apps installed",
|
||||
"empty_installed_apps_description": "Apps enable you to enhance your workflow and improve your scheduling life significantly.",
|
||||
@@ -774,6 +784,8 @@
|
||||
"terms_of_service": "Terms of Service",
|
||||
"remove": "Remove",
|
||||
"add": "Add",
|
||||
"installed_one": "Installed",
|
||||
"installed_other": "{{count}} installed",
|
||||
"verify_wallet": "Verify Wallet",
|
||||
"connect_metamask": "Connect Metamask",
|
||||
"create_events_on": "Create events on:",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Credential } from "@prisma/client";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import { App } from "@calcom/types/App";
|
||||
|
||||
@@ -47,3 +49,31 @@ export async function getAppRegistry() {
|
||||
}
|
||||
return apps;
|
||||
}
|
||||
|
||||
export async function getAppRegistryWithCredentials(userId: number) {
|
||||
const dbApps = await prisma.app.findMany({ include: { credentials: { where: { userId } } } });
|
||||
const apps = [] as (Omit<App, "key"> & {
|
||||
credentials: Credential[];
|
||||
})[];
|
||||
for await (const dbapp of dbApps) {
|
||||
const app = await getAppWithMetadata(dbapp);
|
||||
if (!app) continue;
|
||||
// Skip if app isn't installed
|
||||
/* This is now handled from the DB */
|
||||
// if (!app.installed) return apps;
|
||||
|
||||
const { rating, reviews, trending, verified, ...remainingAppProps } = app;
|
||||
apps.push({
|
||||
rating: rating || 0,
|
||||
reviews: reviews || 0,
|
||||
trending: trending || true,
|
||||
verified: verified || true,
|
||||
...remainingAppProps,
|
||||
categories: dbapp.categories,
|
||||
credentials: dbapp.credentials,
|
||||
installed:
|
||||
true /* All apps from DB are considered installed by default. @TODO: Add and filter our by `enabled` property */,
|
||||
});
|
||||
}
|
||||
return apps;
|
||||
}
|
||||
@@ -6,8 +6,7 @@ import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { App } from "@calcom/types/App";
|
||||
|
||||
import { UpgradeToProDialog } from "@components/UpgradeToProDialog";
|
||||
import { UpgradeToProDialog } from "@calcom/ui/UpgradeToProDialog";
|
||||
|
||||
import { InstallAppButtonMap } from "./apps.browser.generated";
|
||||
import { InstallAppButtonProps } from "./types";
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
/* A `package` should never import from `apps` ↓ */
|
||||
"@components/*": ["../../apps/web/components/*"],
|
||||
/* A `package` should never import from `apps` ↓ */
|
||||
"@lib/*": ["../../apps/web/lib/*"],
|
||||
"@prisma/client/*": ["@calcom/prisma/client/*"]
|
||||
},
|
||||
|
||||
Vendored
+4
-1
@@ -1,4 +1,5 @@
|
||||
import { ButtonBaseProps } from "@calcom/ui/Button";
|
||||
import { ButtonBaseProps as v2ButtonBaseProps } from "@calcom/ui/v2/core/Button";
|
||||
|
||||
export type IntegrationOAuthCallbackState = {
|
||||
returnTo: string;
|
||||
@@ -7,9 +8,11 @@ export type IntegrationOAuthCallbackState = {
|
||||
export interface InstallAppButtonProps {
|
||||
render: (
|
||||
renderProps:
|
||||
| ButtonBaseProps & {
|
||||
| Omit<ButtonBaseProps, "color" | "size"> & {
|
||||
/** Tells that the default render component should be used */
|
||||
useDefaultComponent?: boolean;
|
||||
color?: ButtonBaseProps["color"] & v2ButtonBaseProps["color"];
|
||||
size?: ButtonBaseProps["size"] & v2ButtonBaseProps["size"];
|
||||
}
|
||||
) => JSX.Element;
|
||||
onChanged?: () => unknown;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Trans } from "next-i18next";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import Button from "@calcom/ui/Button";
|
||||
import { Dialog, DialogClose, DialogContent } from "@calcom/ui/Dialog";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
|
||||
import Button from "./Button";
|
||||
import { Dialog, DialogClose, DialogContent } from "./Dialog";
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
export function UpgradeToProDialog({
|
||||
modalOpen,
|
||||
@@ -623,7 +623,7 @@ function SideBar() {
|
||||
<Navigation />
|
||||
</div>
|
||||
|
||||
{/* TODO @Peer_Rich: reintroduce in 2.1
|
||||
{/* TODO @Peer_Rich: reintroduce in 2.1
|
||||
<Tips />
|
||||
*/}
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import type { Credential } from "@prisma/client";
|
||||
import { useRouter } from "next/router";
|
||||
import { UIEvent, useEffect, useRef, useState } from "react";
|
||||
import { ChevronLeft, ChevronRight } from "react-feather";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { App } from "@calcom/types/App";
|
||||
|
||||
import AppCard from "./AppCard";
|
||||
|
||||
export function useShouldShowArrows() {
|
||||
const ref = useRef<HTMLUListElement>(null);
|
||||
const [showArrowScroll, setShowArrowScroll] = useState({
|
||||
left: false,
|
||||
right: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const appCategoryList = ref.current;
|
||||
if (appCategoryList && appCategoryList.scrollWidth > appCategoryList.clientWidth) {
|
||||
setShowArrowScroll({ left: false, right: true });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const calculateScroll = (e: UIEvent<HTMLUListElement>) => {
|
||||
setShowArrowScroll({
|
||||
left: e.currentTarget.scrollLeft > 0,
|
||||
right:
|
||||
Math.floor(e.currentTarget.scrollWidth) - Math.floor(e.currentTarget.offsetWidth) !==
|
||||
Math.floor(e.currentTarget.scrollLeft),
|
||||
});
|
||||
};
|
||||
|
||||
return { ref, calculateScroll, leftVisible: showArrowScroll.left, rightVisible: showArrowScroll.right };
|
||||
}
|
||||
|
||||
type AllAppsPropsType = { apps: (App & { credentials: Credential[] | undefined })[] };
|
||||
|
||||
export default function AllApps({ apps }: AllAppsPropsType) {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
const [appsContainerRef] = useAutoAnimate<HTMLDivElement>();
|
||||
const { ref, calculateScroll, leftVisible, rightVisible } = useShouldShowArrows();
|
||||
const categories = apps
|
||||
.map((app) => app.category)
|
||||
.filter((cat, pos, self) => {
|
||||
return self.indexOf(cat) === pos;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const queryCategory =
|
||||
typeof router.query.category === "string" && categories.includes(router.query.category)
|
||||
? router.query.category
|
||||
: null;
|
||||
setSelectedCategory(queryCategory);
|
||||
}, [router.query.category]);
|
||||
|
||||
return (
|
||||
<div className="mb-16">
|
||||
<div className="mb-4 flex flex-col justify-between lg:flex-row lg:items-center">
|
||||
<h2 className="text-lg font-semibold text-gray-900 ">
|
||||
{t("explore_apps", {
|
||||
category:
|
||||
(selectedCategory && selectedCategory[0].toUpperCase() + selectedCategory.slice(1)) ||
|
||||
t("all_apps"),
|
||||
})}
|
||||
</h2>
|
||||
{leftVisible && (
|
||||
<div className="absolute left-1/2 flex">
|
||||
<div className="flex h-12 w-5 items-center justify-end bg-white">
|
||||
<ChevronLeft className="h-4 w-4 text-gray-500" />
|
||||
</div>
|
||||
<div className="flex h-12 w-5 bg-gradient-to-l from-transparent to-white" />
|
||||
</div>
|
||||
)}
|
||||
<ul
|
||||
className="no-scrollbar mt-3 flex max-w-full space-x-1 overflow-x-auto lg:mt-0 lg:max-w-[50%]"
|
||||
onScroll={(e) => calculateScroll(e)}
|
||||
ref={ref}>
|
||||
<li
|
||||
onClick={() => {
|
||||
router.replace(router.asPath.split("?")[0], undefined, { shallow: true });
|
||||
}}
|
||||
className={classNames(
|
||||
selectedCategory === null ? "bg-gray-200" : "bg-gray-50",
|
||||
"rounded-md px-4 py-2.5 text-sm font-medium text-gray-900 hover:cursor-pointer hover:bg-gray-200"
|
||||
)}>
|
||||
{t("all_apps")}
|
||||
</li>
|
||||
{categories.map((cat, pos) => (
|
||||
<li
|
||||
key={pos}
|
||||
onClick={() => {
|
||||
if (selectedCategory === cat) {
|
||||
router.replace(router.asPath.split("?")[0], undefined, { shallow: true });
|
||||
} else {
|
||||
router.replace(router.asPath.split("?")[0] + `?category=${cat}`, undefined, {
|
||||
shallow: true,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={classNames(
|
||||
selectedCategory === cat ? "bg-gray-200" : "bg-gray-50",
|
||||
"rounded-md px-4 py-2.5 text-sm font-medium text-gray-900 hover:cursor-pointer hover:bg-gray-200"
|
||||
)}>
|
||||
{cat[0].toUpperCase() + cat.slice(1)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{rightVisible && (
|
||||
<div className="absolute right-12 flex">
|
||||
<div className="flex h-12 w-5 bg-gradient-to-r from-transparent to-white" />
|
||||
<div className="flex h-12 w-5 items-center justify-end bg-white">
|
||||
<ChevronRight className="h-4 w-4 text-gray-500" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4" ref={appsContainerRef}>
|
||||
{apps
|
||||
.filter((app) => (selectedCategory !== null ? app.category === selectedCategory : true))
|
||||
.map((app) => (
|
||||
<AppCard key={app.name} app={app} credentials={app.credentials} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import type { Credential } from "@prisma/client";
|
||||
|
||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||
import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { App } from "@calcom/types/App";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import Button from "@calcom/ui/v2/core/Button";
|
||||
|
||||
interface AppCardProps {
|
||||
app: App;
|
||||
credentials?: Credential[];
|
||||
}
|
||||
|
||||
export default function AppCard({ app, credentials }: AppCardProps) {
|
||||
const { t } = useLocale();
|
||||
const { data: user } = trpc.useQuery(["viewer.me"]);
|
||||
|
||||
const mutation = useAddAppMutation(null, {
|
||||
onSuccess: () => {
|
||||
showToast(t("app_successfully_installed"), "success");
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error");
|
||||
},
|
||||
});
|
||||
|
||||
const allowedMultipleInstalls = app.categories && app.categories.indexOf("calendar") > -1;
|
||||
const appAdded = (credentials && credentials.length) || 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex h-64 flex-col rounded-md border border-gray-300 p-5"
|
||||
data-testid={`app-store-app-card-${app.slug}`}>
|
||||
<div className="flex">
|
||||
<img src={app.logo} alt={app.name + " Logo"} className="mb-4 h-12 w-12 rounded-sm" />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<h3 className="font-medium">{app.name}</h3>
|
||||
</div>
|
||||
{/* TODO: add reviews <div className="flex text-sm text-gray-800">
|
||||
<span>{props.rating} stars</span> <StarIcon className="ml-1 mt-0.5 h-4 w-4 text-yellow-600" />
|
||||
<span className="pl-1 text-gray-500">{props.reviews} reviews</span>
|
||||
</div> */}
|
||||
<p
|
||||
className="mt-2 flex-grow text-sm text-gray-500"
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
display: "-webkit-box",
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: "3",
|
||||
}}>
|
||||
{app.description}
|
||||
</p>
|
||||
<div className="mt-5 flex max-w-full flex-row flex-wrap justify-between gap-2">
|
||||
<Button color="secondary" className="flex w-32 flex-grow justify-center" href={"/apps/" + app.slug}>
|
||||
{t("details")}
|
||||
</Button>
|
||||
{app.isGlobal || (credentials && credentials.length > 0 && allowedMultipleInstalls)
|
||||
? !app.isGlobal && (
|
||||
<InstallAppButton
|
||||
type={app.type}
|
||||
isProOnly={app.isProOnly}
|
||||
render={({ useDefaultComponent, ...props }) => {
|
||||
if (useDefaultComponent) {
|
||||
props = {
|
||||
onClick: () => {
|
||||
mutation.mutate({ type: app.type });
|
||||
},
|
||||
};
|
||||
}
|
||||
return (
|
||||
<Button color="secondary" StartIcon={Icon.FiPlus} {...props}>
|
||||
{t("install")}
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: credentials &&
|
||||
credentials.length === 0 && (
|
||||
<InstallAppButton
|
||||
type={app.type}
|
||||
isProOnly={app.isProOnly}
|
||||
render={({ useDefaultComponent, ...props }) => {
|
||||
if (useDefaultComponent) {
|
||||
props = {
|
||||
onClick: () => {
|
||||
mutation.mutate({ type: app.type });
|
||||
},
|
||||
};
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
StartIcon={Icon.FiPlus}
|
||||
color="secondary"
|
||||
data-testid="install-app-button"
|
||||
{...props}>
|
||||
{t("install")}
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-w-44 absolute right-0 mr-4 flex flex-wrap justify-end gap-1">
|
||||
{appAdded > 0 && (
|
||||
<span className="rounded-md bg-green-100 px-2 py-1 text-sm font-normal text-green-800">
|
||||
{t("installed", { count: appAdded })}
|
||||
</span>
|
||||
)}
|
||||
{app.isGlobal && (
|
||||
<span className="flex items-center rounded-md bg-gray-100 px-2 py-1 text-sm font-normal text-gray-800">
|
||||
{t("default")}
|
||||
</span>
|
||||
)}
|
||||
{app.isProOnly && user?.plan === "FREE" && (
|
||||
<span className="flex items-center gap-1 rounded-md bg-orange-100 px-2 py-1 text-sm font-normal text-orange-800">
|
||||
<Icon.FiStar className="h-4 w-4 text-orange-800" />
|
||||
{t("pro")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import Link from "next/link";
|
||||
import { ArrowRight } from "react-feather";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import Slider from "./Slider";
|
||||
|
||||
export default function AppStoreCategories({
|
||||
categories,
|
||||
}: {
|
||||
categories: {
|
||||
name: string;
|
||||
count: number;
|
||||
}[];
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<div className="mb-16">
|
||||
<Slider
|
||||
className="mb-16"
|
||||
title={t("featured_categories")}
|
||||
items={categories}
|
||||
itemKey={(category) => category.name}
|
||||
options={{
|
||||
perView: 5,
|
||||
breakpoints: {
|
||||
768 /* and below */: {
|
||||
perView: 2,
|
||||
},
|
||||
},
|
||||
}}
|
||||
renderItem={(category) => (
|
||||
<Link key={category.name} href={"/apps/categories/" + category.name}>
|
||||
<a
|
||||
data-testid={`app-store-category-${category.name}`}
|
||||
className="relative flex rounded-sm bg-gray-100 px-6 py-4 sm:block">
|
||||
<div className="self-center">
|
||||
<h3 className="font-medium capitalize">{category.name}</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
{t("number_apps", { count: category.count })}{" "}
|
||||
<ArrowRight className="inline-block h-4 w-4" />
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import Glide, { Options } from "@glidejs/glide";
|
||||
import "@glidejs/glide/dist/css/glide.core.min.css";
|
||||
import "@glidejs/glide/dist/css/glide.theme.min.css";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
|
||||
const Slider = <T extends string | unknown>({
|
||||
title = "",
|
||||
className = "",
|
||||
items,
|
||||
itemKey = (item) => `${item}`,
|
||||
renderItem,
|
||||
options = {},
|
||||
}: {
|
||||
title?: string;
|
||||
className?: string;
|
||||
items: T[];
|
||||
itemKey?: (item: T) => string;
|
||||
renderItem?: (item: T) => JSX.Element;
|
||||
options?: Options;
|
||||
}) => {
|
||||
const glide = useRef(null);
|
||||
const slider = useRef<Glide.Properties | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (glide.current) {
|
||||
slider.current = new Glide(glide.current, {
|
||||
type: "carousel",
|
||||
...options,
|
||||
}).mount();
|
||||
}
|
||||
|
||||
return () => slider.current?.destroy();
|
||||
}, [options]);
|
||||
|
||||
return (
|
||||
<div className={`mb-2 ${className}`}>
|
||||
<style jsx global>
|
||||
{`
|
||||
.glide__slide {
|
||||
height: auto !important;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div className="glide" ref={glide}>
|
||||
<div className="flex cursor-default">
|
||||
{title && (
|
||||
<div>
|
||||
<h2 className="mt-0 mb-2 text-lg font-semibold text-gray-900">{title}</h2>
|
||||
</div>
|
||||
)}
|
||||
<div className="glide__arrows ml-auto" data-glide-el="controls">
|
||||
<button data-glide-dir="<" className="mr-4">
|
||||
<Icon.FiArrowLeft className="h-5 w-5 text-gray-600 hover:text-black" />
|
||||
</button>
|
||||
<button data-glide-dir=">">
|
||||
<Icon.FiArrowRight className="h-5 w-5 text-gray-600 hover:text-black" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="glide__track" data-glide-el="track">
|
||||
<ul className="glide__slides">
|
||||
{items.map((item) => {
|
||||
if (typeof renderItem !== "function") return null;
|
||||
return (
|
||||
<li key={itemKey(item)} className="glide__slide h-auto pl-0">
|
||||
{renderItem(item)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Slider;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { App } from "@calcom/types/App";
|
||||
|
||||
import AppCard from "./AppCard";
|
||||
import Slider from "./Slider";
|
||||
|
||||
const TrendingAppsSlider = <T extends App>({ items }: { items: T[] }) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Slider<T>
|
||||
className="mb-16"
|
||||
title={t("trending_apps")}
|
||||
items={items.filter((app) => !!app.trending)}
|
||||
itemKey={(app) => app.name}
|
||||
options={{
|
||||
perView: 3,
|
||||
breakpoints: {
|
||||
768 /* and below */: {
|
||||
perView: 1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
renderItem={(app) => <AppCard app={app} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrendingAppsSlider;
|
||||
@@ -0,0 +1,34 @@
|
||||
import React, { ComponentProps } from "react";
|
||||
|
||||
import HorizontalTabs from "@calcom/ui/v2/core/navigation/tabs/HorizontalTabs";
|
||||
import type { VerticalTabItemProps } from "@calcom/ui/v2/core/navigation/tabs/VerticalTabItem";
|
||||
|
||||
import Shell from "../Shell";
|
||||
|
||||
const tabs: VerticalTabItemProps[] = [
|
||||
{
|
||||
name: "app_store",
|
||||
href: "/apps",
|
||||
},
|
||||
{
|
||||
name: "installed_apps",
|
||||
href: "/apps/installed",
|
||||
},
|
||||
];
|
||||
|
||||
export default function AppsLayout({
|
||||
children,
|
||||
...rest
|
||||
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||
return (
|
||||
<Shell {...rest}>
|
||||
<div className="flex flex-col p-2 md:p-0 xl:flex-row ">
|
||||
<div className="block lg:hidden">
|
||||
<HorizontalTabs tabs={tabs} />
|
||||
</div>
|
||||
<main className="w-full">{children}</main>
|
||||
</div>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
export const getLayout = (page: React.ReactElement) => <AppsLayout>{page}</AppsLayout>;
|
||||
@@ -18,6 +18,7 @@ export type VerticalTabItemProps = {
|
||||
textClassNames?: string;
|
||||
className?: string;
|
||||
isChild?: boolean;
|
||||
hidden?: boolean;
|
||||
} & (
|
||||
| {
|
||||
/** If you want to change query param tabName as per current tab */
|
||||
@@ -58,37 +59,41 @@ const VerticalTabItem: FC<VerticalTabItemProps> = ({ name, href, tabName, info,
|
||||
|
||||
return (
|
||||
<Fragment key={name}>
|
||||
<Link key={name} href={props.disabled ? "#" : newHref}>
|
||||
<a
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
props.textClassNames || "text-sm font-medium leading-none text-gray-600",
|
||||
"group flex h-14 w-64 flex-row items-center rounded-md px-3 py-[10px] hover:bg-gray-100 group-hover:text-gray-700 [&[aria-current='page']]:bg-gray-200 [&[aria-current='page']]:text-gray-900",
|
||||
props.disabled && "pointer-events-none !opacity-30",
|
||||
(isChild || !props.icon) && "ml-9 mr-5 w-auto",
|
||||
!info ? "h-9" : "h-14",
|
||||
props.className
|
||||
)}
|
||||
aria-current={isCurrent ? "page" : undefined}>
|
||||
{props.icon && <props.icon className="mr-[10px] h-[16px] w-[16px] self-start stroke-[2px]" />}
|
||||
<div>
|
||||
<p>{t(name)}</p>
|
||||
{info && <p className="pt-1 text-xs font-normal">{t(info)}</p>}
|
||||
</div>
|
||||
{isCurrent && (
|
||||
<div className="ml-auto self-center">
|
||||
<Icon.FiChevronRight
|
||||
width={20}
|
||||
height={20}
|
||||
className="h-auto w-[20px] stroke-[1.5px] text-gray-700"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
{props.children?.map((child) => (
|
||||
<VerticalTabItem key={child.name} {...child} isChild />
|
||||
))}
|
||||
{!props.hidden && (
|
||||
<>
|
||||
<Link key={name} href={props.disabled ? "#" : newHref}>
|
||||
<a
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
props.textClassNames || "text-sm font-medium leading-none text-gray-600",
|
||||
"group flex h-14 w-64 flex-row items-center rounded-md px-3 py-[10px] hover:bg-gray-100 group-hover:text-gray-700 [&[aria-current='page']]:bg-gray-200 [&[aria-current='page']]:text-gray-900",
|
||||
props.disabled && "pointer-events-none !opacity-30",
|
||||
(isChild || !props.icon) && "ml-9 mr-5 w-auto",
|
||||
!info ? "h-9" : "h-14",
|
||||
props.className
|
||||
)}
|
||||
aria-current={isCurrent ? "page" : undefined}>
|
||||
{props.icon && <props.icon className="mr-[10px] h-[16px] w-[16px] self-start stroke-[2px]" />}
|
||||
<div>
|
||||
<p>{t(name)}</p>
|
||||
{info && <p className="pt-1 text-xs font-normal">{t(info)}</p>}
|
||||
</div>
|
||||
{isCurrent && (
|
||||
<div className="ml-auto self-center">
|
||||
<Icon.FiChevronRight
|
||||
width={20}
|
||||
height={20}
|
||||
className="h-auto w-[20px] stroke-[1.5px] text-gray-700"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
{props.children?.map((child) => (
|
||||
<VerticalTabItem key={child.name} {...child} isChild />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user