From 35459a046b0db5bdf4a1ad43b090a11c2011705b Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Mon, 5 Sep 2022 18:22:28 -0300 Subject: [PATCH] feat: Migrating App Store to v2 (#3911) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-authored-by: Alan 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 * 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 Co-authored-by: zomars Co-authored-by: Hariom Balhara Co-authored-by: Julian Benegas Co-authored-by: Alan Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/web/components/apps/AllApps.tsx | 11 +- apps/web/components/apps/AppCard.tsx | 22 ++- .../components/apps/TrendingAppsSlider.tsx | 14 +- apps/web/components/v2/apps/App.tsx | 6 +- apps/web/middleware.ts | 4 +- apps/web/pages/apps/categories/[category].tsx | 12 +- apps/web/pages/event-types/[type].tsx | 2 +- apps/web/pages/settings/profile.tsx | 3 +- apps/web/pages/v2/apps/[slug]/index.tsx | 2 +- .../pages/v2/apps/categories/[category].tsx | 86 ++++++++++++ apps/web/pages/v2/apps/categories/index.tsx | 57 ++++++++ apps/web/pages/v2/apps/index.tsx | 59 ++++++++ apps/web/public/static/locales/en/common.json | 14 +- packages/app-store/_appRegistry.ts | 30 ++++ packages/app-store/components.tsx | 3 +- packages/app-store/tsconfig.json | 2 + packages/app-store/types.d.ts | 5 +- .../ui}/UpgradeToProDialog.tsx | 7 +- packages/ui/v2/core/Shell.tsx | 2 +- packages/ui/v2/core/apps/AllApps.tsx | 132 ++++++++++++++++++ packages/ui/v2/core/apps/AppCard.tsx | 128 +++++++++++++++++ packages/ui/v2/core/apps/Categories.tsx | 50 +++++++ packages/ui/v2/core/apps/Slider.tsx | 79 +++++++++++ .../ui/v2/core/apps/TrendingAppsSlider.tsx | 29 ++++ packages/ui/v2/core/layouts/AppsLayout.tsx | 34 +++++ .../core/navigation/tabs/VerticalTabItem.tsx | 67 +++++---- 26 files changed, 763 insertions(+), 97 deletions(-) create mode 100644 apps/web/pages/v2/apps/categories/[category].tsx create mode 100644 apps/web/pages/v2/apps/categories/index.tsx create mode 100644 apps/web/pages/v2/apps/index.tsx rename {apps/web/components => packages/ui}/UpgradeToProDialog.tsx (91%) create mode 100644 packages/ui/v2/core/apps/AllApps.tsx create mode 100644 packages/ui/v2/core/apps/AppCard.tsx create mode 100644 packages/ui/v2/core/apps/Categories.tsx create mode 100644 packages/ui/v2/core/apps/Slider.tsx create mode 100644 packages/ui/v2/core/apps/TrendingAppsSlider.tsx create mode 100644 packages/ui/v2/core/layouts/AppsLayout.tsx diff --git a/apps/web/components/apps/AllApps.tsx b/apps/web/components/apps/AllApps.tsx index a1c9231e8b..cfbcca4ec7 100644 --- a/apps/web/components/apps/AllApps.tsx +++ b/apps/web/components/apps/AllApps.tsx @@ -11,16 +11,7 @@ export default function AllApps({ apps }: { apps: App[] }) {

{t("all_apps")}

{apps.map((app) => ( - + ))}
diff --git a/apps/web/components/apps/AppCard.tsx b/apps/web/components/apps/AppCard.tsx index dc1fdc0c49..94a45d492d 100644 --- a/apps/web/components/apps/AppCard.tsx +++ b/apps/web/components/apps/AppCard.tsx @@ -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 ( - + + data-testid={`app-store-app-card-${props.app.slug}`}>
{ // eslint-disable-next-line @next/next/no-img-element - {props.name + {props.app.name }
-

{props.name}

- {props.isProOnly && user?.plan === "FREE" ? ( +

{props.app.name}

+ {props.app.isProOnly && user?.plan === "FREE" ? ( PRO @@ -48,7 +42,7 @@ export default function AppCard(props: AppCardProps) { {props.rating} stars {props.reviews} reviews
*/} -

{props.description}

+

{props.app.description}

); diff --git a/apps/web/components/apps/TrendingAppsSlider.tsx b/apps/web/components/apps/TrendingAppsSlider.tsx index 943e603628..10ec4c0929 100644 --- a/apps/web/components/apps/TrendingAppsSlider.tsx +++ b/apps/web/components/apps/TrendingAppsSlider.tsx @@ -21,20 +21,10 @@ const TrendingAppsSlider = ({ items }: { items: T[] }) => { }, }, }} - renderItem={(app) => ( - - )} + renderItem={(app) => } /> ); }; +/** @deprecated use v2 instead, located at packages/ui/v2/core/apps */ export default TrendingAppsSlider; diff --git a/apps/web/components/v2/apps/App.tsx b/apps/web/components/v2/apps/App.tsx index ae510c0b99..3a5cd1e868 100644 --- a/apps/web/components/v2/apps/App.tsx +++ b/apps/web/components/v2/apps/App.tsx @@ -132,9 +132,7 @@ const Component = ({ isGlobal || (installedAppCount > 0 && allowedMultipleInstalls) ? (
{!isGlobal && ( - {t("add_another")} + {t("install_another")} ); }} diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index b03be521aa..26c42ad037 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -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"]; diff --git a/apps/web/pages/apps/categories/[category].tsx b/apps/web/pages/apps/categories/[category].tsx index 99c3b4c692..1e56914e9e 100644 --- a/apps/web/pages/apps/categories/[category].tsx +++ b/apps/web/pages/apps/categories/[category].tsx @@ -31,17 +31,7 @@ export default function Apps({ apps }: InferGetStaticPropsTypeAll {router.query.category} apps
{apps.map((app) => { - return ( - - ); + return ; })}
diff --git a/apps/web/pages/event-types/[type].tsx b/apps/web/pages/event-types/[type].tsx index c5f736b29f..93884eaaa7 100644 --- a/apps/web/pages/event-types/[type].tsx +++ b/apps/web/pages/event-types/[type].tsx @@ -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"; diff --git a/apps/web/pages/settings/profile.tsx b/apps/web/pages/settings/profile.tsx index ab1b28241f..a9afe6cb54 100644 --- a/apps/web/pages/settings/profile.tsx +++ b/apps/web/pages/settings/profile.tsx @@ -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; function HideBrandingInput(props: { hideBrandingRef: RefObject; user: Props["user"] }) { diff --git a/apps/web/pages/v2/apps/[slug]/index.tsx b/apps/web/pages/v2/apps/[slug]/index.tsx index f2a4cbc925..abf2c7b712 100644 --- a/apps/web/pages/v2/apps/[slug]/index.tsx +++ b/apps/web/pages/v2/apps/[slug]/index.tsx @@ -37,7 +37,7 @@ function SingleAppPage({ data, source }: inferSSRProps) { 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} diff --git a/apps/web/pages/v2/apps/categories/[category].tsx b/apps/web/pages/v2/apps/categories/[category].tsx new file mode 100644 index 0000000000..92191844f9 --- /dev/null +++ b/apps/web/pages/v2/apps/categories/[category].tsx @@ -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) { + const { t } = useLocale(); + const router = useRouter(); + const { category } = router.query; + + return ( + <> + +
+ + + + {t("app_store")}{" "} + + + {category && ( + +  /  + {t("category_apps", { category: category[0].toUpperCase() + category?.slice(1) })} + + )} +
+
+
+ {apps.map((app) => { + return ; + })} +
+
+
+ + ); +} + +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, + }, + }; +}; diff --git a/apps/web/pages/v2/apps/categories/index.tsx b/apps/web/pages/v2/apps/categories/index.tsx new file mode 100644 index 0000000000..5f286b07a0 --- /dev/null +++ b/apps/web/pages/v2/apps/categories/index.tsx @@ -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) { + const { t } = useLocale(); + + return ( + + + + + ); +} + +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); + + return { + props: { + categories: Object.entries(categories).map(([name, count]) => ({ name, count })), + }, + }; +}; diff --git a/apps/web/pages/v2/apps/index.tsx b/apps/web/pages/v2/apps/index.tsx new file mode 100644 index 0000000000..fa735df7b8 --- /dev/null +++ b/apps/web/pages/v2/apps/index.tsx @@ -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) { + const { t } = useLocale(); + + return ( + + + + + + ); +} + +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); + 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, + }, + }; +}; diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index ba9f44ebc7..ecae757995 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -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:", diff --git a/packages/app-store/_appRegistry.ts b/packages/app-store/_appRegistry.ts index d65a520fcb..542e8218df 100644 --- a/packages/app-store/_appRegistry.ts +++ b/packages/app-store/_appRegistry.ts @@ -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 & { + 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; +} diff --git a/packages/app-store/components.tsx b/packages/app-store/components.tsx index 18556f9af9..653dff3191 100644 --- a/packages/app-store/components.tsx +++ b/packages/app-store/components.tsx @@ -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"; diff --git a/packages/app-store/tsconfig.json b/packages/app-store/tsconfig.json index ec53741a1c..b138edc51d 100644 --- a/packages/app-store/tsconfig.json +++ b/packages/app-store/tsconfig.json @@ -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/*"] }, diff --git a/packages/app-store/types.d.ts b/packages/app-store/types.d.ts index 2bfa37c3c1..01a150ba45 100644 --- a/packages/app-store/types.d.ts +++ b/packages/app-store/types.d.ts @@ -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 & { /** 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; diff --git a/apps/web/components/UpgradeToProDialog.tsx b/packages/ui/UpgradeToProDialog.tsx similarity index 91% rename from apps/web/components/UpgradeToProDialog.tsx rename to packages/ui/UpgradeToProDialog.tsx index 4687a82bd5..b5c481ece5 100644 --- a/apps/web/components/UpgradeToProDialog.tsx +++ b/packages/ui/UpgradeToProDialog.tsx @@ -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, diff --git a/packages/ui/v2/core/Shell.tsx b/packages/ui/v2/core/Shell.tsx index d45dc0e9a9..70d555701a 100644 --- a/packages/ui/v2/core/Shell.tsx +++ b/packages/ui/v2/core/Shell.tsx @@ -623,7 +623,7 @@ function SideBar() { - {/* TODO @Peer_Rich: reintroduce in 2.1 + {/* TODO @Peer_Rich: reintroduce in 2.1 */} diff --git a/packages/ui/v2/core/apps/AllApps.tsx b/packages/ui/v2/core/apps/AllApps.tsx new file mode 100644 index 0000000000..e52523492f --- /dev/null +++ b/packages/ui/v2/core/apps/AllApps.tsx @@ -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(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) => { + 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(null); + const [appsContainerRef] = useAutoAnimate(); + 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 ( +
+
+

+ {t("explore_apps", { + category: + (selectedCategory && selectedCategory[0].toUpperCase() + selectedCategory.slice(1)) || + t("all_apps"), + })} +

+ {leftVisible && ( +
+
+ +
+
+
+ )} +
    calculateScroll(e)} + ref={ref}> +
  • { + 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")} +
  • + {categories.map((cat, pos) => ( +
  • { + 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)} +
  • + ))} +
+ {rightVisible && ( +
+
+
+ +
+
+ )} +
+
+ {apps + .filter((app) => (selectedCategory !== null ? app.category === selectedCategory : true)) + .map((app) => ( + + ))} +
+
+ ); +} diff --git a/packages/ui/v2/core/apps/AppCard.tsx b/packages/ui/v2/core/apps/AppCard.tsx new file mode 100644 index 0000000000..aa7babdc6d --- /dev/null +++ b/packages/ui/v2/core/apps/AppCard.tsx @@ -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 ( +
+
+ {app.name +
+
+

{app.name}

+
+ {/* TODO: add reviews
+ {props.rating} stars + {props.reviews} reviews +
*/} +

+ {app.description} +

+
+ + {app.isGlobal || (credentials && credentials.length > 0 && allowedMultipleInstalls) + ? !app.isGlobal && ( + { + if (useDefaultComponent) { + props = { + onClick: () => { + mutation.mutate({ type: app.type }); + }, + }; + } + return ( + + ); + }} + /> + ) + : credentials && + credentials.length === 0 && ( + { + if (useDefaultComponent) { + props = { + onClick: () => { + mutation.mutate({ type: app.type }); + }, + }; + } + return ( + + ); + }} + /> + )} +
+
+ {appAdded > 0 && ( + + {t("installed", { count: appAdded })} + + )} + {app.isGlobal && ( + + {t("default")} + + )} + {app.isProOnly && user?.plan === "FREE" && ( + + + {t("pro")} + + )} +
+
+ ); +} diff --git a/packages/ui/v2/core/apps/Categories.tsx b/packages/ui/v2/core/apps/Categories.tsx new file mode 100644 index 0000000000..fd3f5c4637 --- /dev/null +++ b/packages/ui/v2/core/apps/Categories.tsx @@ -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 ( +
+ category.name} + options={{ + perView: 5, + breakpoints: { + 768 /* and below */: { + perView: 2, + }, + }, + }} + renderItem={(category) => ( + + +
+

{category.name}

+

+ {t("number_apps", { count: category.count })}{" "} + +

+
+
+ + )} + /> +
+ ); +} diff --git a/packages/ui/v2/core/apps/Slider.tsx b/packages/ui/v2/core/apps/Slider.tsx new file mode 100644 index 0000000000..e9885a2e07 --- /dev/null +++ b/packages/ui/v2/core/apps/Slider.tsx @@ -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 = ({ + 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(null); + + useEffect(() => { + if (glide.current) { + slider.current = new Glide(glide.current, { + type: "carousel", + ...options, + }).mount(); + } + + return () => slider.current?.destroy(); + }, [options]); + + return ( +
+ +
+
+ {title && ( +
+

{title}

+
+ )} +
+ + +
+
+
+
    + {items.map((item) => { + if (typeof renderItem !== "function") return null; + return ( +
  • + {renderItem(item)} +
  • + ); + })} +
+
+
+
+ ); +}; + +export default Slider; diff --git a/packages/ui/v2/core/apps/TrendingAppsSlider.tsx b/packages/ui/v2/core/apps/TrendingAppsSlider.tsx new file mode 100644 index 0000000000..46891cc199 --- /dev/null +++ b/packages/ui/v2/core/apps/TrendingAppsSlider.tsx @@ -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 = ({ items }: { items: T[] }) => { + const { t } = useLocale(); + + return ( + + 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) => } + /> + ); +}; + +export default TrendingAppsSlider; diff --git a/packages/ui/v2/core/layouts/AppsLayout.tsx b/packages/ui/v2/core/layouts/AppsLayout.tsx new file mode 100644 index 0000000000..b4229328dd --- /dev/null +++ b/packages/ui/v2/core/layouts/AppsLayout.tsx @@ -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) { + return ( + +
+
+ +
+
{children}
+
+
+ ); +} +export const getLayout = (page: React.ReactElement) => {page}; diff --git a/packages/ui/v2/core/navigation/tabs/VerticalTabItem.tsx b/packages/ui/v2/core/navigation/tabs/VerticalTabItem.tsx index f299a61fba..ce05a702c0 100644 --- a/packages/ui/v2/core/navigation/tabs/VerticalTabItem.tsx +++ b/packages/ui/v2/core/navigation/tabs/VerticalTabItem.tsx @@ -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 = ({ name, href, tabName, info, return ( - - - {props.icon && } -
-

{t(name)}

- {info &&

{t(info)}

} -
- {isCurrent && ( -
- -
- )} -
- - {props.children?.map((child) => ( - - ))} + {!props.hidden && ( + <> + + + {props.icon && } +
+

{t(name)}

+ {info &&

{t(info)}

} +
+ {isCurrent && ( +
+ +
+ )} +
+ + {props.children?.map((child) => ( + + ))} + + )}
); };