Skip to content

Commit fa1b8b8

Browse files
authored
Merge pull request #1082 from gitroomhq/feat/gmb
Add Google My Business
2 parents 98edbc4 + 69e8944 commit fa1b8b8

File tree

13 files changed

+1087
-66
lines changed

13 files changed

+1087
-66
lines changed

apps/backend/src/api/routes/integrations.controller.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,15 @@ export class IntegrationsController {
560560
return this._integrationService.saveLinkedin(org.id, id, body.page);
561561
}
562562

563+
@Post('/gmb/:id')
564+
async saveGmb(
565+
@Param('id') id: string,
566+
@Body() body: { id: string; accountName: string; locationName: string },
567+
@GetOrgFromRequest() org: Organization
568+
) {
569+
return this._integrationService.saveGmb(org.id, id, body);
570+
}
571+
563572
@Post('/enable')
564573
enableChannel(
565574
@GetOrgFromRequest() org: Organization,
16.4 KB
Loading

apps/frontend/src/components/launches/add.provider.component.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { yupResolver } from '@hookform/resolvers/yup';
1717
import { web3List } from '@gitroom/frontend/components/launches/web3/web3.list';
1818
import { useT } from '@gitroom/react/translation/get.transation.service.client';
1919
import { ModalWrapperComponent } from '@gitroom/frontend/components/new-launch/modal.wrapper.component';
20+
import clsx from 'clsx';
2021
const resolver = classValidatorResolver(ApiKeyDto);
2122
export const useAddProvider = (update?: () => void) => {
2223
const modal = useModals();
@@ -457,7 +458,7 @@ export const AddProviderComponent: FC<{
457458
<img src={`/icons/platforms/youtube.svg`} />
458459
) : (
459460
<img
460-
className="w-[32px] h-[32px] rounded-full"
461+
className={clsx("w-[32px] h-[32px]", item.identifier !== 'google_my_business' && 'rounded-full')}
461462
src={`/icons/platforms/${item.identifier}.png`}
462463
/>
463464
)}
Lines changed: 65 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { FC, useCallback, useMemo } from 'react';
1+
import React, { FC, useCallback, useEffect, useMemo } from 'react';
22
import { useRouter, useSearchParams } from 'next/navigation';
33
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
44
import { IntegrationContext } from '@gitroom/frontend/components/launches/helpers/use.integration';
@@ -7,6 +7,7 @@ import useSWR, { useSWRConfig } from 'swr';
77
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
88
import { continueProviderList } from '@gitroom/frontend/components/new-launch/providers/continue-provider/list';
99
import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone';
10+
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
1011
export const Null: FC<{
1112
closeModal: () => void;
1213
existingId: string[];
@@ -46,74 +47,73 @@ export const ContinueProvider: FC = () => {
4647
continueProviderList[added as keyof typeof continueProviderList] || Null
4748
);
4849
}, [added]);
50+
4951
if (!added || !continueId || !integrations) {
5052
return null;
5153
}
54+
5255
return (
53-
<div
54-
className="fixed start-0 top-0 w-full h-full bg-primary/40 z-[499]"
55-
onClick={closeModal}
56+
<ContinueModal
57+
added={added}
58+
continueId={continueId}
59+
integrations={integrations.map((p: any) => p.internalId)}
60+
provider={Provider}
61+
/>
62+
);
63+
};
64+
65+
const ModalContent: FC<{
66+
continueId: string;
67+
added: any;
68+
provider: any;
69+
closeModal: () => void;
70+
integrations: string[];
71+
}> = ({ continueId, added, provider: Provider, closeModal, integrations }) => {
72+
return (
73+
<IntegrationContext.Provider
74+
value={{
75+
date: newDayjs(),
76+
value: [],
77+
allIntegrations: [],
78+
integration: {
79+
editor: 'normal',
80+
additionalSettings: '',
81+
display: '',
82+
time: [
83+
{
84+
time: 0,
85+
},
86+
],
87+
id: continueId,
88+
type: '',
89+
name: '',
90+
picture: '',
91+
inBetweenSteps: true,
92+
changeNickName: false,
93+
changeProfilePicture: false,
94+
identifier: added,
95+
},
96+
}}
5697
>
57-
<div
58-
className="w-[100%] max-w-[674px] absolute start-[50%] top-[65px] bg-customColor3 z-[500] -translate-x-[50%] text-textColor p-[16px] !pt-0 border border-customColor6 min-h-[300px]"
59-
onClick={(e) => e.stopPropagation()}
60-
>
61-
<div className="w-full h-full relative">
62-
<TopTitle title="Configure Provider" />
63-
<button
64-
onClick={closeModal}
65-
className="outline-none absolute end-0 top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
66-
type="button"
67-
>
68-
<svg
69-
viewBox="0 0 15 15"
70-
fill="none"
71-
xmlns="http://www.w3.org/2000/svg"
72-
width="16"
73-
height="16"
74-
>
75-
<path
76-
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
77-
fill="currentColor"
78-
fillRule="evenodd"
79-
clipRule="evenodd"
80-
></path>
81-
</svg>
82-
</button>
83-
<div className="pt-[16px] max-h-[600px] overflow-hidden overflow-y-auto">
84-
<IntegrationContext.Provider
85-
value={{
86-
date: newDayjs(),
87-
value: [],
88-
allIntegrations: [],
89-
integration: {
90-
editor: 'normal',
91-
additionalSettings: '',
92-
display: '',
93-
time: [
94-
{
95-
time: 0,
96-
},
97-
],
98-
id: continueId,
99-
type: '',
100-
name: '',
101-
picture: '',
102-
inBetweenSteps: true,
103-
changeNickName: false,
104-
changeProfilePicture: false,
105-
identifier: added,
106-
},
107-
}}
108-
>
109-
<Provider
110-
closeModal={closeModal}
111-
existingId={integrations.map((p: any) => p.internalId)}
112-
/>
113-
</IntegrationContext.Provider>
114-
</div>
115-
</div>
116-
</div>
117-
</div>
98+
<Provider closeModal={closeModal} existingId={integrations} />
99+
</IntegrationContext.Provider>
118100
);
119101
};
102+
103+
const ContinueModal: FC<{
104+
continueId: string;
105+
added: any;
106+
provider: any;
107+
integrations: string[];
108+
}> = (props) => {
109+
const modals = useModals();
110+
111+
useEffect(() => {
112+
modals.openModal({
113+
title: 'Configure Channel',
114+
children: (close) => <ModalContent {...props} closeModal={close} />,
115+
});
116+
}, []);
117+
118+
return null;
119+
};
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
'use client';
2+
3+
import { FC, useCallback, useMemo, useState } from 'react';
4+
import useSWR from 'swr';
5+
import clsx from 'clsx';
6+
import { Button } from '@gitroom/react/form/button';
7+
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
8+
import { useT } from '@gitroom/react/translation/get.transation.service.client';
9+
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
10+
import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration';
11+
12+
export const GmbContinue: FC<{
13+
closeModal: () => void;
14+
existingId: string[];
15+
}> = (props) => {
16+
const { closeModal, existingId } = props;
17+
const call = useCustomProviderFunction();
18+
const { integration } = useIntegration();
19+
const [location, setSelectedLocation] = useState<null | {
20+
id: string;
21+
accountName: string;
22+
locationName: string;
23+
}>(null);
24+
const fetch = useFetch();
25+
const t = useT();
26+
27+
const loadPages = useCallback(async () => {
28+
try {
29+
const pages = await call.get('pages');
30+
return pages;
31+
} catch (e) {
32+
closeModal();
33+
}
34+
}, []);
35+
36+
const setLocation = useCallback(
37+
(param: { id: string; accountName: string; locationName: string }) => () => {
38+
setSelectedLocation(param);
39+
},
40+
[]
41+
);
42+
43+
const { data, isLoading } = useSWR('load-gmb-locations', loadPages, {
44+
refreshWhenHidden: false,
45+
refreshWhenOffline: false,
46+
revalidateOnFocus: false,
47+
revalidateIfStale: false,
48+
revalidateOnMount: true,
49+
revalidateOnReconnect: false,
50+
refreshInterval: 0,
51+
});
52+
53+
const saveGmb = useCallback(async () => {
54+
await fetch(`/integrations/gmb/${integration?.id}`, {
55+
method: 'POST',
56+
body: JSON.stringify(location),
57+
});
58+
closeModal();
59+
}, [integration, location]);
60+
61+
const filteredData = useMemo(() => {
62+
return (
63+
data?.filter((p: { id: string }) => !existingId.includes(p.id)) || []
64+
);
65+
}, [data, existingId]);
66+
67+
if (!isLoading && !data?.length) {
68+
return (
69+
<div className="text-center flex flex-col justify-center items-center text-[18px] leading-[26px] h-[300px]">
70+
{t(
71+
'gmb_no_locations_found',
72+
"We couldn't find any business locations connected to your account."
73+
)}
74+
<br />
75+
<br />
76+
{t(
77+
'gmb_ensure_business_verified',
78+
'Please ensure your business is verified on Google My Business.'
79+
)}
80+
<br />
81+
<br />
82+
{t(
83+
'gmb_try_again',
84+
'Please close this dialog, delete the integration and try again.'
85+
)}
86+
</div>
87+
);
88+
}
89+
90+
return (
91+
<div className="flex flex-col gap-[20px]">
92+
<div>{t('select_location', 'Select Business Location:')}</div>
93+
<div className="grid grid-cols-3 justify-items-center select-none cursor-pointer gap-[10px]">
94+
{filteredData?.map(
95+
(p: {
96+
id: string;
97+
name: string;
98+
accountName: string;
99+
locationName: string;
100+
picture: {
101+
data: {
102+
url: string;
103+
};
104+
};
105+
}) => (
106+
<div
107+
key={p.id}
108+
className={clsx(
109+
'flex flex-col w-full text-center gap-[10px] border border-input p-[10px] hover:bg-seventh rounded-[8px]',
110+
location?.id === p.id && 'bg-seventh border-primary'
111+
)}
112+
onClick={setLocation({
113+
id: p.id,
114+
accountName: p.accountName,
115+
locationName: p.locationName,
116+
})}
117+
>
118+
<div className="flex justify-center">
119+
{p.picture?.data?.url ? (
120+
<img
121+
className="w-[80px] h-[80px] object-cover rounded-[8px]"
122+
src={p.picture.data.url}
123+
alt={p.name}
124+
/>
125+
) : (
126+
<div className="w-[80px] h-[80px] bg-input rounded-[8px] flex items-center justify-center">
127+
<svg
128+
xmlns="http://www.w3.org/2000/svg"
129+
width="40"
130+
height="40"
131+
viewBox="0 0 24 24"
132+
fill="none"
133+
stroke="currentColor"
134+
strokeWidth="1.5"
135+
strokeLinecap="round"
136+
strokeLinejoin="round"
137+
>
138+
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
139+
<circle cx="12" cy="10" r="3" />
140+
</svg>
141+
</div>
142+
)}
143+
</div>
144+
<div className="text-sm font-medium">{p.name}</div>
145+
</div>
146+
)
147+
)}
148+
</div>
149+
<div>
150+
<Button disabled={!location} onClick={saveGmb}>
151+
{t('save', 'Save')}
152+
</Button>
153+
</div>
154+
</div>
155+
);
156+
};
157+

apps/frontend/src/components/new-launch/providers/continue-provider/list.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
import { InstagramContinue } from '@gitroom/frontend/components/new-launch/providers/continue-provider/instagram/instagram.continue';
44
import { FacebookContinue } from '@gitroom/frontend/components/new-launch/providers/continue-provider/facebook/facebook.continue';
55
import { LinkedinContinue } from '@gitroom/frontend/components/new-launch/providers/continue-provider/linkedin/linkedin.continue';
6+
import { GmbContinue } from '@gitroom/frontend/components/new-launch/providers/continue-provider/gmb/gmb.continue';
7+
68
export const continueProviderList = {
79
instagram: InstagramContinue,
810
facebook: FacebookContinue,
911
'linkedin-page': LinkedinContinue,
12+
gmb: GmbContinue,
1013
};

0 commit comments

Comments
 (0)