├── api ├── Procfile ├── src │ ├── controllers │ │ └── v1 │ │ │ ├── config │ │ │ ├── index.js │ │ │ └── get.action.js │ │ │ ├── health │ │ │ ├── index.js │ │ │ └── get.action.js │ │ │ ├── upload │ │ │ ├── index.js │ │ │ └── post.action.js │ │ │ ├── auth │ │ │ ├── password │ │ │ │ └── index.js │ │ │ ├── index.js │ │ │ ├── post.action.js │ │ │ └── login.action.js │ │ │ ├── transcript │ │ │ ├── index.js │ │ │ └── get.action.js │ │ │ ├── plugin │ │ │ ├── integration │ │ │ │ ├── index.js │ │ │ │ ├── clearbit │ │ │ │ │ └── enrich.action.js │ │ │ │ └── blaze_verify │ │ │ │ │ └── verify.action.js │ │ │ ├── index.js │ │ │ ├── list.action.js │ │ │ ├── post.action.js │ │ │ ├── destroy.action.js │ │ │ ├── get.action.js │ │ │ └── update.action.js │ │ │ ├── faq │ │ │ ├── index.js │ │ │ ├── post.action.js │ │ │ ├── get.action.js │ │ │ ├── list.action.js │ │ │ ├── update.action.js │ │ │ └── destroy.action.js │ │ │ ├── tag │ │ │ ├── index.js │ │ │ ├── list.action.js │ │ │ ├── post.action.js │ │ │ ├── destroy.action.js │ │ │ ├── get.action.js │ │ │ └── update.action.js │ │ │ ├── agent │ │ │ ├── index.js │ │ │ ├── post.action.js │ │ │ ├── list.action.js │ │ │ ├── update.action.js │ │ │ ├── get.action.js │ │ │ └── destroy.action.js │ │ │ ├── invite │ │ │ ├── index.js │ │ │ ├── get.action.js │ │ │ ├── update.action.js │ │ │ ├── destroy.action.js │ │ │ └── list.action.js │ │ │ ├── response │ │ │ ├── index.js │ │ │ ├── list.action.js │ │ │ ├── post.action.js │ │ │ ├── get.action.js │ │ │ ├── destroy.action.js │ │ │ └── update.action.js │ │ │ ├── user │ │ │ ├── index.js │ │ │ ├── get.action.js │ │ │ ├── update.action.js │ │ │ ├── list.action.js │ │ │ ├── destroy.action.js │ │ │ └── post.action.js │ │ │ ├── organization │ │ │ ├── index.js │ │ │ ├── post.action.js │ │ │ ├── list.action.js │ │ │ ├── get.action.js │ │ │ ├── destroy.action.js │ │ │ └── update.action.js │ │ │ └── chat │ │ │ ├── index.js │ │ │ ├── get.action.js │ │ │ ├── update.action.js │ │ │ ├── list.action.js │ │ │ ├── exchange.action.js │ │ │ └── destroy.action.js │ ├── routes │ │ ├── config.js │ │ ├── health.js │ │ ├── transcript.js │ │ ├── upload.js │ │ ├── auth.js │ │ ├── faq.js │ │ ├── tag.js │ │ ├── chat.js │ │ ├── user.js │ │ ├── agent.js │ │ ├── invite.js │ │ ├── response.js │ │ ├── organization.js │ │ └── plugin.js │ ├── utils │ │ ├── controllers.js │ │ ├── email │ │ │ └── transcript.ejs │ │ ├── db │ │ │ └── index.js │ │ ├── stream │ │ │ └── index.js │ │ └── auth │ │ │ └── whitelist.js │ ├── models │ │ ├── tag.js │ │ ├── response.js │ │ ├── faq.js │ │ ├── plugin.js │ │ └── invite.js │ └── index.js ├── jsconfig.json ├── swaggerDef.js ├── .editorconfig ├── ecosystem.dev.config.js ├── ecosystem.prod.config.js ├── .env.example ├── .env.gitpod ├── .eslintrc.json ├── docs │ └── API.md └── .gitignore ├── dashboard ├── public │ ├── _redirects │ ├── robots.txt │ ├── favicon.ico │ ├── favicon-196x196.png │ └── manifest.json ├── src │ ├── screens │ │ ├── Auth │ │ │ ├── index.js │ │ │ ├── forms │ │ │ │ ├── LoginForm │ │ │ │ │ ├── index.js │ │ │ │ │ └── validationSchema.js │ │ │ │ ├── SignUpForm │ │ │ │ │ ├── index.js │ │ │ │ │ └── validationSchema.js │ │ │ │ └── InvitationForm │ │ │ │ │ ├── index.js │ │ │ │ │ └── validationSchema.js │ │ │ ├── views │ │ │ │ ├── Invite.js │ │ │ │ └── Login.js │ │ │ └── Auth.js │ │ ├── Dashboard │ │ │ ├── index.js │ │ │ ├── views │ │ │ │ ├── Agents │ │ │ │ │ ├── index.js │ │ │ │ │ └── views │ │ │ │ │ │ ├── AgentDetail │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── widgets │ │ │ │ │ │ │ ├── TotalThreadsWidget.js │ │ │ │ │ │ │ └── ChatActivityWidget.js │ │ │ │ │ │ └── InviteAgents │ │ │ │ │ │ └── index.js │ │ │ │ ├── Inbox │ │ │ │ │ ├── index.js │ │ │ │ │ ├── views │ │ │ │ │ │ ├── Threads │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ └── Threads.js │ │ │ │ │ │ ├── SideDrawer │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ ├── views │ │ │ │ │ │ │ │ └── InfoDrawer │ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ └── SideDrawer.js │ │ │ │ │ │ └── MessageThread │ │ │ │ │ │ │ └── index.js │ │ │ │ │ └── Inbox.js │ │ │ │ ├── Plugins │ │ │ │ │ ├── index.js │ │ │ │ │ └── Plugins.js │ │ │ │ └── Settings │ │ │ │ │ ├── index.js │ │ │ │ │ ├── views │ │ │ │ │ ├── SettingsList │ │ │ │ │ │ └── index.js │ │ │ │ │ └── SettingsDetail │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── views │ │ │ │ │ │ ├── AppSettings │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── UserSettings │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── UserSettings.js │ │ │ │ │ │ └── OrganizationSettings │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── OrganizationSettings.js │ │ │ │ │ └── Settings.js │ │ │ ├── forms │ │ │ │ ├── PluginForm │ │ │ │ │ └── index.js │ │ │ │ ├── UserSettings │ │ │ │ │ ├── UserProfileForm │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── validationSchema.js │ │ │ │ │ ├── UserAvailabilityForm │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── validationSchema.js │ │ │ │ │ └── index.js │ │ │ │ └── OrganizationSettings │ │ │ │ │ ├── OrganizationProfileForm │ │ │ │ │ ├── index.js │ │ │ │ │ └── validationSchema.js │ │ │ │ │ ├── OrganizationChatSettingsForm │ │ │ │ │ ├── index.js │ │ │ │ │ └── validationSchema.js │ │ │ │ │ └── index.js │ │ │ ├── Dashboard.js │ │ │ └── routes.js │ │ └── Welcome │ │ │ ├── index.js │ │ │ └── Welcome.js │ ├── components │ │ ├── Tabs │ │ │ ├── index.js │ │ │ ├── Tabs.js │ │ │ └── Tab.js │ │ ├── PageSheet │ │ │ ├── index.js │ │ │ ├── PageSheet.js │ │ │ └── SearchHeader.js │ │ ├── Shell │ │ │ ├── Drawer │ │ │ │ ├── index.js │ │ │ │ └── Drawer.js │ │ │ ├── Sidenav │ │ │ │ ├── index.js │ │ │ │ └── SidenavItem.js │ │ │ └── Helmet.js │ │ ├── ListDetailView │ │ │ ├── index.js │ │ │ └── ListDetailView.js │ │ ├── AvailabilityField │ │ │ ├── dayMap.js │ │ │ ├── index.js │ │ │ ├── Availability.js │ │ │ └── AvailabilityDay.js │ │ ├── OrgProtectedRoute.js │ │ ├── AuthedRoute.js │ │ ├── UnauthedRoute.js │ │ ├── SettingsListItem.js │ │ ├── OrganizationCard.js │ │ ├── MobileHeader.js │ │ └── AgentItem.js │ ├── shared │ │ ├── Modal │ │ │ ├── index.js │ │ │ └── Undersheet.js │ │ ├── InputField.js │ │ ├── ColorField.js │ │ ├── AvatarField.js │ │ ├── ScreenRoot.js │ │ ├── Logo.js │ │ ├── SectionTitle.js │ │ ├── InfoWidget.js │ │ ├── SearchInput.js │ │ ├── MenuButton.js │ │ ├── UserBlock.js │ │ ├── IconLabel.js │ │ ├── StreamLogo.js │ │ └── ListItem.js │ ├── widgets │ │ ├── ChatWidget │ │ │ ├── index.js │ │ │ └── ChatWidget.js │ │ ├── Clearbit │ │ │ └── EnrichmentWidget │ │ │ │ └── index.js │ │ ├── BlazeVerify │ │ │ └── EmailVerificationWidget │ │ │ │ └── index.js │ │ └── Combase │ │ │ └── ChatDataWidget.js │ ├── utils │ │ ├── delay.js │ │ ├── history.js │ │ ├── ResizeObserver.js │ │ ├── getNestedProperty.js │ │ ├── upload.js │ │ ├── request.js │ │ └── GetRef.js │ ├── contexts │ │ ├── Agents │ │ │ └── index.js │ │ ├── Auth │ │ │ └── index.js │ │ ├── Plugins │ │ │ └── index.js │ │ ├── Shell │ │ │ └── index.js │ │ ├── ThemeSwitcher │ │ │ ├── index.js │ │ │ └── ThemeSwitcher.js │ │ └── Snackbar │ │ │ ├── index.js │ │ │ └── useSnackbar.js │ ├── sounds │ │ ├── new_message.wav │ │ └── new_conversation.wav │ ├── styles │ │ ├── fonts │ │ │ └── cerebri │ │ │ │ ├── CerebriSansPro-Bold.eot │ │ │ │ ├── CerebriSansPro-Book.eot │ │ │ │ ├── CerebriSansPro-Bold.woff │ │ │ │ ├── CerebriSansPro-Bold.woff2 │ │ │ │ ├── CerebriSansPro-Book.woff │ │ │ │ ├── CerebriSansPro-Book.woff2 │ │ │ │ ├── CerebriSansPro-Heavy.eot │ │ │ │ ├── CerebriSansPro-Heavy.woff │ │ │ │ ├── CerebriSansPro-Heavy.woff2 │ │ │ │ ├── CerebriSansPro-Italic.eot │ │ │ │ ├── CerebriSansPro-Italic.woff │ │ │ │ ├── CerebriSansPro-Light.eot │ │ │ │ ├── CerebriSansPro-Light.woff │ │ │ │ ├── CerebriSansPro-Light.woff2 │ │ │ │ ├── CerebriSansPro-Medium.eot │ │ │ │ ├── CerebriSansPro-Medium.woff │ │ │ │ ├── CerebriSansPro-Regular.eot │ │ │ │ ├── CerebriSansPro-ExtraBold.eot │ │ │ │ ├── CerebriSansPro-Italic.woff2 │ │ │ │ ├── CerebriSansPro-Medium.woff2 │ │ │ │ ├── CerebriSansPro-Regular.woff │ │ │ │ ├── CerebriSansPro-Regular.woff2 │ │ │ │ ├── CerebriSansPro-SemiBold.eot │ │ │ │ ├── CerebriSansPro-SemiBold.woff │ │ │ │ ├── CerebriSansPro-BoldItalic.eot │ │ │ │ ├── CerebriSansPro-BoldItalic.woff │ │ │ │ ├── CerebriSansPro-BoldItalic.woff2 │ │ │ │ ├── CerebriSansPro-BookItalic.eot │ │ │ │ ├── CerebriSansPro-BookItalic.woff │ │ │ │ ├── CerebriSansPro-BookItalic.woff2 │ │ │ │ ├── CerebriSansPro-ExtraBold.woff │ │ │ │ ├── CerebriSansPro-ExtraBold.woff2 │ │ │ │ ├── CerebriSansPro-HeavyItalic.eot │ │ │ │ ├── CerebriSansPro-HeavyItalic.woff │ │ │ │ ├── CerebriSansPro-LightItalic.eot │ │ │ │ ├── CerebriSansPro-LightItalic.woff │ │ │ │ ├── CerebriSansPro-MediumItalic.eot │ │ │ │ ├── CerebriSansPro-SemiBold.woff2 │ │ │ │ ├── CerebriSansPro-HeavyItalic.woff2 │ │ │ │ ├── CerebriSansPro-LightItalic.woff2 │ │ │ │ ├── CerebriSansPro-MediumItalic.woff │ │ │ │ ├── CerebriSansPro-MediumItalic.woff2 │ │ │ │ ├── CerebriSansPro-SemiBoldItalic.eot │ │ │ │ ├── CerebriSansPro-ExtraBoldItalic.eot │ │ │ │ ├── CerebriSansPro-ExtraBoldItalic.woff │ │ │ │ ├── CerebriSansPro-ExtraBoldItalic.woff2 │ │ │ │ ├── CerebriSansPro-SemiBoldItalic.woff │ │ │ │ └── CerebriSansPro-SemiBoldItalic.woff2 │ │ ├── theme │ │ │ ├── breakpoints.js │ │ │ ├── colors.js │ │ │ ├── base.js │ │ │ └── index.js │ │ ├── global.js │ │ └── css │ │ │ ├── pageCard.js │ │ │ ├── baseInputStyle.js │ │ │ └── listItemInteractions.js │ ├── hooks │ │ ├── usePrevious.js │ │ ├── useLayoutProvider.js │ │ ├── useLiveMoment.js │ │ ├── useAuth.js │ │ ├── useAgent.js │ │ ├── useConfig.js │ │ ├── useLayout.js │ │ ├── useMedia.js │ │ ├── usePluginEndpoint.js │ │ ├── useNotifications.js │ │ ├── useInvite.js │ │ ├── usePlugin.js │ │ ├── useActivePlugins.js │ │ ├── useChats.js │ │ ├── useAgents.js │ │ └── usePageSheet.js │ ├── hocs │ │ ├── withAuth.js │ │ ├── asField.js │ │ └── withLayout.js │ ├── setupTests.js │ ├── index.js │ └── App.js ├── .vscode │ └── settings.json ├── jsconfig.json └── .gitignore ├── widget ├── src │ ├── screens │ │ ├── Home │ │ │ ├── index.js │ │ │ └── Home.js │ │ └── Thread │ │ │ ├── index.js │ │ │ ├── views │ │ │ └── MessageThread │ │ │ │ ├── index.js │ │ │ │ └── MessageThread.js │ │ │ └── Thread.js │ ├── components │ │ ├── ListDetailView │ │ │ ├── index.js │ │ │ └── ListDetailView.js │ │ ├── Home │ │ │ ├── ConversationsWidget │ │ │ │ └── index.js │ │ │ └── KnowledgeBaseWidget │ │ │ │ ├── index.js │ │ │ │ └── KnowledgeBaseWidget.js │ │ ├── CardFooter.js │ │ └── CardHeader.js │ ├── contexts │ │ ├── Auth │ │ │ └── index.js │ │ └── ScrollAnimation │ │ │ ├── useScrollAnim.js │ │ │ ├── index.js │ │ │ └── Provider.js │ ├── hocs │ │ └── withAuth.js │ ├── setupTests.js │ ├── index.js │ ├── hooks │ │ ├── useAuth.js │ │ └── useChats.js │ ├── Root.js │ ├── utils │ │ └── request.js │ └── App.js ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ └── manifest.json ├── jsconfig.json ├── .gitignore └── package.json ├── .vscode └── settings.json ├── netlify.toml ├── .gitpod.Dockerfile ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── app.json /api/Procfile: -------------------------------------------------------------------------------- 1 | web: yarn start --only api -------------------------------------------------------------------------------- /dashboard/public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /widget/src/screens/Home/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Home'; -------------------------------------------------------------------------------- /dashboard/src/screens/Auth/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Auth'; 2 | -------------------------------------------------------------------------------- /widget/src/screens/Thread/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Thread'; -------------------------------------------------------------------------------- /api/src/controllers/v1/config/index.js: -------------------------------------------------------------------------------- 1 | export { get } from './get.action'; 2 | -------------------------------------------------------------------------------- /api/src/controllers/v1/health/index.js: -------------------------------------------------------------------------------- 1 | export { get } from './get.action'; 2 | -------------------------------------------------------------------------------- /dashboard/src/components/Tabs/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Tabs'; 2 | -------------------------------------------------------------------------------- /dashboard/src/shared/Modal/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Modal"; 2 | -------------------------------------------------------------------------------- /api/src/controllers/v1/upload/index.js: -------------------------------------------------------------------------------- 1 | export { post } from './post.action'; 2 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Dashboard"; 2 | -------------------------------------------------------------------------------- /dashboard/src/screens/Welcome/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Welcome'; 2 | -------------------------------------------------------------------------------- /dashboard/src/components/PageSheet/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './PageSheet'; 2 | -------------------------------------------------------------------------------- /dashboard/src/components/Shell/Drawer/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Drawer"; 2 | -------------------------------------------------------------------------------- /dashboard/src/components/Shell/Sidenav/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Sidenav"; 2 | -------------------------------------------------------------------------------- /dashboard/src/widgets/ChatWidget/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./ChatWidget"; 2 | -------------------------------------------------------------------------------- /api/src/controllers/v1/auth/password/index.js: -------------------------------------------------------------------------------- 1 | export { reset } from './reset.action'; 2 | -------------------------------------------------------------------------------- /dashboard/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Agents/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Agents"; 2 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Inbox/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Inbox"; 2 | -------------------------------------------------------------------------------- /widget/src/components/ListDetailView/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./ListDetailView"; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "recyclerlistview" 4 | ] 5 | } -------------------------------------------------------------------------------- /dashboard/src/components/ListDetailView/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./ListDetailView"; 2 | -------------------------------------------------------------------------------- /dashboard/src/screens/Auth/forms/LoginForm/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './LoginForm'; 2 | -------------------------------------------------------------------------------- /dashboard/src/screens/Auth/forms/SignUpForm/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './SignUpForm'; 2 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Plugins/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Plugins"; 2 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Settings/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Settings"; 2 | -------------------------------------------------------------------------------- /widget/src/screens/Thread/views/MessageThread/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './MessageThread'; -------------------------------------------------------------------------------- /dashboard/src/components/AvailabilityField/dayMap.js: -------------------------------------------------------------------------------- 1 | export default ['M', 'T', 'W', 'T', 'F', 'S', 'S']; -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/forms/PluginForm/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './PluginForm'; 2 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Inbox/views/Threads/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Threads'; -------------------------------------------------------------------------------- /widget/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /widget/src/components/Home/ConversationsWidget/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './ConversationsWidget'; -------------------------------------------------------------------------------- /widget/src/components/Home/KnowledgeBaseWidget/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './KnowledgeBaseWidget'; -------------------------------------------------------------------------------- /dashboard/src/screens/Auth/forms/InvitationForm/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './InvitationForm'; 2 | -------------------------------------------------------------------------------- /dashboard/src/utils/delay.js: -------------------------------------------------------------------------------- 1 | export default time => new Promise(res => setTimeout(() => res(), time)); 2 | -------------------------------------------------------------------------------- /dashboard/src/widgets/Clearbit/EnrichmentWidget/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./EnrichmentWidget"; 2 | -------------------------------------------------------------------------------- /widget/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/widget/public/favicon.ico -------------------------------------------------------------------------------- /widget/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/widget/public/logo192.png -------------------------------------------------------------------------------- /widget/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/widget/public/logo512.png -------------------------------------------------------------------------------- /dashboard/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/public/favicon.ico -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Inbox/views/SideDrawer/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './SideDrawer'; 2 | -------------------------------------------------------------------------------- /dashboard/src/contexts/Agents/index.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | export default createContext(); 3 | -------------------------------------------------------------------------------- /dashboard/src/contexts/Auth/index.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export default createContext(); -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Agents/views/AgentDetail/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './AgentDetail'; 2 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Agents/views/InviteAgents/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./InviteAgents"; 2 | -------------------------------------------------------------------------------- /api/src/controllers/v1/auth/index.js: -------------------------------------------------------------------------------- 1 | export { login } from './login.action'; 2 | export { post } from './post.action'; 3 | -------------------------------------------------------------------------------- /dashboard/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "clearbit", 4 | "refetch" 5 | ] 6 | } -------------------------------------------------------------------------------- /dashboard/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src" 4 | }, 5 | "include": ["src"] 6 | } 7 | -------------------------------------------------------------------------------- /dashboard/src/contexts/Plugins/index.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export default createContext(); 4 | -------------------------------------------------------------------------------- /dashboard/src/contexts/Shell/index.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export default createContext(); 4 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/forms/UserSettings/UserProfileForm/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './UserProfileForm'; -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Inbox/views/MessageThread/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./MessageThread"; 2 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Settings/views/SettingsList/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./SettingsList"; 2 | -------------------------------------------------------------------------------- /dashboard/src/widgets/BlazeVerify/EmailVerificationWidget/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./EmailVerificationWidget"; 2 | -------------------------------------------------------------------------------- /api/src/controllers/v1/transcript/index.js: -------------------------------------------------------------------------------- 1 | export { get } from './get.action'; 2 | export { post } from './post.action'; 3 | -------------------------------------------------------------------------------- /dashboard/public/favicon-196x196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/public/favicon-196x196.png -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Settings/views/SettingsDetail/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./SettingsDetail"; 2 | -------------------------------------------------------------------------------- /dashboard/src/sounds/new_message.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/sounds/new_message.wav -------------------------------------------------------------------------------- /api/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "*": ["src/*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/forms/UserSettings/UserAvailabilityForm/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './UserAvailabilityForm'; -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Inbox/views/SideDrawer/views/InfoDrawer/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './InfoDrawer'; 2 | -------------------------------------------------------------------------------- /dashboard/src/sounds/new_conversation.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/sounds/new_conversation.wav -------------------------------------------------------------------------------- /api/src/controllers/v1/health/get.action.js: -------------------------------------------------------------------------------- 1 | exports.get = (req, res) => { 2 | res.status(200).json({ code: 200, status: 'OK' }); 3 | }; 4 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/forms/OrganizationSettings/OrganizationProfileForm/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './OrganizationProfileForm'; -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Settings/views/SettingsDetail/views/AppSettings/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./AppSettings"; 2 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Settings/views/SettingsDetail/views/UserSettings/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./UserSettings"; 2 | -------------------------------------------------------------------------------- /widget/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src" 4 | }, 5 | "include": [ 6 | "src" 7 | ] 8 | } -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/forms/OrganizationSettings/OrganizationChatSettingsForm/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './OrganizationChatSettingsForm'; -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Settings/views/SettingsDetail/views/OrganizationSettings/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./OrganizationSettings"; 2 | -------------------------------------------------------------------------------- /dashboard/src/utils/history.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from "history"; 2 | const history = createBrowserHistory(); 3 | export default history; 4 | -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Bold.eot -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Book.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Book.eot -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Bold.woff -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Bold.woff2 -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Book.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Book.woff -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Book.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Book.woff2 -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Heavy.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Heavy.eot -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Heavy.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Heavy.woff -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Heavy.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Heavy.woff2 -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Italic.eot -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Italic.woff -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Light.eot -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Light.woff -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Light.woff2 -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Medium.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Medium.eot -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Medium.woff -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Regular.eot -------------------------------------------------------------------------------- /widget/src/contexts/Auth/index.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export default createContext(); 4 | export { default as AuthProvider } from './AuthProvider'; -------------------------------------------------------------------------------- /dashboard/src/components/AvailabilityField/index.js: -------------------------------------------------------------------------------- 1 | import Availability from './Availability'; 2 | import asField from 'hocs/asField'; 3 | 4 | export default asField(Availability); -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/forms/UserSettings/UserAvailabilityForm/validationSchema.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | 3 | export default Yup.object().shape({ 4 | 5 | }); 6 | -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBold.eot -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Italic.woff2 -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Medium.woff2 -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Regular.woff -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Regular.woff2 -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBold.eot -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBold.woff -------------------------------------------------------------------------------- /api/swaggerDef.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | info: { 3 | title: 'Combase API', 4 | version: '1.0.0', 5 | description: 'API for https://comba.se' 6 | }, 7 | basePath: '/src' 8 | }; 9 | -------------------------------------------------------------------------------- /dashboard/src/contexts/ThemeSwitcher/index.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export default createContext(); 4 | export { default as ThemeSwitcher } from './ThemeSwitcher'; -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-BoldItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-BoldItalic.eot -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-BoldItalic.woff -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-BoldItalic.woff2 -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-BookItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-BookItalic.eot -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-BookItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-BookItalic.woff -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-BookItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-BookItalic.woff2 -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBold.woff -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBold.woff2 -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-HeavyItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-HeavyItalic.eot -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-HeavyItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-HeavyItalic.woff -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-LightItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-LightItalic.eot -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-LightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-LightItalic.woff -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-MediumItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-MediumItalic.eot -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBold.woff2 -------------------------------------------------------------------------------- /dashboard/src/utils/ResizeObserver.js: -------------------------------------------------------------------------------- 1 | import { ResizeObserver as Polyfill } from '@juggle/resize-observer'; 2 | const ResizeObserver = Polyfill; 3 | 4 | export default ResizeObserver; 5 | -------------------------------------------------------------------------------- /api/src/controllers/v1/plugin/integration/index.js: -------------------------------------------------------------------------------- 1 | export { blazeVerifyExecVerify } from './blaze_verify/verify.action'; 2 | export { clearbitExecEnrich } from './clearbit/enrich.action'; 3 | -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-HeavyItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-HeavyItalic.woff2 -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-LightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-LightItalic.woff2 -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-MediumItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-MediumItalic.woff -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-MediumItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-MediumItalic.woff2 -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBoldItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBoldItalic.eot -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBoldItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBoldItalic.eot -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBoldItalic.woff -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBoldItalic.woff2 -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBoldItalic.woff -------------------------------------------------------------------------------- /dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBoldItalic.woff2 -------------------------------------------------------------------------------- /widget/src/contexts/ScrollAnimation/useScrollAnim.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import Context from './index'; 3 | 4 | export default () => { 5 | return useContext(Context) 6 | } -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/forms/UserSettings/index.js: -------------------------------------------------------------------------------- 1 | export { default as UserProfileForm } from './UserProfileForm'; 2 | export { default as UserAvailabilityForm } from './UserAvailabilityForm'; -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | base = "dashboard" 3 | command = "yarn build" 4 | publish = "build" 5 | ignore = "git diff --quiet HEAD^ HEAD api/" 6 | 7 | [template.environment] 8 | SASS_PATH = "./node_modules" -------------------------------------------------------------------------------- /dashboard/src/styles/theme/breakpoints.js: -------------------------------------------------------------------------------- 1 | export default { 2 | xs: 600, // tablet_portrait_up 3 | sm: 900, // tablet_landscape_up 4 | md: 1200, // desktop_up 5 | lg: 1800, // lg_desktop_up, 6 | xl: 2200 7 | }; 8 | -------------------------------------------------------------------------------- /api/src/routes/config.js: -------------------------------------------------------------------------------- 1 | import { get } from '../controllers/v1/config'; 2 | 3 | import { wrapAsync } from '../utils/controllers'; 4 | 5 | module.exports = api => { 6 | api.route('/v1/configs').get(wrapAsync(get)); 7 | }; 8 | -------------------------------------------------------------------------------- /api/src/routes/health.js: -------------------------------------------------------------------------------- 1 | import { get } from '../controllers/v1/health'; 2 | 3 | import { wrapAsync } from '../utils/controllers'; 4 | 5 | module.exports = api => { 6 | api.route('/v1/health').get(wrapAsync(get)); 7 | }; 8 | -------------------------------------------------------------------------------- /api/src/utils/controllers.js: -------------------------------------------------------------------------------- 1 | function wrapAsync(fn) { 2 | return function(req, res, next) { 3 | fn(req, res, next).catch(e => { 4 | next(e); 5 | }); 6 | }; 7 | } 8 | 9 | exports.wrapAsync = wrapAsync; 10 | -------------------------------------------------------------------------------- /dashboard/src/shared/InputField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | import asField from 'hocs/asField'; 3 | import { Input } from '@comba.se/ui'; 4 | 5 | export default asField(Input); 6 | -------------------------------------------------------------------------------- /api/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /dashboard/src/shared/ColorField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | import asField from 'hocs/asField'; 3 | import { ColorInput } from '@comba.se/ui'; 4 | 5 | export default asField(ColorInput); 6 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/forms/OrganizationSettings/index.js: -------------------------------------------------------------------------------- 1 | export { default as OrganizationProfileForm } from './OrganizationProfileForm'; 2 | export { default as OrganizationChatSettingsForm } from './OrganizationChatSettingsForm'; -------------------------------------------------------------------------------- /dashboard/src/shared/AvatarField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | import asField from 'hocs/asField'; 3 | import AvatarInput from 'shared/AvatarInput'; 4 | 5 | export default asField(AvatarInput); 6 | -------------------------------------------------------------------------------- /api/src/controllers/v1/faq/index.js: -------------------------------------------------------------------------------- 1 | export { get } from './get.action'; 2 | export { list } from './list.action'; 3 | export { post } from './post.action'; 4 | export { update } from './update.action'; 5 | export { destroy } from './destroy.action'; 6 | -------------------------------------------------------------------------------- /api/src/controllers/v1/tag/index.js: -------------------------------------------------------------------------------- 1 | export { get } from './get.action'; 2 | export { list } from './list.action'; 3 | export { post } from './post.action'; 4 | export { update } from './update.action'; 5 | export { destroy } from './destroy.action'; 6 | -------------------------------------------------------------------------------- /api/ecosystem.dev.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'api', 5 | script: 'src/index.js', 6 | interpreter: 'babel-node', 7 | watch: true, 8 | ignore_watch: [ '.git', 'node_modules' ] 9 | } 10 | ] 11 | }; 12 | -------------------------------------------------------------------------------- /api/src/controllers/v1/agent/index.js: -------------------------------------------------------------------------------- 1 | export { get } from './get.action'; 2 | export { list } from './list.action'; 3 | export { update } from './update.action'; 4 | export { post } from './post.action'; 5 | export { destroy } from './destroy.action'; 6 | -------------------------------------------------------------------------------- /api/src/controllers/v1/invite/index.js: -------------------------------------------------------------------------------- 1 | export { get } from './get.action'; 2 | export { list } from './list.action'; 3 | export { post } from './post.action'; 4 | export { update } from './update.action'; 5 | export { destroy } from './destroy.action'; 6 | -------------------------------------------------------------------------------- /api/src/controllers/v1/plugin/index.js: -------------------------------------------------------------------------------- 1 | export { get } from './get.action'; 2 | export { list } from './list.action'; 3 | export { post } from './post.action'; 4 | export { update } from './update.action'; 5 | export { destroy } from './destroy.action'; 6 | -------------------------------------------------------------------------------- /api/src/controllers/v1/response/index.js: -------------------------------------------------------------------------------- 1 | export { get } from './get.action'; 2 | export { list } from './list.action'; 3 | export { post } from './post.action'; 4 | export { update } from './update.action'; 5 | export { destroy } from './destroy.action'; 6 | -------------------------------------------------------------------------------- /api/src/controllers/v1/user/index.js: -------------------------------------------------------------------------------- 1 | export { get } from './get.action'; 2 | export { list } from './list.action'; 3 | export { post } from './post.action'; 4 | export { update } from './update.action'; 5 | export { destroy } from './destroy.action'; 6 | -------------------------------------------------------------------------------- /widget/src/contexts/ScrollAnimation/index.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export default createContext(); 4 | export { default as ScrollAnimationProvider } from './Provider'; 5 | export { default as useScrollAnim } from './useScrollAnim'; -------------------------------------------------------------------------------- /api/src/controllers/v1/organization/index.js: -------------------------------------------------------------------------------- 1 | export { list } from './list.action'; 2 | export { get } from './get.action'; 3 | export { update } from './update.action'; 4 | export { post } from './post.action'; 5 | export { destroy } from './destroy.action'; 6 | -------------------------------------------------------------------------------- /dashboard/src/contexts/Snackbar/index.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export default createContext(); 4 | 5 | export { default as SnackbarProvider } from './SnackbarProvider'; 6 | export { default as useSnackbar } from './useSnackbar'; 7 | -------------------------------------------------------------------------------- /dashboard/src/hooks/usePrevious.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export default value => { 4 | const ref = useRef(); 5 | useEffect(() => { 6 | ref.current = value; 7 | }); 8 | return ref.current; 9 | }; 10 | -------------------------------------------------------------------------------- /dashboard/src/utils/getNestedProperty.js: -------------------------------------------------------------------------------- 1 | export default (nestedObj, path) => { 2 | const pathArr = path.split('.'); 3 | return pathArr.reduce( 4 | (obj, key) => (obj && obj[key] !== 'undefined' ? obj[key] : undefined), 5 | nestedObj 6 | ); 7 | }; 8 | -------------------------------------------------------------------------------- /dashboard/src/hocs/withAuth.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useAuth from 'hooks/useAuth'; 3 | 4 | export default WrappedComponent => props => { 5 | const [{ organization, user }] = useAuth(); 6 | return 7 | } -------------------------------------------------------------------------------- /widget/src/hocs/withAuth.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useAuth from 'hooks/useAuth'; 3 | 4 | export default WrappedComponent => props => { 5 | const [{ organization, user }] = useAuth(); 6 | return 7 | } -------------------------------------------------------------------------------- /api/ecosystem.prod.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'api', 5 | script: 'dist/index.js', 6 | instances: process.env.WEB_CONCURRENCY || 1, 7 | exec_mode: 'cluster', 8 | autorestart: true, 9 | watch: false 10 | } 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /dashboard/src/contexts/Snackbar/useSnackbar.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import SnackbarContext from './index'; 3 | 4 | export default () => { 5 | const { addSnackbar, queueSnackbar } = useContext(SnackbarContext); 6 | return { addSnackbar, queueSnackbar }; 7 | }; 8 | -------------------------------------------------------------------------------- /dashboard/src/hocs/asField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useField } from 'formik'; 3 | 4 | export default WrappedInput => props => { 5 | const [field, meta] = useField(props); 6 | return ; 7 | }; 8 | -------------------------------------------------------------------------------- /dashboard/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /widget/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /api/src/controllers/v1/chat/index.js: -------------------------------------------------------------------------------- 1 | export { get } from './get.action'; 2 | export { list } from './list.action'; 3 | export { post } from './post.action'; 4 | export { update } from './update.action'; 5 | export { destroy } from './destroy.action'; 6 | export { exchange } from './exchange.action'; 7 | -------------------------------------------------------------------------------- /api/src/routes/transcript.js: -------------------------------------------------------------------------------- 1 | import { get, post } from '../controllers/v1/transcript'; 2 | 3 | import { wrapAsync } from '../utils/controllers'; 4 | 5 | module.exports = (api) => { 6 | api.route('/v1/transcripts').get(wrapAsync(get)); 7 | api.route('/v1/transcripts').post(wrapAsync(post)); 8 | }; 9 | -------------------------------------------------------------------------------- /api/src/routes/upload.js: -------------------------------------------------------------------------------- 1 | import multer from 'multer'; 2 | 3 | import { post } from '../controllers/v1/upload'; 4 | 5 | import { wrapAsync } from '../utils/controllers'; 6 | 7 | const upload = multer(); 8 | 9 | module.exports = api => { 10 | api.route('/v1/uploads').post(upload.single('file'), post); 11 | }; 12 | -------------------------------------------------------------------------------- /dashboard/src/styles/global.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | import fonts from './fonts'; 3 | 4 | /* eslint no-unused-expressions: 0 */ 5 | export default createGlobalStyle` 6 | ${fonts} 7 | 8 | body { 9 | background-color: ${({ theme }) => theme.color.surface}; 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /dashboard/src/styles/css/pageCard.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | 3 | export default css` 4 | overflow: hidden; 5 | box-shadow: -4px 0px 24px rgba(0, 0, 0, 0.12); 6 | border-top-left-radius: ${({ theme }) => theme.borderRadius}px; 7 | border-bottom-left-radius: ${({ theme }) => theme.borderRadius}px; 8 | `; 9 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/forms/OrganizationSettings/OrganizationChatSettingsForm/validationSchema.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | 3 | export default Yup.object().shape({ 4 | welcome: Yup.object().shape({ 5 | enabled: Yup.bool(), 6 | message: Yup.string() 7 | }), 8 | response: Yup.string(), 9 | }); 10 | -------------------------------------------------------------------------------- /api/src/controllers/v1/tag/list.action.js: -------------------------------------------------------------------------------- 1 | import Tag from 'models/tag'; 2 | 3 | exports.list = async (req, res) => { 4 | try { 5 | const data = req.query; 6 | 7 | const tags = await Tag.apiQuery(data); 8 | res.status(200).json(tags); 9 | } catch (error) { 10 | console.log(error); 11 | res.status(500).json({ error: error.message }); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /api/src/utils/email/transcript.ejs: -------------------------------------------------------------------------------- 1 | <% if (transcript.length) { %> 2 |
    3 | <% transcript.forEach((msg) => { %> 4 |
  • 5 | » <%= msg.name %>
    7 | <%= msg.timestamp %> 8 |

    <%= msg.text %>

    9 |
  • 10 | <% }) %> 11 |
12 | <% } %> 13 | -------------------------------------------------------------------------------- /api/src/controllers/v1/tag/post.action.js: -------------------------------------------------------------------------------- 1 | import Tag from 'models/tag'; 2 | 3 | exports.post = async (req, res) => { 4 | try { 5 | const data = { ...req.body, ...req.params }; 6 | 7 | const tag = await Tag.create(data); 8 | res.status(200).json(tag); 9 | } catch (error) { 10 | console.error(error); 11 | res.status(500).json({ error: error.message }); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /api/src/controllers/v1/faq/post.action.js: -------------------------------------------------------------------------------- 1 | import Faq from 'models/faq'; 2 | 3 | exports.post = async (req, res) => { 4 | try { 5 | const data = { ...req.body, ...req.params }; 6 | 7 | const faq = await Faq.create(data); 8 | 9 | res.status(200).json(faq); 10 | } catch (error) { 11 | console.error(error); 12 | res.status(500).json({ error: error.message }); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /api/src/controllers/v1/tag/destroy.action.js: -------------------------------------------------------------------------------- 1 | import Tag from 'models/tag'; 2 | 3 | exports.destroy = async (req, res) => { 4 | try { 5 | const data = { ...req.body, ...req.params }; 6 | 7 | await Tag.findByIdAndRemove(data.tag); 8 | 9 | res.sendStatus(204); 10 | } catch (error) { 11 | console.error(error); 12 | res.status(500).json({ error: error.message }); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /api/src/controllers/v1/faq/get.action.js: -------------------------------------------------------------------------------- 1 | import Faq from 'models/faq'; 2 | 3 | exports.get = async (req, res) => { 4 | try { 5 | const data = req.params; 6 | 7 | const faq = await Faq.findById(data.faq).lean({ autopopulate: true }); 8 | res.status(200).json(faq); 9 | } catch (error) { 10 | console.error(error); 11 | res.status(500).json({ error: error.message }); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /api/src/controllers/v1/tag/get.action.js: -------------------------------------------------------------------------------- 1 | import Tag from 'models/tag'; 2 | 3 | exports.get = async (req, res) => { 4 | try { 5 | const data = req.params; 6 | 7 | const tag = await Tag.findById(data.tag).lean({ autopopulate: true }); 8 | res.status(200).json(tag); 9 | } catch (error) { 10 | console.error(error); 11 | res.status(500).json({ error: error.message }); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /dashboard/src/screens/Auth/forms/LoginForm/validationSchema.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | 3 | const requiredError = 'This is required.'; 4 | const emailError = 'Invalid Email Address.'; 5 | 6 | export default Yup.object().shape({ 7 | email: Yup.string() 8 | .email(emailError) 9 | .required(requiredError), 10 | password: Yup.string().required(requiredError), 11 | }); 12 | -------------------------------------------------------------------------------- /.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-mongodb 2 | 3 | # Install custom tools, runtimes, etc. 4 | # For example "bastet", a command-line tetris clone: 5 | # RUN brew install bastet 6 | # 7 | # More information: https://www.gitpod.io/docs/config-docker/ 8 | 9 | # Install Redis. 10 | RUN sudo apt-get update \ 11 | && sudo apt-get install -y \ 12 | redis-server \ 13 | && sudo rm -rf /var/lib/apt/lists/* -------------------------------------------------------------------------------- /api/src/controllers/v1/response/list.action.js: -------------------------------------------------------------------------------- 1 | import Response from 'models/response'; 2 | 3 | exports.list = async (req, res) => { 4 | try { 5 | const data = { ...req.body, ...req.params }; 6 | 7 | const responses = await Response.apiQuery(data); 8 | res.status(200).json(responses); 9 | } catch (error) { 10 | console.log(error); 11 | res.status(500).json({ error: error.message }); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /api/src/routes/auth.js: -------------------------------------------------------------------------------- 1 | import { login, post } from '../controllers/v1/auth'; 2 | import { reset } from '../controllers/v1/auth/password'; 3 | 4 | import { wrapAsync } from '../utils/controllers'; 5 | 6 | module.exports = (api) => { 7 | api.route('/v1/auth/login').post(wrapAsync(login)); 8 | api.route('/v1/auth').post(wrapAsync(post)); 9 | api.route('/v1/auth/password-reset').post(wrapAsync(reset)); 10 | }; 11 | -------------------------------------------------------------------------------- /api/src/controllers/v1/faq/list.action.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | import Faq from 'models/faq'; 4 | 5 | exports.list = async (req, res) => { 6 | try { 7 | const data = { ...req.body, ...req.params }; 8 | 9 | const faqs = await Faq.apiQuery(data); 10 | res.status(200).json(faqs); 11 | } catch (error) { 12 | console.log(error); 13 | res.status(500).json({ error: error.message }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /api/src/controllers/v1/response/post.action.js: -------------------------------------------------------------------------------- 1 | import Response from 'models/response'; 2 | 3 | exports.post = async (req, res) => { 4 | try { 5 | const data = { ...req.body, ...req.params }; 6 | 7 | const response = await Response.create(data); 8 | 9 | res.status(200).json(response); 10 | } catch (error) { 11 | console.error(error); 12 | res.status(500).json({ error: error.message }); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /api/src/controllers/v1/user/get.action.js: -------------------------------------------------------------------------------- 1 | import User from 'models/user'; 2 | 3 | exports.get = async (req, res) => { 4 | try { 5 | const data = req.params; 6 | 7 | const user = await User.findById(data.user).lean({ 8 | autopopulate: true, 9 | }); 10 | res.status(200).json(user); 11 | } catch (error) { 12 | console.error(error); 13 | res.status(500).json({ error: error.message }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /api/src/controllers/v1/agent/post.action.js: -------------------------------------------------------------------------------- 1 | import Agent from 'models/agent'; 2 | 3 | exports.post = async (req, res) => { 4 | try { 5 | const data = req.body; 6 | 7 | const agent = await Agent.findOneOrCreate({ email: data.email.toLowerCase() }, data); 8 | 9 | res.status(200).json(agent); 10 | } catch (error) { 11 | console.error(error); 12 | res.status(500).json({ error: error.message }); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /api/src/controllers/v1/invite/get.action.js: -------------------------------------------------------------------------------- 1 | import Invite from 'models/invite'; 2 | 3 | exports.get = async (req, res) => { 4 | try { 5 | const data = req.params; 6 | const invite = await Invite.findById(data.invite).lean({ 7 | autopopulate: true, 8 | }); 9 | res.status(200).json(invite); 10 | } catch (error) { 11 | console.error(error); 12 | res.status(500).json({ error: error.message }); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /dashboard/src/components/Shell/Helmet.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import Helmet from 'react-helmet'; 3 | import { ThemeContext } from 'styled-components'; 4 | 5 | export default () => { 6 | const theme = useContext(ThemeContext); 7 | return ( 8 | 9 | 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /widget/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /api/src/controllers/v1/tag/update.action.js: -------------------------------------------------------------------------------- 1 | import Tag from 'models/tag'; 2 | 3 | exports.update = async (req, res) => { 4 | try { 5 | const data = req.body; 6 | const params = req.params; 7 | 8 | const tag = await Tag.updateOne({ _id: params.tag }, { $set: data }); 9 | res.status(200).json(tag); 10 | } catch (error) { 11 | console.error(error); 12 | res.status(500).json({ error: error.message }); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /api/src/controllers/v1/chat/get.action.js: -------------------------------------------------------------------------------- 1 | import Chat from 'models/chat'; 2 | 3 | exports.get = async (req, res) => { 4 | try { 5 | const data = { ...req.body, ...req.params }; 6 | 7 | const chat = await Chat.findById(data.chat).lean({ 8 | autopopulate: true, 9 | }); 10 | res.status(200).json(chat); 11 | } catch (error) { 12 | console.error(error); 13 | res.status(500).json({ error: error.message }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /api/src/controllers/v1/faq/update.action.js: -------------------------------------------------------------------------------- 1 | import Faq from 'models/faq'; 2 | 3 | exports.update = async (req, res) => { 4 | try { 5 | const data = req.body; 6 | const params = req.params; 7 | 8 | const faq = await Faq.updateOne({ _id: params.faq }, { $set: data }); 9 | 10 | res.status(200).json(faq); 11 | } catch (error) { 12 | console.error(error); 13 | res.status(500).json({ error: error.message }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /api/src/controllers/v1/organization/post.action.js: -------------------------------------------------------------------------------- 1 | import Organization from 'models/organization'; 2 | 3 | exports.post = async (req, res) => { 4 | try { 5 | const data = { ...req.body, ...req.params }; 6 | 7 | const organization = await Organization.create(data); 8 | 9 | res.status(200).json(organization); 10 | } catch (error) { 11 | console.error(error); 12 | res.status(500).json({ error: error.message }); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /dashboard/src/styles/theme/colors.js: -------------------------------------------------------------------------------- 1 | // TODO: need more colors 2 | // for dark theme. 3 | export default { 4 | yellow: "#FFD47D", 5 | blue: '#4D7CFE', 6 | red: '#FF5B5B', 7 | green: '#43FC99', 8 | white: '#ffffff', 9 | slate: '#606264', 10 | light_gray: '#EFF1F5', 11 | gray: '#C6C6C8', 12 | off_white: '#FCFCFC', 13 | black: '#302F32', 14 | true_black: '#000000', 15 | stream: '#4D7CFE', 16 | }; 17 | -------------------------------------------------------------------------------- /api/src/controllers/v1/chat/update.action.js: -------------------------------------------------------------------------------- 1 | import Chat from 'models/chat'; 2 | 3 | exports.update = async (req, res) => { 4 | try { 5 | const data = req.body; 6 | const params = req.params; 7 | 8 | const chat = await Chat.updateOne({ _id: params.chat }, { $set: data }); 9 | 10 | res.status(200).json(chat); 11 | } catch (error) { 12 | console.error(error); 13 | res.status(500).json({ error: error.message }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /api/src/controllers/v1/response/get.action.js: -------------------------------------------------------------------------------- 1 | import Response from 'models/response'; 2 | 3 | exports.get = async (req, res) => { 4 | try { 5 | const data = req.params; 6 | 7 | const response = await Response.findById(data.response).lean({ 8 | autopopulate: true, 9 | }); 10 | res.status(200).json(response); 11 | } catch (error) { 12 | console.error(error); 13 | res.status(500).json({ error: error.message }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /api/src/controllers/v1/user/update.action.js: -------------------------------------------------------------------------------- 1 | import User from 'models/user'; 2 | 3 | exports.update = async (req, res) => { 4 | try { 5 | const data = req.body; 6 | const params = req.params; 7 | 8 | const user = await User.updateOne({ _id: params.user }, { $set: data }); 9 | 10 | res.status(200).json(user); 11 | } catch (error) { 12 | console.error(error); 13 | res.status(500).json({ error: error.message }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /api/src/controllers/v1/organization/list.action.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | import Organization from 'models/organization'; 4 | 5 | exports.list = async (req, res) => { 6 | try { 7 | const data = req.query; 8 | 9 | const organizations = await Organization.apiQuery(data); 10 | res.status(200).json(organizations); 11 | } catch (error) { 12 | console.log(error); 13 | res.status(500).json({ error: error.message }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /api/src/controllers/v1/invite/update.action.js: -------------------------------------------------------------------------------- 1 | import Invite from 'models/invite'; 2 | 3 | exports.update = async (req, res) => { 4 | try { 5 | const data = req.body; 6 | const params = req.params; 7 | 8 | const invite = await Invite.updateOne({ _id: params.invite }, { $set: data }); 9 | 10 | res.status(200).json(invite); 11 | } catch (error) { 12 | console.error(error); 13 | res.status(500).json({ error: error.message }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /dashboard/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Stream", 3 | "short_name": "Stream", 4 | "url": "https://getstream.io", 5 | "icons": [ 6 | { 7 | "src": "foo", 8 | "sizes": "16x16", 9 | "type": "image/x-icon" 10 | }, 11 | { 12 | "src": "bar", 13 | "type": "image/png", 14 | "sizes": "512x512" 15 | } 16 | ], 17 | "start_url": ".", 18 | "display": "standalone", 19 | "theme_color": "baz", 20 | "background_color": "bingo" 21 | } 22 | -------------------------------------------------------------------------------- /widget/src/components/CardFooter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | // Components // 5 | const Root = styled.div` 6 | padding: 16px; 7 | flex-direction: row; 8 | justify-content: space-between; 9 | align-items: center; 10 | `; 11 | 12 | const CardFooter = ({ children }) => ( 13 | 14 |
15 | {children} 16 | 17 | ); 18 | 19 | export default CardFooter; -------------------------------------------------------------------------------- /api/src/routes/faq.js: -------------------------------------------------------------------------------- 1 | import { get, list, post, put, destroy } from '../controllers/v1/faq'; 2 | 3 | import { wrapAsync } from '../utils/controllers'; 4 | 5 | module.exports = api => { 6 | api.route('/v1/faqs').get(wrapAsync(list)); 7 | api.route('/v1/faqs/:faq').get(wrapAsync(get)); 8 | api.route('/v1/faqs/:faq').put(wrapAsync(put)); 9 | api.route('/v1/faqs').post(wrapAsync(post)); 10 | api.route('/v1/faqs/:faq').delete(wrapAsync(destroy)); 11 | }; 12 | -------------------------------------------------------------------------------- /api/src/routes/tag.js: -------------------------------------------------------------------------------- 1 | import { get, list, post, put, destroy } from '../controllers/v1/tag'; 2 | 3 | import { wrapAsync } from '../utils/controllers'; 4 | 5 | module.exports = api => { 6 | api.route('/v1/tags').get(wrapAsync(list)); 7 | api.route('/v1/tags/:tag').get(wrapAsync(get)); 8 | api.route('/v1/tags/:tag').put(wrapAsync(put)); 9 | api.route('/v1/tags').post(wrapAsync(post)); 10 | api.route('/v1/tags/:tag').delete(wrapAsync(destroy)); 11 | }; 12 | -------------------------------------------------------------------------------- /api/src/routes/chat.js: -------------------------------------------------------------------------------- 1 | import { get, list, post, put, destroy } from '../controllers/v1/chat'; 2 | 3 | import { wrapAsync } from '../utils/controllers'; 4 | 5 | module.exports = api => { 6 | api.route('/v1/chats').get(wrapAsync(list)); 7 | api.route('/v1/chats/:chat').get(wrapAsync(get)); 8 | api.route('/v1/chats/:chat').put(wrapAsync(put)); 9 | api.route('/v1/chats').post(wrapAsync(post)); 10 | api.route('/v1/chats/:chat').delete(wrapAsync(destroy)); 11 | }; 12 | -------------------------------------------------------------------------------- /api/src/routes/user.js: -------------------------------------------------------------------------------- 1 | import { get, list, post, put, destroy } from '../controllers/v1/user'; 2 | 3 | import { wrapAsync } from '../utils/controllers'; 4 | 5 | module.exports = api => { 6 | api.route('/v1/users').get(wrapAsync(list)); 7 | api.route('/v1/users/:user').get(wrapAsync(get)); 8 | api.route('/v1/users/:user').put(wrapAsync(put)); 9 | api.route('/v1/users').post(wrapAsync(post)); 10 | api.route('/v1/users/:user').delete(wrapAsync(destroy)); 11 | }; 12 | -------------------------------------------------------------------------------- /api/src/controllers/v1/response/destroy.action.js: -------------------------------------------------------------------------------- 1 | import Response from 'models/response'; 2 | 3 | exports.destroy = async (req, res) => { 4 | try { 5 | const data = { ...req.body, ...req.params }; 6 | 7 | const response = await Response.updateOne( 8 | { _id: data.faq }, 9 | { $set: data } 10 | ); 11 | res.status(200).json(response); 12 | } catch (error) { 13 | console.error(error); 14 | res.status(500).json({ error: error.message }); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /api/src/routes/agent.js: -------------------------------------------------------------------------------- 1 | import { get, list, post, update, destroy } from '../controllers/v1/agent'; 2 | 3 | import { wrapAsync } from '../utils/controllers'; 4 | 5 | module.exports = api => { 6 | api.route('/v1/agents').get(wrapAsync(list)); 7 | api.route('/v1/agents/:agent').get(wrapAsync(get)); 8 | api.route('/v1/agents/:agent').put(wrapAsync(update)); 9 | api.route('/v1/agents').post(wrapAsync(post)); 10 | api.route('/v1/agents/:agent').delete(wrapAsync(destroy)); 11 | }; 12 | -------------------------------------------------------------------------------- /widget/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import * as serviceWorker from './serviceWorker'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | 8 | // If you want your app to work offline and load faster, you can change 9 | // unregister() to register() below. Note this comes with some pitfalls. 10 | // Learn more about service workers: https://bit.ly/CRA-PWA 11 | serviceWorker.unregister(); 12 | -------------------------------------------------------------------------------- /api/src/controllers/v1/response/update.action.js: -------------------------------------------------------------------------------- 1 | import Response from 'models/response'; 2 | 3 | exports.update = async (req, res) => { 4 | try { 5 | const data = req.body; 6 | const params = req.params; 7 | 8 | const response = await Response.updateOne( 9 | { _id: params.response }, 10 | { $set: data } 11 | ); 12 | 13 | res.status(200).json(response); 14 | } catch (error) { 15 | console.error(error); 16 | res.status(500).json({ error: error.message }); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /api/src/controllers/v1/user/list.action.js: -------------------------------------------------------------------------------- 1 | import User from 'models/user'; 2 | 3 | exports.list = async (req, res) => { 4 | try { 5 | const data = req.query; 6 | 7 | const users = await User.apiQuery(data); 8 | 9 | const sanitized = users.map((user) => { 10 | user.password = undefined; 11 | return user; 12 | }); 13 | 14 | res.status(200).json(sanitized); 15 | } catch (error) { 16 | console.log(error); 17 | res.status(500).json({ error: error.message }); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /api/src/routes/invite.js: -------------------------------------------------------------------------------- 1 | import { get, list, post, update, destroy } from '../controllers/v1/invite'; 2 | 3 | import { wrapAsync } from '../utils/controllers'; 4 | 5 | module.exports = api => { 6 | api.route('/v1/invites').get(wrapAsync(list)); 7 | api.route('/v1/invites/:invite').get(wrapAsync(get)); 8 | api.route('/v1/invites/:invite').put(wrapAsync(update)); 9 | api.route('/v1/invites').post(wrapAsync(post)); 10 | api.route('/v1/invites/:invite').delete(wrapAsync(destroy)); 11 | }; 12 | -------------------------------------------------------------------------------- /api/src/routes/response.js: -------------------------------------------------------------------------------- 1 | import { get, list, post, put, destroy } from '../controllers/v1/response'; 2 | 3 | import { wrapAsync } from '../utils/controllers'; 4 | 5 | module.exports = api => { 6 | api.route('/v1/responses').get(wrapAsync(list)); 7 | api.route('/v1/responses/:response').get(wrapAsync(get)); 8 | api.route('/v1/responses/:response').put(wrapAsync(put)); 9 | api.route('/v1/responses').post(wrapAsync(post)); 10 | api.route('/v1/responses/:response').delete(wrapAsync(destroy)); 11 | }; 12 | -------------------------------------------------------------------------------- /dashboard/src/hooks/useLayoutProvider.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export default (LayoutUtil, itemHeight) => { 4 | const [{ width }, onResize] = useState({ width: 0 }); 5 | const [layoutProvider, setLayoutProvider] = useState( 6 | LayoutUtil.getLayoutProvider(width, itemHeight) 7 | ); 8 | useEffect(() => { 9 | setLayoutProvider(LayoutUtil.getLayoutProvider(width, itemHeight)); 10 | }, [width]); 11 | return [layoutProvider, onResize]; 12 | }; 13 | -------------------------------------------------------------------------------- /widget/src/screens/Thread/Thread.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | // Views // 5 | import MessageThread from './views/MessageThread'; 6 | 7 | // Components // 8 | const Root = styled.div` 9 | flex: 1; 10 | ` 11 | 12 | const Thread = ({ match, ...props }) => { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default Thread; -------------------------------------------------------------------------------- /dashboard/src/index.js: -------------------------------------------------------------------------------- 1 | import 'whatwg-fetch'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import App from './App'; 6 | import * as serviceWorker from './serviceWorker'; 7 | 8 | ReactDOM.render(, document.getElementById('root')); 9 | 10 | // If you want your app to work offline and load faster, you can change 11 | // unregister() to register() below. Note this comes with some pitfalls. 12 | // Learn more about service workers: https://bit.ly/CRA-PWA 13 | serviceWorker.unregister(); 14 | -------------------------------------------------------------------------------- /api/src/controllers/v1/agent/list.action.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | import Agent from 'models/agent'; 4 | 5 | exports.list = async (req, res) => { 6 | try { 7 | const data = req.query; 8 | 9 | const agents = await Agent.apiQuery(data); 10 | 11 | const sanitized = agents.map(agent => { 12 | agent.password = undefined; 13 | return agent; 14 | }); 15 | 16 | res.status(200).json(sanitized); 17 | } catch (error) { 18 | console.error(error); 19 | res.status(500).json({ error: error.message }); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /dashboard/src/hooks/useLiveMoment.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import moment from "moment"; 3 | 4 | export default (time = Date.now(), interval = 10000) => { 5 | const [momentValue, setMoment] = useState(moment(time)); 6 | const tick = useCallback(() => { 7 | setMoment(moment(time)); 8 | }, [time]); 9 | 10 | useEffect(() => { 11 | const timer = setInterval(tick, interval); 12 | return () => clearInterval(timer); 13 | }, [tick, interval]); 14 | 15 | return momentValue; 16 | }; 17 | -------------------------------------------------------------------------------- /dashboard/src/shared/ScreenRoot.js: -------------------------------------------------------------------------------- 1 | import React from "react"; // eslint-disable-line no-unused-vars 2 | import styled from "styled-components"; 3 | 4 | // CSS // 5 | import pageCard from "styles/css/pageCard"; 6 | 7 | const ScreenRoot = styled.div` 8 | z-index: ${({ theme }) => theme.z.page}; 9 | 10 | @media (min-width: ${({ theme }) => theme.breakpoints.sm}px) { 11 | position: absolute; 12 | top: 0; 13 | right: 0; 14 | bottom: 0; 15 | left: 96px; 16 | ${pageCard} 17 | } 18 | `; 19 | 20 | export default ScreenRoot; 21 | -------------------------------------------------------------------------------- /api/src/routes/organization.js: -------------------------------------------------------------------------------- 1 | import { get, list, post, update, destroy } from '../controllers/v1/organization'; 2 | 3 | import { wrapAsync } from '../utils/controllers'; 4 | 5 | module.exports = api => { 6 | api.route('/v1/organizations').get(wrapAsync(list)); 7 | api.route('/v1/organizations/:organization').get(wrapAsync(get)); 8 | api.route('/v1/organizations/:organization').put(wrapAsync(update)); 9 | api.route('/v1/organizations').post(wrapAsync(post)); 10 | api.route('/v1/organizations/:organization').delete(wrapAsync(destroy)); 11 | }; 12 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/forms/UserSettings/UserProfileForm/validationSchema.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | 3 | const requiredError = 'This is required.'; 4 | const emailError = 'Invalid email address.'; 5 | 6 | export default Yup.object().shape({ 7 | name: Yup.object().shape({ 8 | first: Yup.string().required(requiredError), 9 | last: Yup.string().required(requiredError), 10 | }), 11 | email: Yup.string().email(emailError).required(requiredError), 12 | title: Yup.string().required(requiredError), 13 | }); 14 | -------------------------------------------------------------------------------- /api/.env.example: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | NODE_ENV=development 3 | 4 | APP_BASE=api/ 5 | 6 | AUTH_SECRET=YOUR_AUTH_SECRET 7 | 8 | ALGOLIASEARCH_API_KEY=YOUR_ALGOLIA_API_KEY 9 | ALGOLIASEARCH_API_KEY_SEARCH=YOUR_ALGOLIA_SEARCH_API_KEY 10 | ALGOLIASEARCH_APPLICATION_ID=YOUR_ALGOLIA_APP_ID 11 | 12 | MONGODB_URI=YOUR_MONGODB_URI 13 | REDIS_URL=YOUR_REDIS_URL 14 | 15 | STREAM_URL=YOUR_STREAM_URL 16 | CLOUDINARY_URL=YOUR_CLOUDINARY_URL 17 | PAPERTRAIL_API_TOKEN=YOUR_PAPERTRAIL_API_TOKEN 18 | 19 | MAILGUN_API_KEY=YOUR_MAILGUN_API_KEY 20 | MAILGUN_DOMAIN=YOUR_MAILGUN_DOMAIN -------------------------------------------------------------------------------- /dashboard/src/styles/theme/base.js: -------------------------------------------------------------------------------- 1 | import breakpoints from "./breakpoints"; 2 | import * as colorUtils from "./colorUtils"; 3 | 4 | export default { 5 | borderRadius: 8, 6 | breakpoints, 7 | colorUtils, 8 | easing: { 9 | accelerate: [0.4, 0.0, 1, 1], 10 | deccelerate: [0.0, 0.0, 0.2, 1], 11 | standard: [0.4, 0.0, 0.2, 1], 12 | css: easing => `cubic-bezier(${easing.join(",")})` 13 | }, 14 | gutter: 16, 15 | z: { 16 | page: 0, 17 | sidenav: 1, 18 | modal: 5, 19 | snackbar: 6, 20 | tooltip: 7 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /api/src/controllers/v1/config/get.action.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | import StreamClient from 'utils/stream'; 4 | 5 | exports.get = async (req, res) => { 6 | try { 7 | const { key } = await StreamClient(); 8 | 9 | res.status(200).json({ 10 | stream: { 11 | key 12 | }, 13 | algolia: { 14 | id: process.env.ALGOLIASEARCH_APPLICATION_ID, 15 | key: process.env.ALGOLIASEARCH_API_KEY_SEARCH 16 | } 17 | }); 18 | } catch (error) { 19 | console.error(error); 20 | res.status(500).json({ error: error.message }); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Agents/views/AgentDetail/widgets/TotalThreadsWidget.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text } from '@comba.se/ui'; 3 | 4 | // Components // 5 | import InfoWidget from 'shared/InfoWidget'; 6 | 7 | const ThreadCount = () => { 8 | return ( 9 | 10 | 12 11 | 12 | ); 13 | }; 14 | 15 | const TotalThreadsWidget = () => { 16 | return ; 17 | }; 18 | 19 | export default TotalThreadsWidget; 20 | -------------------------------------------------------------------------------- /api/.env.gitpod: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | NODE_ENV=development 3 | 4 | APP_BASE=api/ 5 | 6 | AUTH_SECRET=YOUR_AUTH_SECRET 7 | 8 | ALGOLIASEARCH_API_KEY=YOUR_ALGOLIA_API_KEY 9 | ALGOLIASEARCH_API_KEY_SEARCH=YOUR_ALGOLIA_SEARCH_API_KEY 10 | ALGOLIASEARCH_APPLICATION_ID=YOUR_ALGOLIA_APP_ID 11 | 12 | MONGODB_URI=mongodb://localhost:27017/combase 13 | REDIS_URL=localhost:6379 14 | 15 | STREAM_URL=YOUR_STREAM_URL 16 | CLOUDINARY_URL=YOUR_CLOUDINARY_URL 17 | PAPERTRAIL_API_TOKEN=YOUR_PAPERTRAIL_API_TOKEN 18 | 19 | MAILGUN_API_KEY=YOUR_MAILGUN_API_KEY 20 | MAILGUN_DOMAIN=YOUR_MAILGUN_DOMAIN 21 | -------------------------------------------------------------------------------- /dashboard/src/hooks/useAuth.js: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import AuthContext from "contexts/Auth"; 3 | 4 | export default () => { 5 | const { 6 | user, 7 | organization, 8 | organizations, 9 | loading, 10 | error, 11 | login, 12 | logout, 13 | setCurrentOrganization, 14 | refetchCurrentOrg, 15 | refetchUser, 16 | } = useContext(AuthContext); 17 | return [ 18 | { organization, organizations, user }, 19 | { loading, error, login, logout, setCurrentOrganization, refetchCurrentOrg, refetchUser } 20 | ]; 21 | }; 22 | -------------------------------------------------------------------------------- /dashboard/src/styles/css/baseInputStyle.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | 3 | export default css` 4 | color: ${({ theme }) => theme.color.alt_text}; 5 | font-size: 14px; 6 | padding: 12px; 7 | 8 | &:-webkit-autofill, 9 | &:-webkit-autofill:hover, 10 | &:-webkit-autofill:focus, 11 | &:-webkit-autofill:active { 12 | -webkit-text-fill-color: ${({ theme }) => 13 | theme.color.alt_text} !important; 14 | -webkit-box-shadow: 0 0 0 30px ${({ theme }) => theme.color.surface} 15 | inset !important; 16 | } 17 | `; -------------------------------------------------------------------------------- /api/src/controllers/v1/organization/get.action.js: -------------------------------------------------------------------------------- 1 | import Organization from 'models/organization'; 2 | 3 | import StreamClient from 'utils/stream'; 4 | 5 | exports.get = async (req, res) => { 6 | try { 7 | const data = req.params; 8 | 9 | const { key } = await StreamClient(); 10 | 11 | const organization = await Organization.findById( 12 | data.organization 13 | ).lean({ autopopulate: true }); 14 | 15 | res.status(200).json({ stream: { key }, ...organization }); 16 | } catch (error) { 17 | console.error(error); 18 | res.status(500).json({ error: error.message }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /api/src/controllers/v1/plugin/list.action.js: -------------------------------------------------------------------------------- 1 | import Plugin from 'models/plugin'; 2 | 3 | exports.list = async (req, res) => { 4 | try { 5 | const data = req.query; 6 | const { serialized } = req; 7 | 8 | // if (serialized.role !== 'admin') { 9 | // return res.status(403).json({ 10 | // status: 'Invalid permissions to view or modify this resource.', 11 | // }); 12 | // } 13 | 14 | const plugins = await Plugin.apiQuery(data); 15 | res.status(200).json(plugins); 16 | } catch (error) { 17 | console.log(error); 18 | res.status(500).json({ error: error.message }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /api/src/controllers/v1/chat/list.action.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | import Chat from 'models/chat'; 4 | 5 | exports.list = async (req, res) => { 6 | try { 7 | const { serialized, query: data } = req; 8 | 9 | // if (serialized.role !== 'admin') { 10 | // return res.status(403).json({ 11 | // status: 'Invalid permissions to view or modify this resource.', 12 | // }); 13 | // } 14 | 15 | const chats = await Chat.apiQuery(data); 16 | res.status(200).json(chats); 17 | } catch (error) { 18 | console.log(error); 19 | res.status(500).json({ error: error.message }); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /api/src/controllers/v1/plugin/post.action.js: -------------------------------------------------------------------------------- 1 | import Plugin from 'models/plugin'; 2 | 3 | exports.post = async (req, res) => { 4 | try { 5 | const data = { ...req.body, ...req.params }; 6 | const { serialized } = req; 7 | 8 | if (serialized.role !== 'admin') { 9 | return res.status(403).json({ 10 | status: 'Invalid permissions to view or modify this resource.', 11 | }); 12 | } 13 | 14 | const plugin = await Plugin.create(data); 15 | res.status(200).json(plugin); 16 | } catch (error) { 17 | console.error(error); 18 | res.status(500).json({ error: error.message }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /widget/src/hooks/useAuth.js: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import AuthContext from "contexts/Auth"; 3 | 4 | export default () => { 5 | const { 6 | isFirstVisit, 7 | user, 8 | organization, 9 | organizations, 10 | loading, 11 | error, 12 | login, 13 | logout, 14 | setCurrentOrganization, 15 | refetchCurrentOrg, 16 | refetchUser, 17 | } = useContext(AuthContext); 18 | return [ 19 | { organization, organizations, user }, 20 | { loading, error, login, logout, setCurrentOrganization, refetchCurrentOrg, refetchUser, isFirstVisit } 21 | ]; 22 | }; 23 | -------------------------------------------------------------------------------- /api/src/controllers/v1/plugin/destroy.action.js: -------------------------------------------------------------------------------- 1 | import Plugin from 'models/plugin'; 2 | 3 | exports.destroy = async (req, res) => { 4 | try { 5 | const data = { ...req.body, ...req.params }; 6 | const { serialized } = req; 7 | 8 | if (serialized.role !== 'admin') { 9 | return res.status(403).json({ 10 | status: 'Invalid permissions to view or modify this resource.', 11 | }); 12 | } 13 | 14 | await Plugin.findByIdAndRemove(data.plugin); 15 | 16 | res.sendStatus(204); 17 | } catch (error) { 18 | console.error(error); 19 | res.status(500).json({ error: error.message }); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /dashboard/src/widgets/ChatWidget/ChatWidget.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from "react"; 2 | import { FAB } from "@comba.se/ui"; 3 | import { ChatIcon } from "@comba.se/ui/Icons"; 4 | 5 | // Components // 6 | import Widget from "./Widget"; 7 | 8 | const ChatWidget = () => { 9 | const [open, setOpen] = useState(false); 10 | const toggleChatWidget = useCallback(() => { 11 | setOpen(!open); 12 | }, [open]); 13 | return ( 14 | <> 15 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default ChatWidget; 22 | -------------------------------------------------------------------------------- /widget/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /api/src/controllers/v1/invite/destroy.action.js: -------------------------------------------------------------------------------- 1 | import Invite from 'models/invite'; 2 | 3 | exports.destroy = async (req, res) => { 4 | try { 5 | const data = { ...req.body, ...req.params }; 6 | const { serialized } = req; 7 | 8 | if (serialized.role !== 'admin') { 9 | return res.status(403).json({ 10 | status: 'Invalid permissions to view or modify this resource.' 11 | }); 12 | } 13 | 14 | const invite = await Invite.findByIdAndRemove(data.invite); 15 | 16 | res.sendStatus(204); 17 | } catch (error) { 18 | console.error(error); 19 | res.status(500).json({ error: error.message }); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /api/src/controllers/v1/invite/list.action.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | import Invite from 'models/invite'; 4 | 5 | exports.list = async (req, res) => { 6 | try { 7 | const data = req.query; 8 | const { serialized } = req; 9 | 10 | if (serialized.role !== 'admin') { 11 | return res.status(403).json({ 12 | status: 'Invalid permissions to view or modify this resource.', 13 | }); 14 | } 15 | 16 | const invites = await Invite.apiQuery(data); 17 | 18 | res.status(200).json(invites); 19 | } catch (error) { 20 | console.error(error); 21 | res.status(500).json({ error: error.message }); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /api/src/controllers/v1/faq/destroy.action.js: -------------------------------------------------------------------------------- 1 | import Faq from 'models/faq'; 2 | 3 | exports.destroy = async (req, res) => { 4 | try { 5 | const data = { ...req.body, ...req.params }; 6 | const { serialized } = req; 7 | 8 | if (serialized.role !== 'admin') { 9 | return res.status(403).json({ 10 | status: 'Invalid permissions to view or modify this resource.' 11 | }); 12 | } 13 | 14 | const faq = await Faq.updateOne({ _id: data.faq }, { $set: data }); 15 | 16 | res.status(200).json(faq); 17 | } catch (error) { 18 | console.error(error); 19 | res.status(500).json({ error: error.message }); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /api/src/controllers/v1/organization/destroy.action.js: -------------------------------------------------------------------------------- 1 | import Organization from 'models/organization'; 2 | 3 | exports.destroy = async (req, res) => { 4 | try { 5 | const data = { ...req.body, ...req.params }; 6 | const { serialized } = req; 7 | 8 | if (serialized.role !== 'admin') { 9 | return res.status(403).json({ 10 | status: 'Invalid permissions to view or modify this resource.' 11 | }); 12 | } 13 | 14 | await Organization.findByIdAndRemove(data.organization); 15 | 16 | res.sendStatus(204); 17 | } catch (error) { 18 | console.error(error); 19 | res.status(500).json({ error: error.message }); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /api/src/controllers/v1/plugin/get.action.js: -------------------------------------------------------------------------------- 1 | import Plugin from 'models/plugin'; 2 | 3 | exports.get = async (req, res) => { 4 | try { 5 | const data = req.params; 6 | const { serialized } = req; 7 | 8 | // if (serialized.role !== 'admin') { 9 | // return res.status(403).json({ 10 | // status: 'Invalid permissions to view or modify this resource.', 11 | // }); 12 | // } 13 | 14 | const plugin = await Plugin.findById(data.plugin).lean({ 15 | autopopulate: true, 16 | }); 17 | res.status(200).json(plugin); 18 | } catch (error) { 19 | console.error(error); 20 | res.status(500).json({ error: error.message }); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /dashboard/src/screens/Auth/forms/InvitationForm/validationSchema.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | 3 | const requiredError = 'This is required.'; 4 | const emailError = 'Invalid Email Address.'; 5 | 6 | 7 | export default Yup.object().shape({ 8 | invitations: Yup.array().of( 9 | Yup.object().shape({ 10 | email: Yup.string() 11 | .email(emailError) 12 | .required(requiredError), 13 | name: Yup.object().shape({ 14 | first: Yup.string().required(requiredError), 15 | last: Yup.string().required(requiredError), 16 | }) 17 | }) 18 | ), 19 | }) 20 | -------------------------------------------------------------------------------- /api/src/controllers/v1/plugin/update.action.js: -------------------------------------------------------------------------------- 1 | import Plugin from 'models/plugin'; 2 | 3 | exports.update = async (req, res) => { 4 | try { 5 | const data = req.body; 6 | const params = req.params; 7 | const { serialized } = req; 8 | 9 | if (serialized.role !== 'admin') { 10 | return res.status(403).json({ 11 | status: 'Invalid permissions to view or modify this resource.', 12 | }); 13 | } 14 | 15 | const plugin = await Plugin.updateOne( 16 | { _id: params.plugin }, 17 | { $set: data } 18 | ); 19 | res.status(200).json(plugin); 20 | } catch (error) { 21 | console.error(error); 22 | res.status(500).json({ error: error.message }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /dashboard/src/shared/Logo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | import { withTheme } from 'styled-components'; 3 | import { Icon } from '@comba.se/ui'; 4 | 5 | const Logo = ({ theme, ...props }) => ( 6 | 7 | 8 | 17 | 18 | 19 | ); 20 | 21 | export default withTheme(Logo); 22 | -------------------------------------------------------------------------------- /api/src/utils/db/index.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import mongoose from 'mongoose'; 3 | 4 | const db = mongoose.connection; 5 | 6 | mongoose.promise = global.Promise; 7 | mongoose.connect(process.env.MONGODB_URI, { 8 | useNewUrlParser: true, 9 | useUnifiedTopology: true, 10 | }); 11 | mongoose.set('useCreateIndex', true); 12 | mongoose.set('useFindAndModify', false); 13 | 14 | db.on('error', err => { 15 | console.error(err); 16 | }); 17 | 18 | db.on('disconnected', () => { 19 | console.info('Database disconnected!'); 20 | }); 21 | 22 | process.on('SIGINT', () => { 23 | mongoose.connection.close(() => { 24 | process.exit(0); 25 | }); 26 | }); 27 | 28 | export default db; 29 | -------------------------------------------------------------------------------- /dashboard/src/shared/Modal/Undersheet.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | import styled from 'styled-components'; 3 | import Animated from 'animated/lib/targets/react-dom'; 4 | 5 | const Undersheet = styled(Animated.div)` 6 | position: fixed; 7 | top: 0; 8 | right: 0; 9 | bottom: 0; 10 | left: 0; 11 | background-color: ${({ show, theme }) => 12 | show ? theme.color.undersheet : 'transparent'}; 13 | ${''} 14 | z-index: ${({ theme }) => theme.z.modal}; 15 | visibility: ${({ open }) => (open ? 'visible' : 'hidden')}; 16 | ${({ show }) => (show ? 'backdrop-filter: blur(5px);' : null)} 17 | `; 18 | 19 | export default Undersheet; 20 | -------------------------------------------------------------------------------- /api/src/controllers/v1/agent/update.action.js: -------------------------------------------------------------------------------- 1 | import Agent from 'models/agent'; 2 | 3 | exports.update = async (req, res) => { 4 | try { 5 | const data = req.body; 6 | const params = req.params; 7 | const { serialized } = req; 8 | 9 | // if (serialized.role !== 'admin') { 10 | // return res.status(403).json({ 11 | // status: 'Invalid permissions to view or modify this resource.' 12 | // }); 13 | // } 14 | 15 | const agent = await Agent.findOneAndUpdate({ _id: params.agent }, { $set: data }, { new: true }); 16 | 17 | res.status(200).json(agent); 18 | } catch (error) { 19 | console.error(error); 20 | res.status(500).json({ error: error.message }); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /dashboard/src/hooks/useAgent.js: -------------------------------------------------------------------------------- 1 | import { useContext, useMemo } from 'react'; 2 | 3 | // Hooks // 4 | import usePrevious from 'hooks/usePrevious'; 5 | 6 | // Contexts // 7 | import AgentsContext from 'contexts/Agents'; 8 | 9 | export default agentId => { 10 | const [agents, tabs, { refetchAgents }] = useContext(AgentsContext); // eslint-disable-line no-unused-vars 11 | const prevAgentId = usePrevious(agentId); 12 | const agent = useMemo(() => { 13 | if (!agentId && !prevAgentId) { 14 | return null; 15 | } 16 | return agents.find(({ _id }) => (agentId || prevAgentId) === _id); 17 | }, [agents, agentId, prevAgentId]); 18 | return [agent, { refetchAgents }]; 19 | }; 20 | -------------------------------------------------------------------------------- /api/src/controllers/v1/user/destroy.action.js: -------------------------------------------------------------------------------- 1 | import User from 'models/user'; 2 | import Chat from 'models/chat'; 3 | 4 | exports.destroy = async (req, res) => { 5 | try { 6 | const data = { ...req.body, ...req.params }; 7 | const { serialized } = req; 8 | 9 | if (serialized.role !== 'admin') { 10 | return res.status(403).json({ 11 | status: 'Invalid permissions to view or modify this resource.' 12 | }); 13 | } 14 | 15 | await User.findByIdAndRemove(data.user); 16 | await Chat.remove({ 17 | 'refs.organization': data.organization 18 | }); 19 | 20 | res.sendStatus(204); 21 | } catch (error) { 22 | console.error(error); 23 | res.status(500).json({ error: error.message }); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /api/src/controllers/v1/organization/update.action.js: -------------------------------------------------------------------------------- 1 | import Organization from 'models/organization'; 2 | 3 | exports.update = async (req, res) => { 4 | try { 5 | const data = req.body; 6 | const params = req.params; 7 | const { serialized } = req; 8 | 9 | // if (serialized.role !== 'admin') { 10 | // return res.status(403).json({ 11 | // status: 'Invalid permissions to view or modify this resource.' 12 | // }); 13 | // } 14 | 15 | const organization = await Organization.findOneAndUpdate({ _id: params.organization }, { $set: data }, { new: true }); 16 | 17 | res.status(200).json(organization); 18 | } catch (error) { 19 | console.error(error); 20 | res.status(500).json({ error: error.message }); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Switch } from "react-router-dom"; 3 | 4 | // Data // 5 | import routes from "./routes"; 6 | 7 | // Hooks // 8 | import useNotifications from 'hooks/useNotifications'; 9 | 10 | // HOCs // 11 | import withShell from "hocs/withShell"; 12 | 13 | const renderRoutes = match => 14 | routes.map(({ slug, component, isExact }, key) => ( 15 | 16 | )); 17 | 18 | const Dashboard = ({ match }) => { 19 | useNotifications(); // Triggers Notification Sounds & Snackbars. 20 | return {renderRoutes(match)}; 21 | }; 22 | 23 | export default withShell(Dashboard, routes); 24 | -------------------------------------------------------------------------------- /widget/src/Root.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import compose from 'lodash.flowright'; 3 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 4 | 5 | // HOCs // 6 | import withAuth from 'hocs/withAuth'; 7 | import withChannels from '@comba.se/chat/hocs/withChannels'; 8 | 9 | // Screens // 10 | import Home from 'screens/Home'; 11 | import Thread from 'screens/Thread'; 12 | 13 | const Root = () => { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | 24 | export default compose(withAuth, withChannels)(Root); -------------------------------------------------------------------------------- /dashboard/src/utils/upload.js: -------------------------------------------------------------------------------- 1 | const parse = response => response.json(); 2 | export default async (file, token) => { 3 | console.log(file); 4 | const body = new FormData(); 5 | body.append("file", file); 6 | 7 | try { 8 | const response = await fetch( 9 | `${process.env.REACT_APP_API_ENDPOINT}v1/uploads`, 10 | { 11 | method: "POST", 12 | headers: { 13 | Authorization: `Bearer ${token || 14 | process.env.REACT_APP_API_KEY}`, 15 | }, 16 | body, 17 | } 18 | ).then(parse); 19 | return response; 20 | } catch (error) { 21 | throw new Error(error); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /widget/src/utils/request.js: -------------------------------------------------------------------------------- 1 | const parseJSON = response => response.json(); 2 | export default async (path, method = 'get', options = {}, token) => { 3 | try { 4 | const response = await fetch( 5 | `${process.env.REACT_APP_API_ENDPOINT}${path}`, 6 | { 7 | method: method.toUpperCase(), 8 | headers: { 9 | Authorization: `Bearer ${token || 10 | process.env.REACT_APP_API_KEY}`, 11 | 'Content-Type': 'application/json', 12 | }, 13 | ...options, 14 | } 15 | ).then(parseJSON); 16 | return response; 17 | } catch (error) { 18 | throw new Error(error); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /api/src/controllers/v1/user/post.action.js: -------------------------------------------------------------------------------- 1 | import { StreamChat } from 'stream-chat'; 2 | 3 | import User from 'models/user'; 4 | import StreamClient from 'utils/stream'; 5 | 6 | exports.post = async (req, res) => { 7 | try { 8 | const data = req.body; 9 | 10 | const user = await User.findOneOrCreate({ email: data.email }, data); 11 | 12 | const { key, secret } = await StreamClient(); 13 | const client = new StreamChat(key, secret); 14 | 15 | await client.updateUser({ 16 | id: user._id.toString(), 17 | name: `${user.name.first} ${user.name.last}`, 18 | role: 'user' 19 | }); 20 | 21 | res.status(200).json(user); 22 | } catch (error) { 23 | console.error(error); 24 | res.status(500).json({ error: error.message }); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /dashboard/src/utils/request.js: -------------------------------------------------------------------------------- 1 | const parseJSON = response => response.json(); 2 | export default async (path, method = 'get', options = {}, token) => { 3 | try { 4 | const response = await fetch( 5 | `${process.env.REACT_APP_API_ENDPOINT}${path}`, 6 | { 7 | method: method.toUpperCase(), 8 | headers: { 9 | Authorization: `Bearer ${token || 10 | process.env.REACT_APP_API_KEY}`, 11 | 'Content-Type': 'application/json', 12 | }, 13 | ...options, 14 | } 15 | ).then(parseJSON); 16 | return response; 17 | } catch (error) { 18 | throw new Error(error); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /dashboard/src/widgets/Combase/ChatDataWidget.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import moment from 'moment'; 5 | import { ListItem } from '@comba.se/ui'; 6 | 7 | // Components // 8 | const Root = styled.div` 9 | 10 | ` 11 | 12 | const ChatDataWidget = ({ createdAt, partnerId }) => { 13 | return ( 14 | 15 | 16 | 17 | 18 | ) 19 | }; 20 | 21 | ChatDataWidget.propTypes = { 22 | createdAt: PropTypes.string, 23 | partnerId: PropTypes.string, 24 | } 25 | 26 | export default ChatDataWidget; -------------------------------------------------------------------------------- /widget/src/components/CardHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { Text } from "@comba.se/ui"; 5 | 6 | // Components // 7 | const Root = styled.div` 8 | flex-direction: row; 9 | align-items: center; 10 | padding: 20px 24px 12px 24px; 11 | 12 | & ${Text} { 13 | margin-left: 8px; 14 | } 15 | `; 16 | 17 | const CardHeader = ({ icon: Icon, title }) => { 18 | return ( 19 | 20 | 21 | {title} 22 | 23 | ); 24 | }; 25 | 26 | CardHeader.propTypes = { 27 | icon: PropTypes.func, 28 | text: PropTypes.string, 29 | } 30 | 31 | export default CardHeader; -------------------------------------------------------------------------------- /dashboard/src/components/OrgProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | import { LoadingState } from "@comba.se/ui"; 4 | import useAuth from 'hooks/useAuth'; 5 | 6 | export default ({ component: Component, ...rest }) => { 7 | const [{ organization }, { loading }] = useAuth(); 8 | return ( 9 | { 12 | if (loading) { 13 | return ; 14 | } else if (organization) { 15 | return ; 16 | } 17 | return ; 18 | }} 19 | /> 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /api/src/routes/plugin.js: -------------------------------------------------------------------------------- 1 | import { get, list, post, update, destroy } from '../controllers/v1/plugin'; 2 | import { blazeVerifyExecVerify, clearbitExecEnrich } from '../controllers/v1/plugin/integration'; 3 | 4 | import { wrapAsync } from '../utils/controllers'; 5 | 6 | module.exports = (api) => { 7 | api.route('/v1/plugins').get(wrapAsync(list)); 8 | api.route('/v1/plugins/:plugin').get(wrapAsync(get)); 9 | api.route('/v1/plugins/:plugin').put(wrapAsync(update)); 10 | api.route('/v1/plugins').post(wrapAsync(post)); 11 | api.route('/v1/plugins/:plugin').delete(wrapAsync(destroy)); 12 | 13 | // integrations 14 | api.route('/v1/plugins/blaze_verify/verify').post(wrapAsync(blazeVerifyExecVerify)); 15 | api.route('/v1/plugins/clearbit/enrich').post(wrapAsync(clearbitExecEnrich)); 16 | }; 17 | -------------------------------------------------------------------------------- /dashboard/src/shared/SectionTitle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Text } from '@comba.se/ui'; 4 | 5 | // Components // 6 | const Root = styled.div` 7 | flex-direction: row; 8 | padding-bottom: 8px; 9 | padding-left: 8px; 10 | padding-right: 8px; 11 | border-bottom: 1px solid ${({ theme }) => theme.color.border}; 12 | 13 | @media (min-width: ${({ theme }) => theme.breakpoints.sm}px){ 14 | padding-left: 16px; 15 | padding-right: 16px; 16 | } 17 | `; 18 | 19 | const SectionTitle = ({ className, title }) => ( 20 | 21 | 22 | {title} 23 | 24 | 25 | ); 26 | 27 | export default SectionTitle; 28 | -------------------------------------------------------------------------------- /dashboard/src/screens/Auth/forms/SignUpForm/validationSchema.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | 3 | const requiredError = 'This is required.'; 4 | const emailError = 'Invalid Email Address.'; 5 | 6 | export default Yup.object().shape({ 7 | name: Yup.object().shape({ 8 | first: Yup.string().required(requiredError), 9 | last: Yup.string().required(requiredError), 10 | }), 11 | email: Yup.string() 12 | .email(emailError) 13 | .required(requiredError), 14 | password: Yup.string().required(requiredError), 15 | confirm: Yup.string().when('password', { 16 | is: val => val && val.length > 0, 17 | then: Yup.string() 18 | .oneOf([Yup.ref('password')], 'Both passwords need to be the same') 19 | .required(requiredError), 20 | }), 21 | }); 22 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Settings/Settings.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route } from "react-router-dom"; 3 | 4 | // Views // 5 | import SettingsDetail from "./views/SettingsDetail"; 6 | import SettingsList from "./views/SettingsList"; 7 | 8 | // Components // 9 | import ListDetailView from "components/ListDetailView"; 10 | import ScreenRoot from "shared/ScreenRoot"; 11 | 12 | const renderSettingsDetail = props => ; 13 | const renderSettingsList = props => ; 14 | 15 | export default props => ( 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /dashboard/src/hooks/useConfig.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react'; 2 | import request from 'utils/request'; 3 | 4 | export default () => { 5 | const [config, setConfig] = useState({}); 6 | const [loading, setLoading] = useState(false); 7 | const [error, setError] = useState(null); 8 | 9 | const getConfig = useCallback(async () => { 10 | try { 11 | setLoading(true); 12 | const data = await request('v1/configs'); 13 | setConfig(data); 14 | setLoading(false); 15 | } catch (error) { 16 | setLoading(false); 17 | setError(error); 18 | } 19 | }, []); 20 | 21 | useEffect(() => { 22 | getConfig(); 23 | }, [getConfig]); 24 | 25 | return [config, { loading, error }]; 26 | }; 27 | -------------------------------------------------------------------------------- /widget/src/components/Home/KnowledgeBaseWidget/KnowledgeBaseWidget.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Card, Input } from "@comba.se/ui"; 4 | import { KnowledgeBaseIcon } from "@comba.se/ui/Icons"; 5 | 6 | // Components // 7 | import CardHeader from 'components/CardHeader'; 8 | 9 | const Root = styled(Card)` 10 | width: 100%; 11 | `; 12 | 13 | const List = styled.div` 14 | padding: 16px 24px; 15 | ` 16 | 17 | const KnowledgeBaseWidget = ({ className }) => { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default KnowledgeBaseWidget; -------------------------------------------------------------------------------- /dashboard/src/components/AvailabilityField/Availability.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | // Components // 5 | import AvailabilityDay from './AvailabilityDay'; 6 | 7 | const Root = styled.div` 8 | flex-direction: row; 9 | align-items: center; 10 | justify-content: space-between; 11 | 12 | @media (min-width: ${ ({ theme }) => theme.breakpoints.sm}px) { 13 | padding: 0px 16px; 14 | } 15 | `; 16 | 17 | const renderDays = (days, onChange) => Object.entries(days).map(([day, data], key) => ()); 18 | 19 | const Availability = ({ value, onChange }) => { 20 | return ( 21 | 22 | {renderDays(value, onChange)} 23 | 24 | ); 25 | }; 26 | 27 | export default Availability; -------------------------------------------------------------------------------- /dashboard/src/components/AuthedRoute.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Redirect } from "react-router-dom"; 3 | import { LoadingState } from "@comba.se/ui"; 4 | import useAuth from "hooks/useAuth"; 5 | 6 | export default ({ component: Component, ...rest }) => { 7 | const [{ user }, { loading }] = useAuth(); 8 | return ( 9 | { 12 | if (loading) { 13 | return ; 14 | } else if (!!user) { 15 | return ; 16 | } 17 | return ( 18 | 25 | ); 26 | }} 27 | /> 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /dashboard/src/hooks/useLayout.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react'; 2 | import ResizeObserver from 'utils/ResizeObserver'; 3 | 4 | export default () => { 5 | const [layout, setLayout] = useState(null); 6 | const [el, setEl] = useState(null); 7 | 8 | const setRef = useCallback(el => { 9 | console.log('setRef'); 10 | setEl(el); 11 | }, []); 12 | 13 | const handleResize = useCallback(entries => { 14 | console.log(entries); 15 | const [entry] = entries; 16 | setLayout(entry.contentRect); 17 | }, []); 18 | 19 | useEffect(() => { 20 | const observer = new ResizeObserver(handleResize); 21 | if (el) { 22 | observer.observe(el); 23 | return observer.disconnect(); 24 | } 25 | }, [el]); 26 | 27 | return [layout, setRef]; 28 | }; 29 | -------------------------------------------------------------------------------- /api/src/models/tag.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import query from 'mongoose-string-query'; 3 | import timestamps from 'mongoose-timestamp'; 4 | import autopopulate from 'mongoose-autopopulate'; 5 | 6 | export const TagSchema = new Schema( 7 | { 8 | name: { 9 | type: String, 10 | trim: true, 11 | required: true, 12 | }, 13 | colors: { 14 | primary: { 15 | type: String, 16 | trim: true, 17 | default: '#4D7CFE', 18 | }, 19 | secondary: { 20 | type: String, 21 | trim: true, 22 | default: '#ffffff', 23 | }, 24 | }, 25 | }, 26 | { 27 | collection: 'tags', 28 | } 29 | ); 30 | 31 | TagSchema.plugin(timestamps); 32 | TagSchema.plugin(query); 33 | TagSchema.plugin(autopopulate); 34 | 35 | TagSchema.index({ createdAt: 1, updatedAt: 1 }); 36 | 37 | module.exports = exports = mongoose.model('Tag', TagSchema); 38 | -------------------------------------------------------------------------------- /dashboard/src/components/UnauthedRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | import { LoadingState } from "@comba.se/ui"; 4 | import useAuth from 'hooks/useAuth'; 5 | 6 | export default ({ component: Component, ...rest }) => { 7 | const [{ organization, user }, { loading }] = useAuth(); 8 | return ( 9 | { 12 | if (loading) { 13 | return ; 14 | } else if (!organization) { 15 | return ; 16 | } else if (!!user) { 17 | return ; 18 | } 19 | return ; 20 | }} 21 | /> 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /api/src/utils/stream/index.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { StreamChat } from 'stream-chat'; 3 | 4 | export default async () => { 5 | try { 6 | let apiKey; 7 | let apiSecret; 8 | 9 | // if this env is found, it's assumed that the api is running on heroku 10 | if (process.env.STREAM_URL) { 11 | // extract the key and secret from the environment variable 12 | [apiKey, apiSecret] = process.env.STREAM_URL.substr(8) 13 | .split('@')[0] 14 | .split(':'); 15 | } else { 16 | // api key and secret were provided from a .env file 17 | apiKey = process.env.STREAM_API_KEY; 18 | apiSecret = process.env.STREAM_API_SECRET; 19 | } 20 | 21 | const client = { 22 | key: apiKey, 23 | secret: apiSecret, 24 | client: new StreamChat(apiKey, apiSecret), 25 | }; 26 | 27 | return client; 28 | } catch (error) { 29 | return new Error(Error); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /dashboard/src/screens/Auth/views/Invite.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Container, Text } from '@comba.se/ui'; 4 | 5 | // Forms // 6 | import InvitationForm from '../forms/InvitationForm'; 7 | 8 | // Components // 9 | const Root = styled(Container)` 10 | flex: 1; 11 | justify-content: center; 12 | `; 13 | 14 | const Header = styled.div` 15 | padding: 32px 0px; 16 | justify-content: center; 17 | align-items: center; 18 | text-align: center; 19 | `; 20 | 21 | const Invite = () => { 22 | return ( 23 | 24 |
25 | 26 | Invite your Team 27 | 28 |
29 | 30 |
31 | ); 32 | }; 33 | 34 | export default Invite; 35 | -------------------------------------------------------------------------------- /dashboard/src/shared/InfoWidget.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { Text } from '@comba.se/ui'; 5 | 6 | // Components // 7 | const Root = styled.div` 8 | flex: 1; 9 | justify-content: flex-start; 10 | `; 11 | 12 | const Label = styled(Text)` 13 | margin-top: 8px; 14 | text-transform: uppercase; 15 | letter-spacing: 1px; 16 | `; 17 | 18 | const InfoWidget = ({ label, widget: Widget }) => { 19 | return ( 20 | 21 | 22 | 25 | 26 | ); 27 | }; 28 | 29 | InfoWidget.propTypes = { 30 | widget: PropTypes.any.isRequired, 31 | label: PropTypes.string.isRequired, 32 | }; 33 | 34 | export default InfoWidget; 35 | -------------------------------------------------------------------------------- /api/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["babel"], 3 | "extends": ["eslint:recommended"], 4 | "rules": { 5 | "no-console": 0, 6 | "no-mixed-spaces-and-tabs": 1, 7 | "comma-dangle": 0, 8 | "no-unused-vars": 1, 9 | "eqeqeq": [2, "smart"], 10 | "no-useless-concat": 2, 11 | "default-case": 2, 12 | "no-self-compare": 2, 13 | "prefer-const": 2, 14 | "no-underscore-dangle": 0, 15 | "object-shorthand": 1, 16 | "babel/no-invalid-this": 2, 17 | "array-callback-return": 2, 18 | "valid-typeof": 2, 19 | "arrow-body-style": 2, 20 | "require-await": 2, 21 | "react/prop-types": 0, 22 | "no-var": 2, 23 | "linebreak-style": [2, "unix"], 24 | "semi": [1, "always"] 25 | }, 26 | "env": { 27 | "node": true 28 | }, 29 | "parser": "babel-eslint", 30 | "parserOptions": { 31 | "sourceType": "module", 32 | "ecmaVersion": 2018, 33 | "ecmaFeatures": { 34 | "modules": true 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Inbox/views/Threads/Threads.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { ThreadList } from "@comba.se/chat"; 4 | import useChats from 'hooks/useChats'; 5 | 6 | // Components // 7 | import MenuButton from 'shared/MenuButton'; 8 | 9 | const Root = styled.div` 10 | flex: 1; 11 | order: -1; 12 | height: 100%; 13 | background-color: ${({ theme }) => theme.color.background}; 14 | 15 | @media (min-width: ${({ theme }) => theme.breakpoints.sm}px) { 16 | width: 375px; 17 | position: fixed; 18 | top: 0; 19 | left: 96px; 20 | right: 0; 21 | bottom: 0; 22 | } 23 | `; 24 | 25 | const Threads = () => { 26 | const [chats, { error, loading }] = useChats(); 27 | return 28 | 29 | 30 | } 31 | 32 | export default Threads; -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/routes.js: -------------------------------------------------------------------------------- 1 | import { AgentsIcon, InboxIcon, PluginsIcon, SettingsIcon } from "@comba.se/ui/Icons"; 2 | 3 | import Agents from "./views/Agents"; 4 | import Inbox from "./views/Inbox"; 5 | import Settings from "./views/Settings"; 6 | import Plugins from './views/Plugins'; 7 | 8 | export default [ 9 | { 10 | label: "Inbox", 11 | slug: "inbox", 12 | component: Inbox, 13 | isExact: false, 14 | icon: InboxIcon 15 | }, 16 | { 17 | label: "Agents", 18 | slug: "agents", 19 | component: Agents, 20 | isExact: false, 21 | icon: AgentsIcon 22 | }, 23 | { 24 | label: "Plugins", 25 | slug: "plugins", 26 | component: Plugins, 27 | isExact: false, 28 | icon: PluginsIcon 29 | }, 30 | { 31 | label: "Settings", 32 | slug: "settings", 33 | component: Settings, 34 | isExact: false, 35 | icon: SettingsIcon 36 | }, 37 | ]; 38 | -------------------------------------------------------------------------------- /dashboard/src/components/Shell/Sidenav/SidenavItem.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Link, Route } from 'react-router-dom'; 4 | 5 | // Components // 6 | const Root = styled.div` 7 | justify-content: center; 8 | align-items: center; 9 | padding: 24px 0px; 10 | `; 11 | 12 | const NavLink = styled(Link)` 13 | pointer-events: ${({ active }) => (active ? 'none' : 'auto')}; 14 | `; 15 | 16 | export default memo(({ icon: Icon, isExact, path }) => { 17 | return ( 18 | ( 21 | 22 | 23 | 24 | 25 | 26 | )} 27 | /> 28 | ); 29 | }); 30 | -------------------------------------------------------------------------------- /dashboard/src/styles/css/listItemInteractions.js: -------------------------------------------------------------------------------- 1 | import { css } from "styled-components"; 2 | 3 | /* 4 | * Used for hover & active states of a list item component 5 | * Falls back to the theme's primary color 6 | * Optionally pass an 'activeColor' prop with the key name of a color from your theme to customize 7 | */ 8 | 9 | export default css` 10 | background-color: ${({ active, activeColor = "text", theme }) => 11 | theme.colorUtils.fade(theme.color[activeColor], active ? 0.04 : 0)}; 12 | transition: 0.24s background-color 13 | ${({ theme }) => theme.easing.css(theme.easing.standard)}; 14 | 15 | &:hover { 16 | background-color: ${({ activeColor = "text", theme }) => 17 | theme.colorUtils.fade(theme.color[activeColor], 0.04)}; 18 | } 19 | 20 | &:active { 21 | background-color: ${({ activeColor = "text", theme }) => 22 | theme.colorUtils.fade(theme.color[activeColor], 0.08)}; 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /dashboard/src/hooks/useMedia.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from 'react'; // eslint-disable-line no-unused-vars 2 | import { ThemeContext } from 'styled-components'; 3 | 4 | export default (breakpoint, minmax = 'min') => { 5 | const [matches, setMatch] = useState(false); 6 | 7 | if (!breakpoint) { 8 | throw new Error('Must provide a breakpoint'); 9 | } 10 | 11 | const theme = useContext(ThemeContext); 12 | 13 | const query = window.matchMedia( 14 | `(${minmax}-width: ${theme.breakpoints[breakpoint] - 15 | (minmax === 'max' ? 1 : 0)}px)` 16 | ); 17 | 18 | useEffect(() => setMatch(!query.matches), [query.matches]); 19 | 20 | const handleChange = ({ matches: isMatched }) => setMatch(!isMatched); 21 | 22 | useEffect(() => { 23 | query.onchange = handleChange; 24 | return query.removeListener(handleChange); 25 | }, [query]); 26 | 27 | return matches; 28 | }; 29 | -------------------------------------------------------------------------------- /dashboard/src/shared/SearchInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { SearchIcon } from "@comba.se/ui/Icons"; 4 | 5 | // Components // 6 | const Root = styled.div` 7 | border-radius: ${({ theme }) => theme.borderRadius * 1.5}px; 8 | background-color: ${({ theme }) => theme.color.border}; 9 | height: 40px; 10 | flex-direction: row; 11 | overflow: hidden; 12 | `; 13 | 14 | const IconWrapper = styled.div` 15 | padding: 0px 12px; 16 | justify-content: center; 17 | align-items: center; 18 | `; 19 | 20 | const Input = styled.input` 21 | flex: 1; 22 | padding-right: 12px; 23 | `; 24 | 25 | const SearchInput = () => { 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default SearchInput; -------------------------------------------------------------------------------- /widget/src/contexts/ScrollAnimation/Provider.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useMemo } from 'react'; 2 | import { useSpring } from 'react-spring'; 3 | import { window } from 'browser-monads'; 4 | import Context from './index'; 5 | 6 | export default ({ children, target = window }) => { 7 | const [anim, set] = useSpring(() => ({ value: 0, config: { mass: 1, tension: 500, friction: 30 } })); 8 | const onScroll = useCallback(({ target: eventTarget }) => set({ value: eventTarget.scrollTop }), [set]) 9 | 10 | useEffect(() => { 11 | if (!!target) { 12 | target.addEventListener('scroll', onScroll); 13 | return () => target.removeEventListener('scroll', onScroll); 14 | } 15 | }, [onScroll, target]); 16 | 17 | const value = useMemo(() => ({ 18 | anim, 19 | }), [anim]); 20 | 21 | return ( 22 | 23 | {children} 24 | 25 | ); 26 | } -------------------------------------------------------------------------------- /dashboard/src/shared/MenuButton.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { IconButton } from '@comba.se/ui'; 5 | import { MenuIcon } from "@comba.se/ui/Icons"; 6 | 7 | // Contexts // 8 | import ShellContext from 'contexts/Shell'; 9 | 10 | // Components // 11 | const Root = styled(IconButton)` 12 | @media (min-width: ${({ theme }) => theme.breakpoints.sm}px) { 13 | display: none; 14 | } 15 | `; 16 | 17 | const MenuButton = props => { 18 | const { drawer } = useContext(ShellContext); 19 | return ( 20 | 27 | ); 28 | }; 29 | 30 | MenuButton.propTypes = { 31 | color: PropTypes.string, 32 | }; 33 | 34 | MenuButton.defaultProps = { 35 | color: 'text', 36 | }; 37 | 38 | export default MenuButton; 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /api/src/utils/auth/whitelist.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: 'agents', 4 | method: 'GET', 5 | auth: true, 6 | }, 7 | { 8 | path: 'agents', 9 | method: 'POST', 10 | auth: true, 11 | }, 12 | { 13 | path: 'auth', 14 | method: 'POST', 15 | auth: true, 16 | }, 17 | { 18 | path: 'chats', 19 | method: 'POST', 20 | auth: true, 21 | }, 22 | { 23 | path: 'configs', 24 | method: 'GET', 25 | auth: true, 26 | }, 27 | { 28 | path: 'invites', 29 | method: 'GET', 30 | auth: true, 31 | }, 32 | { 33 | path: 'invites', 34 | method: 'PUT', 35 | auth: true, 36 | }, 37 | { 38 | path: 'password-reset', 39 | method: 'POST', 40 | auth: true, 41 | }, 42 | { 43 | path: 'organizations', 44 | method: 'POST', 45 | auth: true, 46 | }, 47 | { 48 | path: 'organizations', 49 | method: 'GET', 50 | auth: true, 51 | }, 52 | { 53 | path: 'users', 54 | method: 'GET', 55 | auth: true, 56 | }, 57 | { 58 | path: 'users', 59 | method: 'POST', 60 | auth: true, 61 | }, 62 | ]; 63 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Inbox/views/SideDrawer/SideDrawer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Switch, Route } from "react-router-dom"; 4 | 5 | // Views // 6 | import InfoDrawer from "./views/InfoDrawer"; 7 | 8 | // Components // 9 | const Root = styled.div` 10 | position: absolute; 11 | top: 0; 12 | right: 0; 13 | bottom: 0; 14 | width: 376px; 15 | z-index: 3; 16 | background-color: ${({ theme }) => theme.color.surface}; 17 | border-left: 1px solid ${({ theme }) => theme.color.border}; 18 | transform: translateX(${({ open }) => (open ? 0 : 100)}%); 19 | `; 20 | 21 | const SideDrawer = ({ match, open, partner }) => ( 22 | 23 | 24 | } /> 25 | "transfer"} /> 26 | 27 | 28 | ); 29 | 30 | export default SideDrawer; 31 | -------------------------------------------------------------------------------- /api/src/index.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import express from 'express'; 5 | import bodyParser from 'body-parser'; 6 | import cors from 'cors'; 7 | import helmet from 'helmet'; 8 | import compression from 'compression'; 9 | 10 | import 'utils/db'; 11 | import auth from 'utils/auth'; 12 | 13 | const api = express(); 14 | 15 | api.use(cors()); 16 | api.use(compression()); 17 | api.use(helmet()); 18 | api.use(bodyParser.urlencoded({ extended: true })); 19 | api.use(bodyParser.json()); 20 | //api.use(auth); 21 | 22 | api.listen(process.env.PORT, (err) => { 23 | if (err) { 24 | console.warn('Database connection error.', new Error(err)); 25 | process.exit(1); 26 | } 27 | 28 | // eslint-disable-next-line array-callback-return 29 | fs.readdirSync(path.join(__dirname, 'routes')).map((file) => { 30 | require('./routes/' + file)(api); 31 | }); 32 | 33 | console.info(`API is now running on port ${process.env.PORT} in ${process.env.NODE_ENV} mode. 👨‍🚀`); 34 | }); 35 | 36 | module.exports = api; 37 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/forms/OrganizationSettings/OrganizationProfileForm/validationSchema.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | 3 | const requiredError = 'This is required.'; 4 | const emailError = 'Invalid email address.'; 5 | 6 | export default Yup.object().shape({ 7 | name: Yup.string().required(requiredError), 8 | meta: Yup.object().shape({ 9 | tagline: Yup.string().required(requiredError), 10 | branding: Yup.object().shape({ 11 | logo: Yup.string(), 12 | color: Yup.object().shape({ 13 | primary: Yup.string(), 14 | }) 15 | }), 16 | }), 17 | phone: Yup.object().shape({ 18 | display: Yup.bool(), 19 | number: Yup.string().required(requiredError) 20 | }), 21 | website: Yup.object().shape({ 22 | display: Yup.bool(), 23 | url: Yup.string().required(requiredError) 24 | }), 25 | email: Yup.object().shape({ 26 | display: Yup.bool(), 27 | address: Yup.string().email(emailError).required(requiredError) 28 | }), 29 | }); 30 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Settings/views/SettingsDetail/views/UserSettings/UserSettings.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Container, ListHeader } from '@comba.se/ui'; 4 | import { UserSettingsIcon } from "@comba.se/ui/Icons"; 5 | 6 | // Hooks // 7 | import useMedia from 'hooks/useMedia'; 8 | 9 | // Forms // 10 | import { UserAvailabilityForm, UserProfileForm } from 'screens/Dashboard/forms/UserSettings'; 11 | 12 | // Components // 13 | import MenuButton from 'shared/MenuButton'; 14 | 15 | const Root = styled.div` 16 | flex: 1; 17 | `; 18 | 19 | const UserSettings = () => { 20 | const isMobile = useMedia('sm'); 21 | return ( 22 | 23 | 24 | {!isMobile ? : null} 25 | 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default UserSettings; 33 | -------------------------------------------------------------------------------- /api/docs/API.md: -------------------------------------------------------------------------------- 1 | # Combase API 2 | API for https://comba.se 3 | 4 | ## Version: 1.0.0 5 | 6 | 7 | ### DELETE /v1/agents/:id 8 | 9 | #### DELETE 10 | ##### Description: 11 | 12 | Deletes an agent 13 | 14 | ##### Parameters 15 | 16 | | Name | Located in | Description | Required | Schema | 17 | | ---- | ---------- | ----------- | -------- | ---- | 18 | | ID | controllers/v1/agent/destroy.action.js | UUID of agent to delete | Yes | string (uuid) | 19 | 20 | ##### Responses 21 | 22 | | Code | Description | 23 | | ---- | ----------- | 24 | | 204 | No response | 25 | 26 | ### GET /v1/agents:id 27 | 28 | #### GET 29 | ##### Description: 30 | 31 | Get a specific agent 32 | 33 | ##### Parameters 34 | 35 | | Name | Located in | Description | Required | Schema | 36 | | ---- | ---------- | ----------- | -------- | ---- | 37 | | ID | controllers/v1/agent/get.action.js | UUID of the agent to retrieve | Yes | string (uuid) | 38 | 39 | ##### Responses 40 | 41 | | Code | Description | Schema | 42 | | ---- | ----------- | ------ | 43 | | 200 | JSON representation of the agent | object | 44 | 45 | -------------------------------------------------------------------------------- /dashboard/src/hooks/usePluginEndpoint.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import useAuth from "hooks/useAuth"; 3 | import request from "utils/request"; 4 | 5 | export default (plugin, endpoint, reqBody) => { 6 | const [{ user }] = useAuth(); 7 | const [data, setData] = useState({}); 8 | const [loading, setLoading] = useState(true); 9 | const [error, setError] = useState(false); 10 | 11 | const getData = useCallback(async () => { 12 | try { 13 | setLoading(true); 14 | const enriched = await request( 15 | `v1/plugins/${plugin}/${endpoint}`, 16 | "post", 17 | { 18 | body: JSON.stringify(reqBody) 19 | }, 20 | user.tokens.api 21 | ); 22 | setData(enriched); 23 | setLoading(false); 24 | } catch (error) { 25 | setLoading(false); 26 | setError(true); 27 | } 28 | }, [endpoint, plugin, reqBody, user.tokens.api]); 29 | 30 | useEffect(() => { 31 | getData(); 32 | }, [getData]); 33 | 34 | return [data, { loading, error, refetch: getData }]; 35 | }; 36 | -------------------------------------------------------------------------------- /api/src/models/response.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import query from 'mongoose-string-query'; 3 | import timestamps from 'mongoose-timestamp'; 4 | import autopopulate from 'mongoose-autopopulate'; 5 | 6 | export const ResponseSchema = new Schema( 7 | { 8 | title: { 9 | type: String, 10 | trim: true, 11 | required: true, 12 | }, 13 | message: { 14 | type: String, 15 | trim: true, 16 | required: true, 17 | }, 18 | refs: { 19 | organization: { 20 | type: Schema.Types.ObjectId, 21 | ref: 'Organization', 22 | required: true, 23 | autopopulate: true, 24 | }, 25 | agent: { 26 | type: Schema.Types.ObjectId, 27 | ref: 'Agent', 28 | required: true, 29 | autopopulate: { 30 | select: ['name', 'email'], 31 | }, 32 | }, 33 | }, 34 | }, 35 | { 36 | collection: 'responses', 37 | } 38 | ); 39 | 40 | ResponseSchema.plugin(timestamps); 41 | ResponseSchema.plugin(query); 42 | ResponseSchema.plugin(autopopulate); 43 | 44 | ResponseSchema.index({ createdAt: 1, updatedAt: 1 }); 45 | 46 | module.exports = exports = mongoose.model('Response', ResponseSchema); 47 | -------------------------------------------------------------------------------- /api/src/models/faq.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import query from 'mongoose-string-query'; 3 | import timestamps from 'mongoose-timestamp'; 4 | import autopopulate from 'mongoose-autopopulate'; 5 | 6 | export const FaqSchema = new Schema( 7 | { 8 | meta: { 9 | question: { 10 | type: String, 11 | trim: true, 12 | required: true, 13 | unique: true, 14 | }, 15 | answer: { 16 | type: String, 17 | trim: true, 18 | required: true, 19 | }, 20 | }, 21 | refs: { 22 | organization: { 23 | type: Schema.Types.ObjectId, 24 | ref: 'Organization', 25 | required: true, 26 | autopopulate: true, 27 | }, 28 | agent: { 29 | type: Schema.Types.ObjectId, 30 | ref: 'Agent', 31 | required: true, 32 | autopopulate: { 33 | select: ['name', 'email'], 34 | }, 35 | }, 36 | }, 37 | }, 38 | { 39 | collection: 'faqs', 40 | } 41 | ); 42 | 43 | FaqSchema.plugin(timestamps); 44 | FaqSchema.plugin(query); 45 | FaqSchema.plugin(autopopulate); 46 | 47 | FaqSchema.index({ createdAt: 1, updatedAt: 1 }); 48 | 49 | module.exports = exports = mongoose.model('Faq', FaqSchema); 50 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Inbox/Inbox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | import compose from 'lodash.flowright'; 4 | 5 | // Views // 6 | import Threads from './views/Threads'; 7 | import MessageThread from './views/MessageThread'; 8 | 9 | // HOCs // 10 | import withAuth from 'hocs/withAuth'; 11 | import withChannels from '@comba.se/chat/hocs/withChannels'; 12 | 13 | // Components /// 14 | import ListDetailView from 'components/ListDetailView'; 15 | import ScreenRoot from 'shared/ScreenRoot'; 16 | 17 | const renderThreadList = props => ; 18 | const renderMessageThread = props => ( 19 | 23 | ); 24 | 25 | const Inbox = props => ( 26 | 27 | 31 | 32 | 33 | ); 34 | 35 | export default compose(withAuth, withChannels)(Inbox); -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Settings/views/SettingsDetail/views/OrganizationSettings/OrganizationSettings.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Container, ListHeader } from '@comba.se/ui'; 4 | import { OrganizationSettingsIcon } from "@comba.se/ui/Icons"; 5 | 6 | // Forms // 7 | import { OrganizationProfileForm, OrganizationChatSettingsForm } from 'screens/Dashboard/forms/OrganizationSettings'; 8 | 9 | // Hooks // 10 | import useMedia from 'hooks/useMedia'; 11 | 12 | // Components // 13 | import MenuButton from 'shared/MenuButton'; 14 | 15 | const Root = styled.div` 16 | flex: 1; 17 | `; 18 | 19 | const OrganizationSettings = () => { 20 | const isMobile = useMedia('sm'); 21 | return ( 22 | 23 | 24 | {!isMobile ? : null} 25 | 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default OrganizationSettings; 33 | -------------------------------------------------------------------------------- /dashboard/src/utils/GetRef.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import PropTypes from "prop-types"; 4 | 5 | class GetRef extends React.Component { 6 | componentDidMount() { 7 | this.ref = ReactDOM.findDOMNode(this); 8 | this.props.rootRef(this.ref); 9 | } 10 | 11 | componentDidUpdate(prevProps) { 12 | const ref = ReactDOM.findDOMNode(this); 13 | 14 | if (prevProps.rootRef !== this.props.rootRef || this.ref !== ref) { 15 | if (prevProps.rootRef !== this.props.rootRef) { 16 | prevProps.rootRef(null); 17 | } 18 | 19 | this.ref = ref; 20 | this.props.rootRef(this.ref); 21 | } 22 | } 23 | 24 | componentWillUnmount() { 25 | this.ref = null; 26 | this.props.rootRef(null); 27 | } 28 | 29 | render() { 30 | return this.props.children; 31 | } 32 | } 33 | 34 | GetRef.propTypes = { 35 | children: PropTypes.element, 36 | // Provide a way to access the DOM node of the wrapped element. 37 | // You can provide a callback ref or a `React.createRef()` ref. 38 | rootRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired 39 | }; 40 | 41 | export default GetRef; 42 | -------------------------------------------------------------------------------- /api/src/controllers/v1/plugin/integration/clearbit/enrich.action.js: -------------------------------------------------------------------------------- 1 | import { Client } from 'clearbit'; 2 | 3 | import Plugin from 'models/plugin'; 4 | 5 | exports.clearbitExecEnrich = async (req, res) => { 6 | try { 7 | const { email } = req.body; 8 | 9 | if (!email) { 10 | console.error('An email is required for enrichment.'); 11 | return res 12 | .status(400) 13 | .json({ error: 'An email is required for enrichment' }); 14 | } 15 | 16 | const { 17 | keys: [{ value: api_key }], 18 | } = await Plugin.findOne({ 19 | name: 'clearbit', 20 | keys: { $elemMatch: { name: 'api_key' } }, 21 | }) 22 | .select('keys') 23 | .lean({ autopopulate: false }); 24 | 25 | if (!api_key) { 26 | console.error('Clearbit has not been initialized.'); 27 | return res 28 | .status(400) 29 | .json({ error: 'Clearbit has not been initialized.' }); 30 | } 31 | 32 | const client = new Client({ key: api_key }); 33 | 34 | const { person: data } = await client.Enrichment.find({ email }); 35 | 36 | res.status(200).json(data); 37 | } catch (error) { 38 | console.error(error); 39 | res.status(500).json({ error: error.message }); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /api/src/controllers/v1/agent/get.action.js: -------------------------------------------------------------------------------- 1 | import Agent from 'models/agent'; 2 | 3 | /** 4 | * @swagger 5 | * GET /v1/agents:id: 6 | * get: 7 | * description: Get a specific agent 8 | * tags: [Agents] 9 | * produces: 10 | * - application/json 11 | * parameters: 12 | * - in: 'controllers/v1/agent/get.action.js' 13 | * name: ID 14 | * schema: 15 | * type: string 16 | * format: uuid 17 | * required: true 18 | * description: UUID of the agent to retrieve 19 | * responses: 20 | * 200: 21 | * description: JSON representation of the agent 22 | * schema: 23 | * type: object 24 | * properties: 25 | * message: 26 | * type: string 27 | */ 28 | exports.get = async (req, res) => { 29 | try { 30 | const data = { ...req.body, ...req.params }; 31 | 32 | const agent = await Agent.findById(data.agent); 33 | 34 | res.status(200).json(agent); 35 | } catch (error) { 36 | console.error(error); 37 | res.status(500).json({ error: error.message }); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /widget/src/App.js: -------------------------------------------------------------------------------- 1 | import 'whatwg-fetch'; 2 | 3 | import React from 'react'; 4 | import { StreamChatProvider } from 'stream-chat-hooks'; 5 | 6 | // Styles // 7 | import { ThemeProvider } from 'styled-components'; 8 | import { light as theme } from '@comba.se/ui/styles/theme'; 9 | import GlobalStyles from '@comba.se/ui/styles/global'; 10 | 11 | // Contexts // 12 | import { AuthProvider } from 'contexts/Auth'; 13 | import { SnackbarProvider } from "@comba.se/ui/Snackbar"; 14 | 15 | import Root from './Root'; 16 | 17 | const apiKey = "pyst6tqux4vf"; 18 | const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNWU1NTUyOTYxZTdmNzAwYWJkMGRmZjIzIn0.Z1WIshH9NZ54eVbcGeNOcfVSNGjUEOtLJ2FDuTfbtVI"; 19 | const user = { 20 | id: '5e5552961e7f700abd0dff23', 21 | name: 'Josh Tilton' 22 | }; 23 | 24 | function App() { 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | } 38 | 39 | export default App; 40 | -------------------------------------------------------------------------------- /api/src/models/plugin.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import query from 'mongoose-string-query'; 3 | import timestamps from 'mongoose-timestamp'; 4 | import autopopulate from 'mongoose-autopopulate'; 5 | 6 | export const PluginSchema = new Schema( 7 | { 8 | name: { 9 | type: String, 10 | trim: true, 11 | required: true 12 | }, 13 | keys: [ 14 | { 15 | name: { 16 | type: String, 17 | trim: true 18 | }, 19 | value: { 20 | type: String, 21 | trim: true 22 | } 23 | } 24 | ], 25 | refs: { 26 | organization: { 27 | type: Schema.Types.ObjectId, 28 | ref: 'Organization', 29 | required: true, 30 | autopopulate: { 31 | select: [ 'name' ] 32 | } 33 | } 34 | }, 35 | enabled: { 36 | type: Boolean, 37 | default: false 38 | } 39 | }, 40 | { 41 | collection: 'plugins' 42 | } 43 | ); 44 | 45 | PluginSchema.plugin(timestamps); 46 | PluginSchema.plugin(query); 47 | PluginSchema.plugin(autopopulate); 48 | 49 | PluginSchema.index({ createdAt: 1, updatedAt: 1 }); 50 | PluginSchema.index({ name: 1, 'refs.organization': 1 }, { unique: true }); 51 | 52 | module.exports = exports = mongoose.model('Plugin', PluginSchema); 53 | -------------------------------------------------------------------------------- /dashboard/src/components/Tabs/Tabs.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | 5 | // Components // 6 | import Tab from './Tab'; 7 | 8 | const Root = styled.div` 9 | flex: 1; 10 | flex-direction: row; 11 | align-items: center; 12 | padding: 24px 16px; 13 | overflow-x: scroll; 14 | 15 | & > * + * { 16 | margin-left: 16px; 17 | } 18 | 19 | &:last-child { 20 | margin-right: 16px; 21 | } 22 | 23 | @media (min-width: ${({ theme }) => theme.breakpoints.sm}px) { 24 | padding: 24px 40px; 25 | 26 | & > * + * { 27 | margin-left: 24px; 28 | } 29 | } 30 | `; 31 | 32 | const renderTabs = (tabs, activeTab, onClick) => 33 | tabs.map((tab, key) => ( 34 | 35 | )); 36 | 37 | const Tabs = ({ active, onTabClick, tabs }) => { 38 | return {renderTabs(tabs, active, onTabClick)}; 39 | }; 40 | 41 | Tabs.propTypes = { 42 | active: PropTypes.any, 43 | onTabClick: PropTypes.func, 44 | tabs: PropTypes.array, 45 | }; 46 | 47 | export default Tabs; 48 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # computed 64 | dist/ 65 | 66 | # osx 67 | .DS_Store -------------------------------------------------------------------------------- /api/src/controllers/v1/agent/destroy.action.js: -------------------------------------------------------------------------------- 1 | import Agent from 'models/agent'; 2 | 3 | /** 4 | * @swagger 5 | * DELETE /v1/agents/:id: 6 | * delete: 7 | * description: Deletes an agent 8 | * tags: [Agents] 9 | * produces: 10 | * - application/json 11 | * parameters: 12 | * - in: 'controllers/v1/agent/destroy.action.js' 13 | * name: ID 14 | * schema: 15 | * type: string 16 | * format: uuid 17 | * required: true 18 | * description: UUID of agent to delete 19 | * responses: 20 | * 204: 21 | * description: No response 22 | */ 23 | exports.destroy = async (req, res) => { 24 | try { 25 | const { agent } = req.params; 26 | const { serialized } = req; 27 | 28 | if (serialized.role !== 'admin') { 29 | return res.status(403).json({ 30 | status: 'Invalid permissions to view or modify this resource.' 31 | }); 32 | } 33 | 34 | const { password, ...removed } = await Agent.findByIdAndRemove(agent); 35 | 36 | res.sendStatus(204); 37 | } catch (error) { 38 | console.error(error); 39 | res.status(500).json({ error: error.message }); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /dashboard/src/hooks/useNotifications.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useContext, useEffect } from 'react'; 2 | import { useChatClient } from 'stream-chat-hooks'; 3 | import UIfx from 'uifx'; 4 | 5 | // Contexts // 6 | import ShellContext from 'contexts/Shell'; 7 | 8 | // Sounds // 9 | import newMessageSound from 'sounds/new_message.wav'; 10 | import newConversationSound from 'sounds/new_conversation.wav'; 11 | 12 | const messageChime = new UIfx(newMessageSound); 13 | const conversationChime = new UIfx(newConversationSound); 14 | 15 | export default () => { 16 | const client = useChatClient(); 17 | const { sounds } = useContext(ShellContext); 18 | 19 | const handleNewMessage = useCallback(() => { 20 | messageChime.play(); 21 | }, []); 22 | 23 | const handleNewConversation = useCallback(() => { 24 | conversationChime.play(); 25 | }, []); 26 | 27 | useEffect(() => { 28 | if (sounds.enabled) { 29 | client.on('notification.message_new', handleNewMessage); 30 | client.on('notification.added_to_channel', handleNewConversation); 31 | } 32 | return () => sounds.enabled ? client.off('message.new', handleNewMessage) : null; 33 | }, [client, handleNewConversation, handleNewMessage, sounds.enabled]); 34 | } -------------------------------------------------------------------------------- /dashboard/src/components/SettingsListItem.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import styled from "styled-components"; 4 | import { IconBubble, Text } from '@comba.se/ui'; 5 | 6 | // Components // 7 | const Root = styled.div` 8 | padding: 16px 0px; 9 | flex-direction: row; 10 | align-items: center; 11 | justify-content: space-between; 12 | @media (min-width: ${({ theme }) => theme.breakpoints.sm}px) { 13 | padding: 16px; 14 | } 15 | `; 16 | 17 | const TitleBlock = styled.div` 18 | flex-direction: row; 19 | align-items: center; 20 | `; 21 | 22 | const Content = styled.div` 23 | margin-left: 24px; 24 | `; 25 | 26 | const SettingsListItem = ({ children, color, icon, text, title }) => ( 27 | 28 | 29 | 30 | 31 | {title} 32 | 33 | {text} 34 | 35 | 36 | 37 | {children} 38 | 39 | ); 40 | 41 | SettingsListItem.propTypes = { 42 | color: PropTypes.string, 43 | icon: PropTypes.func, 44 | text: PropTypes.string, 45 | title: PropTypes.string 46 | }; 47 | 48 | export default SettingsListItem; 49 | -------------------------------------------------------------------------------- /dashboard/src/screens/Auth/views/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Avatar, Container, Text } from '@comba.se/ui'; 4 | 5 | // Hooks // 6 | import useAuth from 'hooks/useAuth'; 7 | 8 | // Forms // 9 | import LoginForm from '../forms/LoginForm'; 10 | 11 | // Components // 12 | const Root = styled(Container)` 13 | flex: 1; 14 | justify-content: center; 15 | `; 16 | 17 | const Header = styled.div` 18 | padding: 32px 0px; 19 | justify-content: center; 20 | align-items: center; 21 | text-align: center; 22 | `; 23 | 24 | const Login = () => { 25 | const [{ organization }] = useAuth(); 26 | return ( 27 | 28 |
29 | 35 | 36 | {organization.name} 37 | 38 | Customer Support Chat 39 |
40 | 41 |
42 | ); 43 | }; 44 | 45 | export default Login; 46 | -------------------------------------------------------------------------------- /api/src/models/invite.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import query from 'mongoose-string-query'; 3 | import timestamps from 'mongoose-timestamp'; 4 | import autopopulate from 'mongoose-autopopulate'; 5 | import moment from 'moment'; 6 | 7 | export const InviteSchema = new Schema( 8 | { 9 | name: { 10 | first: { 11 | type: String, 12 | trim: true, 13 | required: true 14 | }, 15 | last: { 16 | type: String, 17 | trim: true, 18 | required: true 19 | } 20 | }, 21 | email: { 22 | type: String, 23 | lowercase: true, 24 | trim: true, 25 | required: true 26 | }, 27 | refs: { 28 | organization: { 29 | type: Schema.Types.ObjectId, 30 | ref: 'Organization', 31 | required: true, 32 | autopopulate: true 33 | } 34 | }, 35 | expiration: { 36 | type: Date, 37 | default: moment().add('48', 'hours').toISOString() 38 | }, 39 | accepted: { 40 | type: Boolean, 41 | default: false 42 | } 43 | }, 44 | { 45 | collection: 'invites' 46 | } 47 | ); 48 | 49 | InviteSchema.plugin(timestamps); 50 | InviteSchema.plugin(query); 51 | InviteSchema.plugin(autopopulate); 52 | 53 | InviteSchema.index({ createdAt: 1, updatedAt: 1 }); 54 | 55 | module.exports = exports = mongoose.model('Invite', InviteSchema); 56 | -------------------------------------------------------------------------------- /api/src/controllers/v1/chat/exchange.action.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { StreamChat } from 'stream-chat'; 3 | 4 | import Agent from 'models/agent'; 5 | import Chat from 'models/chat'; 6 | import StreamClient from 'utils/stream'; 7 | 8 | exports.exchange = async (req, res) => { 9 | try { 10 | const data = req.body; 11 | 12 | const { key, secret } = await StreamClient(); 13 | const client = new StreamChat(key, secret); 14 | 15 | // get the new agent 16 | const invitee = await Agent.findById(data.invitee).lean(); 17 | const current = await Agent.findById(data.current).lean(); 18 | 19 | // initialize the channel 20 | const channel = client.channel('messaging', data.chat); 21 | 22 | // add the new agent and remove the old agent 23 | await channel.addMembers([ invitee._id ]); 24 | await channel.removeMembers([ current._id ]); 25 | 26 | // update database with user exchange for agent trail (timestamp is automatically generated) 27 | const chat = await Chat.findByIdAndUpdate(data.chat, { 28 | $set: { 29 | refs: { 30 | agents: { 31 | assignee: invitee._id 32 | } 33 | } 34 | } 35 | }); 36 | 37 | res.status(200).json(chat); 38 | } catch (error) { 39 | console.error(error); 40 | res.status(500).json({ error: error.message }); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /dashboard/src/shared/UserBlock.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { Avatar, Text } from '@comba.se/ui'; 5 | 6 | // Components // 7 | const Root = styled.div` 8 | flex-direction: row; 9 | align-items: center; 10 | `; 11 | 12 | const Content = styled.div` 13 | margin-left: 24px; 14 | `; 15 | 16 | const UserBlock = ({ avatar, avatarSize, meta, metaSize, name, textSize }) => ( 17 | 18 | 19 | 20 | 21 | {name} 22 | 23 | 24 | {meta} 25 | 26 | 27 | 28 | ); 29 | 30 | UserBlock.propTypes = { 31 | avatar: PropTypes.string.isRequired, 32 | avatarSize: PropTypes.number.isRequired, 33 | meta: PropTypes.string.isRequired, 34 | metaSize: PropTypes.number, 35 | name: PropTypes.string.isRequired, 36 | textSize: PropTypes.number.isRequired, 37 | }; 38 | 39 | UserBlock.defaultProps = { 40 | avatarSize: 48, 41 | textSize: 16, 42 | }; 43 | 44 | export default UserBlock; 45 | -------------------------------------------------------------------------------- /api/src/controllers/v1/plugin/integration/blaze_verify/verify.action.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import Plugin from 'models/plugin'; 4 | 5 | exports.blazeVerifyExecVerify = async (req, res) => { 6 | try { 7 | const { email } = req.body; 8 | 9 | if (!email) { 10 | console.error('An email is required for verification.'); 11 | return res 12 | .status(400) 13 | .json({ error: 'An email is required for verification' }); 14 | } 15 | 16 | const { 17 | keys: [{ value: api_key }], 18 | } = await Plugin.findOne({ 19 | name: 'blaze_verify', 20 | keys: { $elemMatch: { name: 'api_key' } }, 21 | }) 22 | .select('keys') 23 | .lean({ autopopulate: false }); 24 | 25 | if (!api_key) { 26 | console.error('Blaze Verify has not been initialized.'); 27 | return res 28 | .status(400) 29 | .json({ error: 'Blaze Verify has not been initialized.' }); 30 | } 31 | 32 | const { 33 | data: { disposable, state, domain, score, gender }, 34 | } = await axios.get( 35 | `https://api.blazeverify.com/v1/verify?email=${email.toLowerCase()}&api_key=${api_key}` 36 | ); 37 | 38 | res.status(200).json({ disposable, state, domain, score, gender }); 39 | } catch (error) { 40 | console.error(error); 41 | res.status(500).json({ error: error.message }); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /dashboard/src/components/ListDetailView/ListDetailView.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled, { withTheme } from "styled-components"; 3 | import Animated from "animated/lib/targets/react-dom"; 4 | import { Switch } from "react-router-dom"; 5 | 6 | // Hooks // 7 | import useMedia from "hooks/useMedia"; 8 | 9 | // Components // 10 | import ListDetailTransition from "./ListDetailTransition"; 11 | 12 | const Root = styled.div` 13 | flex: 1; 14 | flex-direction: row; 15 | height: 100%; 16 | overflow: hidden; 17 | `; 18 | 19 | const anim = new Animated.Value(0); 20 | const ListDetailView = ({ 21 | children, 22 | location, 23 | match, 24 | rootAs, 25 | transitionAnim = anim 26 | }) => { 27 | const [animating, setAnimating] = useState(false); 28 | const useStack = useMedia("sm"); 29 | if (useStack) { 30 | return ( 31 | 32 | 37 | {children} 38 | 39 | 40 | ); 41 | } 42 | return {children}; 43 | }; 44 | 45 | export default withTheme(ListDetailView); 46 | -------------------------------------------------------------------------------- /dashboard/src/components/PageSheet/PageSheet.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Card, LoadingState } from '@comba.se/ui'; 4 | 5 | // Components // 6 | import Tabs from 'components/Tabs'; 7 | import SearchHeader from './SearchHeader'; 8 | 9 | const PageSheet = ({ 10 | activeTab, 11 | children, 12 | loading, 13 | onQueryChange, 14 | setActiveTab, 15 | showSearch, 16 | tabs, 17 | ...rest 18 | }) => { 19 | return ( 20 | 21 | {showSearch ? : null} 22 | {tabs && tabs.length ? ( 23 | 28 | ) : null} 29 | {loading ? : children} 30 | 31 | ); 32 | }; 33 | 34 | PageSheet.propTypes = { 35 | activeTab: PropTypes.string, 36 | loading: PropTypes.bool, 37 | onQueryChange: PropTypes.func, 38 | setActiveTab: PropTypes.func, 39 | showSearch: PropTypes.bool, 40 | tabs: PropTypes.array, 41 | }; 42 | 43 | PageSheet.defaultProps = { 44 | showSearch: true, 45 | }; 46 | 47 | export default PageSheet; 48 | -------------------------------------------------------------------------------- /widget/src/components/ListDetailView/ListDetailView.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled, { withTheme } from "styled-components"; 3 | import Animated from "animated/lib/targets/react-dom"; 4 | import { Switch } from "react-router-dom"; 5 | 6 | // Hooks // 7 | import useMedia from "hooks/useMedia"; 8 | 9 | // Components // 10 | import ListDetailTransition from "./ListDetailTransition"; 11 | 12 | const Root = styled.div` 13 | flex: 1; 14 | flex-direction: row; 15 | height: 100%; 16 | overflow: hidden; 17 | `; 18 | 19 | const anim = new Animated.Value(0); 20 | const ListDetailView = ({ 21 | children, 22 | location, 23 | match, 24 | rootAs, 25 | transitionAnim = anim 26 | }) => { 27 | const [animating, setAnimating] = useState(false); 28 | const useStack = useMedia("sm"); 29 | if (useStack) { 30 | return ( 31 | 32 | 37 | {children} 38 | 39 | 40 | ); 41 | } 42 | return {children}; 43 | }; 44 | 45 | export default withTheme(ListDetailView); 46 | -------------------------------------------------------------------------------- /dashboard/src/shared/IconLabel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { PlaceholderIcon, Text } from '@comba.se/ui'; 5 | 6 | // Components // 7 | const Root = styled.div` 8 | flex: 1; 9 | padding: 8px; 10 | justify-content: center; 11 | align-items: center; 12 | 13 | & > ${Text} { 14 | margin-top: 8px; 15 | } 16 | `; 17 | 18 | const IconLabel = ({ icon: Icon, iconColor, iconSize, label, labelColor, labelSize, showPlaceholder }) => { 19 | return ( 20 | 21 | {showPlaceholder ? : } 22 | {label} 23 | 24 | ); 25 | }; 26 | 27 | IconLabel.propTypes = { 28 | icon: PropTypes.func.isRequired, 29 | iconColor: PropTypes.string, 30 | iconSize: PropTypes.number, 31 | label: PropTypes.string.isRequired, 32 | labelColor: PropTypes.string, 33 | labelSize: PropTypes.number, 34 | showPlaceholder: PropTypes.bool, 35 | }; 36 | 37 | IconLabel.defaultProps = { 38 | iconColor: 'primary', 39 | iconSize: 32, 40 | labelColor: 'alt_text', 41 | labelSize: 12, 42 | showPlaceholder: false, 43 | }; 44 | 45 | export default IconLabel; -------------------------------------------------------------------------------- /dashboard/src/components/PageSheet/SearchHeader.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, memo } from 'react'; 2 | import styled from 'styled-components'; 3 | import { SearchIcon } from "@comba.se/ui/Icons"; 4 | 5 | // Components // 6 | const Root = styled.div` 7 | height: 64px; 8 | flex-direction: row; 9 | border-bottom: 1px solid ${({ theme }) => theme.color.border}; 10 | padding: 0px 12px; 11 | `; 12 | 13 | const IconWrapper = styled.div` 14 | padding: 0px 12px; 15 | justify-content: center; 16 | align-items: center; 17 | `; 18 | 19 | const Input = styled.input` 20 | flex: 1; 21 | border: 0; 22 | font-size: 16px; 23 | font-weight: 500; 24 | color: ${({ theme }) => theme.color.text}; 25 | &::placeholder { 26 | color: ${({ theme }) => theme.color.gray}; 27 | } 28 | `; 29 | 30 | const SearchHeader = ({ onChange }) => { 31 | const handleChange = useCallback( 32 | ({ target: { value } }) => { 33 | onChange(value); 34 | }, 35 | [onChange] 36 | ); 37 | return ( 38 | 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default memo(SearchHeader); 48 | -------------------------------------------------------------------------------- /dashboard/src/hooks/useInvite.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import moment from "moment"; 3 | 4 | // Utils // 5 | import request from "utils/request"; 6 | 7 | export default inviteId => { 8 | const [invite, setInvite] = useState(null); 9 | const [loading, setLoading] = useState(true); 10 | const [error, setError] = useState(false); 11 | const [expired, setExpired] = useState(false); 12 | 13 | const fetchInvite = useCallback(async () => { 14 | if (inviteId) { 15 | try { 16 | setLoading(true); 17 | const invite = await request(`v1/invites/${inviteId}`, "get"); 18 | setLoading(false); 19 | if (invite.error) { 20 | throw new Error("Something went wrong"); 21 | } 22 | 23 | setInvite(invite); 24 | if (invite.accepted || moment(invite.expiration).isBefore(moment())) { 25 | throw new Error("Invitation Expired"); 26 | } 27 | } catch (error) { 28 | setLoading(false); 29 | if (error.message === "Invitation Expired") { 30 | setExpired(true); 31 | } else { 32 | setError(true); 33 | } 34 | } 35 | } 36 | }, [inviteId]); 37 | 38 | useEffect(() => { 39 | fetchInvite(); 40 | }, [fetchInvite]); 41 | 42 | return [invite, { loading, error, expired }]; 43 | }; 44 | -------------------------------------------------------------------------------- /dashboard/src/components/Tabs/Tab.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Text } from '@comba.se/ui'; 4 | 5 | // Components // 6 | const Root = styled.div` 7 | padding: 4px 8px; 8 | border-radius: 9999px; 9 | background-color: ${({ active, theme }) => 10 | active ? theme.color.primary : 'transparent'}; 11 | cursor: pointer; 12 | transition: 0.24s background-color 13 | ${({ theme }) => theme.easing.css(theme.easing.standard)}; 14 | 15 | & > ${Text} { 16 | text-transform: capitalize; 17 | user-select: none; 18 | } 19 | 20 | &:hover { 21 | background-color: ${({ active, theme }) => 22 | !active ? theme.colorUtils.fade(theme.color.gray, 0.24) : null}; 23 | } 24 | 25 | &:active { 26 | background-color: ${({ active, theme }) => 27 | !active ? theme.colorUtils.fade(theme.color.gray, 0.48) : null}; 28 | } 29 | `; 30 | 31 | export default memo(({ active, label, onClick }) => { 32 | const handleClick = useCallback(() => { 33 | return onClick ? onClick(label) : null; 34 | }, [label, onClick]); 35 | 36 | return ( 37 | 38 | {label} 39 | 40 | ); 41 | }); 42 | -------------------------------------------------------------------------------- /dashboard/src/components/OrganizationCard.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Card, Text } from '@comba.se/ui'; 4 | 5 | // Hooks // 6 | import useAuth from 'hooks/useAuth'; 7 | 8 | // Components // 9 | const Root = styled(Card)` 10 | padding: 4px; 11 | cursor: pointer; 12 | `; 13 | 14 | const Logo = styled.div` 15 | width: 100%; 16 | padding-top: 100%; 17 | border-radius: ${({ theme }) => theme.borderRadius}px; 18 | background-image: ${({ src }) => `url(${src})`}; 19 | background-size: cover; 20 | background-position: center; 21 | overflow: hidden; 22 | `; 23 | 24 | const Meta = styled.div` 25 | padding: 8px; 26 | `; 27 | 28 | const OrganizationCard = ({ id, logo, name, tagline }) => { 29 | const [_, { setCurrentOrganization }] = useAuth(); // eslint-disable-line no-unused-vars 30 | const onClick = useCallback(() => { 31 | setCurrentOrganization(id); 32 | }, [id, setCurrentOrganization]); 33 | return ( 34 | 35 | 36 | 37 | {name} 38 | 39 | {tagline} 40 | 41 | 42 | 43 | ); 44 | }; 45 | 46 | export default OrganizationCard; 47 | -------------------------------------------------------------------------------- /dashboard/src/components/MobileHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { IconButton, Text } from '@comba.se/ui'; 5 | import { ArrowBackIcon } from "@comba.se/ui/Icons"; 6 | 7 | // Components // 8 | import MenuButton from "shared/MenuButton"; 9 | 10 | const Root = styled.div` 11 | flex: 0 0 64px; 12 | flex-direction: row; 13 | align-items: center; 14 | padding: 0px 16px; 15 | & ${Text} { 16 | margin-left: 16px; 17 | } 18 | `; 19 | 20 | const MobileHeader = ({ color, showBackBtn, onBackClick, title }) => { 21 | return ( 22 | 23 | {showBackBtn ? ( 24 | 25 | ) : ( 26 | 27 | )} 28 | {title ? ( 29 | 30 | {title} 31 | 32 | ) : null} 33 | 34 | ); 35 | }; 36 | 37 | MobileHeader.propTypes = { 38 | color: PropTypes.string, 39 | showBackBtn: PropTypes.bool, 40 | onBackClick: PropTypes.func, 41 | title: PropTypes.string, 42 | }; 43 | 44 | MobileHeader.defaultProps = { 45 | color: 'text', 46 | showBackBtn: false, 47 | } 48 | 49 | export default MobileHeader; -------------------------------------------------------------------------------- /dashboard/src/components/AgentItem.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo, useState } from "react"; 2 | import styled from "styled-components"; 3 | import { Link } from "react-router-dom"; 4 | import { Text } from '@comba.se/ui'; 5 | 6 | // Styles // 7 | import listItemInteractions from "styles/css/listItemInteractions"; 8 | 9 | // Components // 10 | import UserBlock from "shared/UserBlock"; 11 | 12 | const Root = styled.div` 13 | padding: 0px 16px; 14 | flex-direction: row; 15 | align-items: center; 16 | height: 80px; 17 | cursor: pointer; 18 | ${listItemInteractions} 19 | & ${Text} { 20 | user-select: none; 21 | } 22 | 23 | @media (min-width: ${({ theme }) => theme.breakpoints.sm}px) { 24 | padding: 0px 40px; 25 | } 26 | 27 | `; 28 | const AgentItem = ({ _id, email, image, name }) => { 29 | const [dims, setDims] = useState(null); 30 | const rootRef = useCallback(el => { 31 | if (el) { 32 | setDims(el.getBoundingClientRect()); 33 | } 34 | }, []); 35 | const to = useMemo( 36 | () => ({ 37 | pathname: `/agents/${_id}`, 38 | startDims: dims 39 | }), 40 | [dims, _id] 41 | ); 42 | return ( 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | }; 50 | 51 | export default AgentItem; 52 | -------------------------------------------------------------------------------- /widget/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "widget", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@comba.se/chat": "^0.0.31", 7 | "@comba.se/ui": "^0.0.79", 8 | "@juggle/resize-observer": "^3.1.1", 9 | "@testing-library/jest-dom": "^5.1.1", 10 | "@testing-library/react": "^9.5.0", 11 | "@testing-library/user-event": "^10.0.0", 12 | "animated": "^0.2.2", 13 | "lodash.flowright": "^3.5.0", 14 | "moment": "^2.24.0", 15 | "react": "^16.13.0", 16 | "react-dom": "^16.13.0", 17 | "react-lottie": "^1.2.3", 18 | "react-router-dom": "^5.1.2", 19 | "react-scripts": "3.4.0", 20 | "react-spring": "^8.0.27", 21 | "recyclerlistview": "^3.0.0", 22 | "stream-chat": "^1.4.0", 23 | "stream-chat-hooks": "^0.0.9", 24 | "styled-components": "^5.0.1", 25 | "whatwg-fetch": "^3.0.0" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": "react-app" 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /dashboard/src/hooks/usePlugin.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useContext, useMemo } from "react"; 2 | import useAuth from "hooks/useAuth"; 3 | import PluginsContext from "contexts/Plugins"; 4 | import { useSnackbar } from "contexts/Snackbar"; 5 | import request from "utils/request"; 6 | 7 | export default slug => { 8 | const [{ user }] = useAuth(); 9 | const { queueSnackbar } = useSnackbar(); 10 | const [activePlugins, { refetch }] = useContext(PluginsContext); 11 | const plugin = useMemo(() => { 12 | if (!slug || !activePlugins) { 13 | return null; 14 | } 15 | return activePlugins[slug]; 16 | }, [slug, activePlugins]); 17 | 18 | const togglePlugin = useCallback( 19 | async ({ target: { checked } }) => { 20 | try { 21 | await request( 22 | `v1/plugins/${plugin._id}`, 23 | "put", 24 | { 25 | body: JSON.stringify({ 26 | ...plugin, 27 | enabled: checked 28 | }) 29 | }, 30 | user.tokens.api 31 | ); 32 | await refetch(); 33 | } catch (error) { 34 | queueSnackbar({ 35 | replace: true, 36 | isError: true, 37 | text: error.message 38 | }); 39 | console.log(error); 40 | } 41 | }, 42 | [plugin, user.tokens.api, refetch, queueSnackbar] 43 | ); 44 | 45 | return [plugin, refetch, togglePlugin]; 46 | }; 47 | -------------------------------------------------------------------------------- /dashboard/src/screens/Auth/Auth.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Redirect, Route, Switch } from "react-router-dom"; 3 | import styled from "styled-components"; 4 | import { Text } from '@comba.se/ui'; 5 | 6 | // Views // 7 | import Invite from "./views/Invite"; 8 | import Login from "./views/Login"; 9 | import SignUp from "./views/SignUp"; 10 | 11 | // Components // 12 | import StreamLogo from "shared/StreamLogo"; 13 | 14 | const Root = styled.div` 15 | flex: 1; 16 | overflow-y: scroll; 17 | `; 18 | 19 | const Credit = styled.div` 20 | padding: 40px 0px; 21 | justify-content: center; 22 | align-items: center; 23 | text-align: center; 24 | `; 25 | 26 | const redirectToLogin = () => ; 27 | 28 | const Auth = ({ match }) => { 29 | return ( 30 | 31 | 32 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | Powered by Stream. 45 | 46 | 47 | 48 | ); 49 | }; 50 | 51 | export default Auth; 52 | -------------------------------------------------------------------------------- /widget/src/screens/Home/Home.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Container } from '@comba.se/ui'; 4 | 5 | // Contexts // 6 | import { ScrollAnimationProvider } from 'contexts/ScrollAnimation'; 7 | 8 | // Components // 9 | import Header from 'components/Home/Header'; 10 | import ConversationsWidget from 'components/Home/ConversationsWidget'; 11 | import KnowledgeBaseWidget from 'components/Home/KnowledgeBaseWidget'; 12 | 13 | const Root = styled.div` 14 | flex: 1; 15 | width: 100%; 16 | overflow: scroll; 17 | `; 18 | 19 | const Content = styled(Container)` 20 | z-index: 2; 21 | padding-bottom: 40px; 22 | margin-top: 280px; 23 | 24 | & > * + * { 25 | margin-top: 24px; 26 | } 27 | `; 28 | 29 | const Home = () => { 30 | const [rootRef, setRootRef] = useState(); 31 | 32 | const ref = useCallback((el) =>{ 33 | if (el && !rootRef) { 34 | setRootRef(el); 35 | } 36 | }, [rootRef]); 37 | 38 | return ( 39 | 40 | 41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | }; 50 | 51 | export default Home; -------------------------------------------------------------------------------- /api/src/controllers/v1/upload/post.action.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import DataURI from 'datauri'; 3 | import path from 'path'; 4 | import { v2 as cloudinary } from 'cloudinary'; 5 | 6 | exports.post = (req, res) => { 7 | try { 8 | let cloudinaryKey; 9 | let cloudinarySecret; 10 | 11 | // if this env is found, it's assumed that the api is running on heroku 12 | if (process.env.CLOUDINARY_URL) { 13 | // extract the key and secret from the environment variable 14 | [ 15 | cloudinaryKey, 16 | cloudinarySecret, 17 | ] = process.env.CLOUDINARY_URL.substr(13) 18 | .split('@')[0] 19 | .split(':'); 20 | } else { 21 | // api key and secret were provided from a .env file 22 | cloudinaryKey = process.env.CLOUDINARY_API_KEY; 23 | cloudinarySecret = process.env.CLOUDINARY_API_SECRET; 24 | } 25 | 26 | const datauri = new DataURI(); 27 | const file = datauri.format( 28 | path.extname(req.file.originalname).toString(), 29 | req.file.buffer 30 | ); 31 | 32 | cloudinary.uploader.upload(file.content, (error, data) => { 33 | if (error) { 34 | console.error(error); 35 | return res.sendStatus(500); 36 | } 37 | 38 | res.status(200).json({ 39 | dimensions: { 40 | height: data.height, 41 | width: data.width, 42 | }, 43 | format: data.format, 44 | url: data.url, 45 | }); 46 | }); 47 | } catch (error) { 48 | console.error(error); 49 | res.status(500).json({ error: error.message }); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Combase", 3 | "description": "Open Source & White Label Customer Support Chat – Powered by Stream", 4 | "keywords": [ "Customer Support", "Stream", "Open Source", "Chat", "White Label" ], 5 | "website": "https://comba.se/", 6 | "repository": "https://github.com/GetStream/combase", 7 | "logo": "https://i.imgur.com/jwPM769.png", 8 | "success_url": "https://github.com/GetStream/combase", 9 | "formation": { 10 | "web": { 11 | "quantity": 1, 12 | "size": "free" 13 | } 14 | }, 15 | "image": "heroku/nodejs", 16 | "addons": [ 17 | { 18 | "plan": "stream:chat-trial", 19 | "as": "STREAM" 20 | }, 21 | { 22 | "plan": "mongolab:sandbox", 23 | "as": "MONGODB" 24 | }, 25 | { 26 | "plan": "rediscloud:30", 27 | "as": "REDIS" 28 | }, 29 | { 30 | "plan": "algoliasearch:free", 31 | "as": "ALGOLIASEARCH" 32 | }, 33 | { 34 | "plan": "timber-logging:free", 35 | "as": "TIMBER_LOGGING" 36 | }, 37 | { 38 | "plan": "cloudinary:starter", 39 | "as": "CLOUDINARY" 40 | } 41 | ], 42 | "buildpacks": [ 43 | { 44 | "url": "https://github.com/lstoll/heroku-buildpack-monorepo" 45 | }, 46 | { 47 | "url": "heroku/nodejs" 48 | } 49 | ], 50 | "env": { 51 | "AUTH_SECRET": { 52 | "description": "A secret key for verifying the integrity of signed JWTs.", 53 | "generator": "secret" 54 | }, 55 | "APP_BASE": { 56 | "description": "Absolute path to API in monorepo (default = /api).", 57 | "value": "api/" 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /dashboard/src/shared/StreamLogo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | import { Icon } from '@comba.se/ui'; 3 | 4 | const StreamLogo = props => ( 5 | 6 | 7 | 8 | ); 9 | 10 | export default StreamLogo; 11 | -------------------------------------------------------------------------------- /dashboard/src/hooks/useActivePlugins.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | 3 | // Hooks // 4 | import useAuth from "hooks/useAuth"; 5 | import { useSnackbar } from "contexts/Snackbar"; 6 | 7 | // Utils // 8 | import request from "utils/request"; 9 | 10 | export default () => { 11 | const { queueSnackbar } = useSnackbar(); 12 | const [loading, setLoading] = useState(false); 13 | const [plugins, setPlugins] = useState( 14 | JSON.parse(localStorage.getItem("plugins")) || null 15 | ); 16 | const [{ user }] = useAuth(); 17 | 18 | const fetchPlugins = useCallback(async () => { 19 | try { 20 | setLoading(true); 21 | const data = await request( 22 | `v1/plugins?refs.organization._id=${user.refs.organization._id}`, 23 | "get", 24 | null, 25 | user.tokens.api 26 | ); 27 | let pluginData = {}; 28 | data.forEach(plugin => { 29 | pluginData[plugin.name] = plugin; 30 | }); 31 | setPlugins(pluginData); 32 | localStorage.setItem("plugins", JSON.stringify(pluginData)); 33 | setLoading(false); 34 | } catch (error) { 35 | setLoading(false); 36 | queueSnackbar({ 37 | replace: true, 38 | isError: true, 39 | text: error.message 40 | }); 41 | } 42 | }, [user, queueSnackbar]); // eslint-disable-line react-hooks/exhaustive-deps 43 | useEffect(() => { 44 | fetchPlugins(); 45 | }, [fetchPlugins]); 46 | return [plugins, { refetch: fetchPlugins, loading }]; 47 | }; 48 | -------------------------------------------------------------------------------- /dashboard/src/styles/theme/index.js: -------------------------------------------------------------------------------- 1 | import colors from "./colors"; 2 | import baseTheme from "./base"; 3 | 4 | export const light = (overrides) => ({ 5 | ...baseTheme, 6 | color: { 7 | ...colors, 8 | primary: colors.blue, 9 | background: colors.off_white, 10 | surface: colors.white, 11 | error: colors.red, 12 | disabled: colors.gray, 13 | light_text: colors.light_gray, 14 | shadow: colors.black, 15 | border: colors.light_gray, 16 | text: colors.black, 17 | alt_text: colors.slate, 18 | undersheet: baseTheme.colorUtils.fade(colors.black, 0.64), 19 | placeholder: colors.light_gray, 20 | placeholder_shimmer: baseTheme.colorUtils.lighten(colors.light_gray, .64), 21 | ...overrides, 22 | } 23 | }); 24 | 25 | export const dark = overrides => ({ 26 | ...baseTheme, 27 | color: { 28 | ...colors, 29 | primary: colors.blue, 30 | background: baseTheme.colorUtils.darken(colors.black, 0.2), 31 | surface: colors.black, 32 | error: colors.red, 33 | disabled: colors.gray, 34 | light_text: baseTheme.colorUtils.fade(colors.white, 0.32), 35 | shadow: colors.black, 36 | border: baseTheme.colorUtils.fade(colors.white, 0.05), 37 | text: colors.white, 38 | alt_text: baseTheme.colorUtils.fade(colors.white, 0.8), 39 | undersheet: baseTheme.colorUtils.fade(colors.black, 0.64), 40 | placeholder: baseTheme.colorUtils.darken(colors.black, .2), 41 | placeholder_shimmer: baseTheme.colorUtils.darken(colors.black, .1), 42 | ...overrides, 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /api/src/controllers/v1/transcript/get.action.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import fs from 'fs'; 3 | import ejs from 'ejs'; 4 | import moment from 'moment'; 5 | import { StreamChat } from 'stream-chat'; 6 | 7 | import Chat from 'models/chat'; 8 | import StreamClient from 'utils/stream'; 9 | 10 | exports.get = async (req, res) => { 11 | try { 12 | const data = req.query; 13 | 14 | if (!process.env.POSTMARK_KEY) { 15 | return res.status(400).json({ 16 | error: 17 | 'Missing POSTMARK_KEY environment variable. Please add this to your .env file or Heroku settings.' 18 | }); 19 | } 20 | 21 | const chat = await Chat.findById(data.chat); 22 | 23 | const { key, secret } = await StreamClient(); 24 | const client = new StreamChat(key, secret); 25 | 26 | const channel = client.channel('commerce', data.chat); 27 | const { messages } = await channel.query({ 28 | messages: { limit: 1000, offset: 0 }, 29 | members: { limit: 2, offset: 0 } 30 | }); 31 | 32 | const transcript = messages.map((message) => { 33 | return { 34 | name: message.user.name.split(' ')[0] + ' ' + message.user.name.split(' ')[1].charAt(0) + '.', 35 | text: message.text, 36 | timestamp: moment(message.created_at).format('LLLL'), 37 | attachments: message.attachments.map((attachment) => { 38 | return { 39 | url: attachment.asset_url 40 | }; 41 | }) 42 | }; 43 | }); 44 | 45 | res.status(200).json(transcript); 46 | } catch (error) { 47 | console.error(error); 48 | res.status(500).json({ error: error.message }); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /dashboard/src/screens/Welcome/Welcome.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Container, Text } from '@comba.se/ui'; 4 | 5 | // Hooks // 6 | import useAuth from 'hooks/useAuth'; 7 | 8 | // Components // 9 | import OrganizationCard from 'components/OrganizationCard'; 10 | 11 | const Root = styled.div` 12 | flex: 1; 13 | justify-content: center; 14 | & > ${Container} { 15 | flex: 1; 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | `; 20 | 21 | const Header = styled.div` 22 | padding: 32px 0px; 23 | justify-content: center; 24 | align-items: center; 25 | text-align: center; 26 | `; 27 | 28 | const renderOrganizations = (org, key) => { 29 | return ( 30 | 36 | ); 37 | }; 38 | 39 | const Welcome = () => { 40 | const [{ organizations }] = useAuth(); 41 | return ( 42 | 43 |
44 | 45 | Combase 46 | 47 | 48 | Choose an Organization 49 | 50 |
51 | 52 | {organizations.map(renderOrganizations)} 53 | 54 |
55 | ); 56 | }; 57 | 58 | export default Welcome; 59 | -------------------------------------------------------------------------------- /api/src/controllers/v1/auth/post.action.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import bcrypt from 'bcrypt'; 3 | 4 | import Agent from 'models/agent'; 5 | 6 | exports.post = async (req, res) => { 7 | try { 8 | // extract json from body 9 | const data = req.body; 10 | 11 | // if the agent does not exist, create a new agent 12 | const agent = await Agent.findOneOrCreate( 13 | { email: data.email.toLowerCase() }, // lowercase email to avoid lookup issues 14 | { 15 | name: { 16 | first: data.name.first, 17 | last: data.name.last, 18 | }, 19 | email: data.email, // email is set to lowercase automatically by mongoose via model 20 | password: data.password, // password is hashed using bcrypt automatically by mongoose plugin 21 | refs: data.refs, 22 | } 23 | ); 24 | 25 | // if the agent does not exist 26 | if (!agent) { 27 | // sanitize / remove password 28 | delete agent.password; 29 | 30 | // return the response 31 | return res.json(agent); 32 | } 33 | 34 | // validate that the provided password matches the hashed password stored in the database 35 | const match = await bcrypt.compare(data.password, agent.password); 36 | 37 | // if the password does not match, throw a 403 forbidden error status code 38 | if (!match) { 39 | return res.sendStatus(403); 40 | } 41 | 42 | // sanitize / remove password 43 | delete agent.password; 44 | 45 | // return the response with agent data, token, and api key 46 | return res.json(agent); 47 | } catch (error) { 48 | console.error(error); 49 | res.status(500).json({ error: error.message }); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /dashboard/src/hocs/withLayout.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ResizeObserver from 'utils/ResizeObserver'; 3 | 4 | const defaultOptions = { 5 | box: 'border-box', 6 | }; 7 | 8 | export default (WrappedComponent, observerOptions = defaultOptions) => { 9 | return class withResize extends Component { 10 | ref = React.createRef(); 11 | 12 | state = { 13 | layout: {}, 14 | }; 15 | 16 | componentDidMount() { 17 | this.observer = new ResizeObserver(this.handleResize); 18 | this.observer.observe(this.ref.current, observerOptions); 19 | } 20 | 21 | componentWillUnmount() { 22 | this.observer.disconnect(); 23 | } 24 | 25 | handleResize = entries => { 26 | const { onResize } = this.props; 27 | 28 | const [entry] = entries; 29 | 30 | const { 31 | inlineSize: width, 32 | blockSize: height, 33 | } = entry.borderBoxSize[0]; // use for backwards compatibility 34 | this.setState({ 35 | layout: { width, height }, 36 | }); 37 | 38 | if (onResize) { 39 | onResize({ width, height }); 40 | } 41 | }; 42 | render() { 43 | const { layout } = this.state; 44 | return ( 45 | 50 | ); 51 | } 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /dashboard/src/contexts/ThemeSwitcher/ThemeSwitcher.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo, useState } from "react"; 2 | import { ThemeProvider } from "styled-components"; 3 | import ThemeSwitcherContext from "./index"; 4 | 5 | import * as themes from "styles/theme"; 6 | 7 | export default ({ children }) => { 8 | const [currentTheme, changeTheme] = useState( 9 | localStorage.getItem("theme") || "light" 10 | ); 11 | 12 | const [overrides, setOverrides] = useState(JSON.parse(localStorage.getItem('themeOverrides')) || {}); 13 | 14 | const toggleTheme = useCallback(() => { 15 | const newTheme = currentTheme === "light" ? "dark" : "light"; 16 | localStorage.setItem("theme", newTheme); 17 | changeTheme(newTheme); 18 | }, [currentTheme]); 19 | 20 | const setTheme = useCallback((themeName) => { 21 | changeTheme(themeName); 22 | }, []); 23 | 24 | const updateOverrides = useCallback(values => { 25 | localStorage.setItem('themeOverrides', JSON.stringify(values)); 26 | console.log('setting overrides', values); 27 | setOverrides(values); 28 | }, []); 29 | 30 | const value = useMemo( 31 | () => ({ 32 | isDarkMode: currentTheme === "dark", 33 | toggleTheme, 34 | updateOverrides, 35 | setTheme, 36 | }), 37 | [toggleTheme, currentTheme, updateOverrides, setTheme] 38 | ); 39 | 40 | const theme = useMemo(() => themes[currentTheme](overrides), [currentTheme, overrides]); 41 | 42 | return ( 43 | 44 | {children} 45 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Agents/views/AgentDetail/widgets/ChatActivityWidget.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withTheme } from 'styled-components'; 3 | import { Sparklines, SparklinesLine } from 'react-sparklines'; 4 | 5 | // Components // 6 | import InfoWidget from 'shared/InfoWidget'; 7 | 8 | function boxMullerRandom() { 9 | let phase = false, 10 | x1, 11 | x2, 12 | w; 13 | 14 | return (function() { 15 | if ((phase = !phase)) { 16 | do { 17 | x1 = 2.0 * Math.random() - 1.0; 18 | x2 = 2.0 * Math.random() - 1.0; 19 | w = x1 * x1 + x2 * x2; 20 | } while (w >= 1.0); 21 | 22 | w = Math.sqrt((-2.0 * Math.log(w)) / w); 23 | return x1 * w; 24 | } else { 25 | return x2 * w; 26 | } 27 | })(); 28 | } 29 | 30 | function randomData(n = 30) { 31 | return Array.apply(0, Array(n)).map(boxMullerRandom); 32 | } 33 | 34 | const sampleData = randomData(30); 35 | 36 | const SparklineWidget = withTheme(({ theme }) => { 37 | return ( 38 | 39 | 46 | 47 | ); 48 | }); 49 | 50 | const ChatActivityWidget = () => { 51 | return ( 52 | 53 | ); 54 | }; 55 | 56 | export default ChatActivityWidget; 57 | -------------------------------------------------------------------------------- /dashboard/src/shared/ListItem.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import styled, { withTheme } from "styled-components"; 4 | import ContentLoader from 'react-content-loader'; 5 | import { Text } from '@comba.se/ui'; 6 | 7 | // Components // 8 | const Root = styled.div` 9 | flex-direction: row; 10 | align-items: center; 11 | padding: 12px 16px; 12 | `; 13 | 14 | const Content = styled.div` 15 | margin-left: ${({ hasIcon }) => (hasIcon ? 16 : 0)}px; 16 | `; 17 | 18 | const ListItem = ({ icon: Icon, theme, title, value }) => { 19 | if (!title || !value) { 20 | return ( 21 | 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | return ( 36 | 37 | {Icon ? : null} 38 | 39 | 40 | {title} 41 | 42 | {value} 43 | 44 | 45 | ); 46 | }; 47 | 48 | ListItem.propTypes = { 49 | icon: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), 50 | title: PropTypes.string, 51 | value: PropTypes.string 52 | }; 53 | 54 | export default withTheme(ListItem); 55 | -------------------------------------------------------------------------------- /dashboard/src/components/Shell/Drawer/Drawer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Route } from "react-router-dom"; 4 | import Animated from "animated/lib/targets/react-dom"; 5 | 6 | // Components // 7 | import Modal from "shared/Modal"; 8 | import DrawerItem from "./DrawerItem"; 9 | 10 | const Root = styled(Animated.div)` 11 | position: fixed; 12 | top: 0; 13 | left: 0; 14 | bottom: 0; 15 | background-color: ${({ theme }) => theme.color.surface}; 16 | width: 280px; 17 | z-index: ${({ theme }) => theme.z.modal}; 18 | `; 19 | 20 | const renderRoutes = (routes, match, onClose) => 21 | routes.map((route, key) => ( 22 | ( 26 | 33 | )} 34 | /> 35 | )); 36 | 37 | const Drawer = ({ anim, match, onClose, open, routes }) => { 38 | const style = { 39 | transform: [ 40 | { 41 | translateX: anim.interpolate({ 42 | inputRange: [0, 1], 43 | outputRange: ["-100%", "-0%"] 44 | }) 45 | } 46 | ] 47 | }; 48 | return ( 49 | 50 | {renderRoutes(routes, match, onClose)} 51 | 52 | ); 53 | }; 54 | 55 | Drawer.defaultProps = { 56 | anim: new Animated.Value(0) 57 | }; 58 | 59 | export default Drawer; 60 | -------------------------------------------------------------------------------- /widget/src/hooks/useChats.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useMemo, useState } from "react"; 2 | import { useChannels } from "stream-chat-hooks"; 3 | import useAuth from "hooks/useAuth"; 4 | import request from "utils/request"; 5 | 6 | export default () => { 7 | const [{ organization, user }] = useAuth(); 8 | const [loading, setLoading] = useState(true); 9 | const [error, setError] = useState(false); 10 | const filter = useMemo(() => ({ organization: organization._id, members: { $in: [user._id] } }), [organization, user]); 11 | const [channels, { loading: channelsLoading }] = useChannels(user._id, filter); 12 | const [chats, setChats] = useState( 13 | JSON.parse(localStorage.getItem("chats")) || [] 14 | ); 15 | 16 | const getChats = useCallback(async () => { 17 | if (channelsLoading) { 18 | return; 19 | } 20 | 21 | if (channels.length) { 22 | try { 23 | setLoading(true); 24 | const data = await request( 25 | `v1/chats?refs.user._id=${user._id}`, 26 | "get", 27 | null, 28 | process.env.REACT_APP_API_KEY 29 | ); 30 | 31 | const chatData = data.map(chat => ({ 32 | ...chat, 33 | channel: channels.find(({ id }) => id === chat._id) 34 | })); 35 | 36 | setChats(chatData); 37 | setLoading(false); 38 | } catch (error) { 39 | setError(true); 40 | setLoading(false); 41 | } 42 | } else { 43 | setLoading(false); 44 | } 45 | }, [user._id, channels, channelsLoading]); 46 | useEffect(() => { 47 | getChats(); 48 | }, [getChats]); 49 | return [chats, { loading, error }]; 50 | }; 51 | -------------------------------------------------------------------------------- /widget/src/screens/Thread/views/MessageThread/MessageThread.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect } from 'react'; 2 | import { LoadingState } from '@comba.se/ui'; 3 | import Chat from '@comba.se/chat'; 4 | 5 | // Hooks // 6 | import useAuth from 'hooks/useAuth'; 7 | 8 | // HOCs // 9 | import withChat from '@comba.se/chat/hocs/withChat'; 10 | 11 | const MessageThread = (props) => { 12 | const { 13 | channel, 14 | history, 15 | isPartnerTyping, 16 | match, 17 | loading, 18 | loadMoreMessages, 19 | messages, 20 | partner, 21 | read 22 | } = props; 23 | const [{ user }] = useAuth(); 24 | const markRead = useCallback(async () => { 25 | if (channel) { 26 | await channel.markRead(); 27 | } 28 | }, [channel]); 29 | 30 | useEffect(() => { 31 | if (match && match.params.channel) { 32 | markRead(); 33 | } 34 | }, [channel, match, markRead]); 35 | 36 | const onSend = useCallback( 37 | messages => { 38 | channel.sendMessage(messages[0]); 39 | }, 40 | [channel] 41 | ); 42 | 43 | if (loading) { 44 | return 45 | } 46 | 47 | return ( 48 | 60 | ); 61 | }; 62 | 63 | export default withChat(MessageThread); -------------------------------------------------------------------------------- /dashboard/src/components/AvailabilityField/AvailabilityDay.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { Text } from '@comba.se/ui'; 5 | 6 | // Components // 7 | const Root = styled.div` 8 | width: 40px; 9 | height: 40px; 10 | border-radius: 50%; 11 | background-color: ${({ enabled, theme }) => enabled ? theme.color.primary : 'transparent'}; 12 | border: 1px solid ${({ enabled, theme }) => enabled ? theme.color.primary : theme.color.border}; 13 | justify-content: center; 14 | align-items: center; 15 | cursor: pointer; 16 | 17 | & > ${Text} { 18 | user-select: none; 19 | text-transform: uppercase; 20 | } 21 | `; 22 | 23 | const AvailabilityDay = ({ enabled, day, days, onChange }) => { 24 | const onClick = useCallback(() => { 25 | onChange({ 26 | target: { 27 | name: 'availability', 28 | value: { 29 | ...days, 30 | [day]: { 31 | ...days[day], 32 | enabled: !enabled 33 | } 34 | } 35 | } 36 | }); 37 | }, [days, day, enabled, onChange]); 38 | return ( 39 | 40 | {day.charAt(0)} 41 | 42 | ); 43 | } 44 | 45 | AvailabilityDay.propTypes = { 46 | enabled: PropTypes.bool, 47 | day: PropTypes.string, 48 | days: PropTypes.object, 49 | onChange: PropTypes.func, 50 | }; 51 | 52 | export default AvailabilityDay; -------------------------------------------------------------------------------- /dashboard/src/hooks/useChats.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useMemo, useState } from "react"; 2 | import { useChannels } from "stream-chat-hooks"; 3 | import useAuth from "hooks/useAuth"; 4 | import request from "utils/request"; 5 | 6 | export default () => { 7 | const [{ organization, user }] = useAuth(); 8 | const [loading, setLoading] = useState(true); 9 | const [error, setError] = useState(false); 10 | const filter = useMemo(() => ({ organization: organization._id, members: { $in: [user._id] } }), [organization, user]); 11 | const [channels, { loading: channelsLoading }] = useChannels(user._id, filter); 12 | const [chats, setChats] = useState( 13 | JSON.parse(localStorage.getItem("chats")) || [] 14 | ); 15 | 16 | const getChats = useCallback(async () => { 17 | if (channelsLoading) { 18 | return; 19 | } 20 | 21 | if (channels.length) { 22 | try { 23 | setLoading(true); 24 | const data = await request( 25 | `v1/chats?refs.agents.assignee.agent._id=${user._id}`, 26 | "get", 27 | null, 28 | user.tokens.api 29 | ); 30 | 31 | const chatData = data.map(chat => ({ 32 | ...chat, 33 | channel: channels.find(({ id }) => id === chat._id) 34 | })); 35 | 36 | setChats(chatData); 37 | setLoading(false); 38 | } catch (error) { 39 | setError(true); 40 | setLoading(false); 41 | } 42 | } else { 43 | setLoading(false); 44 | } 45 | }, [user._id, user.tokens.api, channels, channelsLoading]); 46 | useEffect(() => { 47 | getChats(); 48 | }, [getChats]); 49 | return [chats, { loading, error }]; 50 | }; 51 | -------------------------------------------------------------------------------- /dashboard/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import moment from "moment"; 3 | 4 | // Router // 5 | import { Router, Switch } from "react-router-dom"; 6 | import history from "utils/history"; 7 | 8 | // Styles // 9 | import { ThemeSwitcher } from 'contexts/ThemeSwitcher'; 10 | import GlobalStyles from "styles/global"; 11 | 12 | // Context // 13 | import AuthProvider from "contexts/Auth/AuthProvider"; 14 | import SnackbarProvider from "contexts/Snackbar/SnackbarProvider"; 15 | 16 | // Screens // 17 | import Auth from "screens/Auth"; 18 | import Dashboard from "screens/Dashboard"; 19 | import Welcome from "screens/Welcome"; 20 | 21 | // Components // 22 | import AuthedRoute from "components/AuthedRoute"; 23 | import OrgProtectedRoute from "components/OrgProtectedRoute"; 24 | import UnauthedRoute from "components/UnauthedRoute"; 25 | 26 | moment.updateLocale("en", { 27 | calendar: { 28 | lastDay: "[Yesterday at] h:mma", 29 | sameDay: "h:mma", 30 | nextDay: "[Tomorrow at] h:mma", 31 | lastWeek: "MMMM Do YYYY", 32 | nextWeek: "MMMM Do YYYY", 33 | sameElse: "L" 34 | } 35 | }); 36 | 37 | function App() { 38 | return ( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ); 54 | } 55 | 56 | export default App; 57 | -------------------------------------------------------------------------------- /api/src/controllers/v1/chat/destroy.action.js: -------------------------------------------------------------------------------- 1 | // import Chat from 'models/chat'; 2 | 3 | // exports.destroy = async (req, res) => { 4 | // try { 5 | // const data = { ...req.body, ...req.params }; 6 | // const { serialized } = req; 7 | 8 | // if (serialized.role !== 'admin') { 9 | // return res.status(403).json({ 10 | // status: 'Invalid permissions to view or modify this resource.' 11 | // }); 12 | // } 13 | 14 | // await Chat.updateOne({ _id: data.chat }, { $push: { status: { type: 'Archived', timestamp: Date.now() } } }); 15 | 16 | // res.sendStatus(204); 17 | // } catch (error) { 18 | // console.error(error); 19 | // res.status(500).json({ error: error.message }); 20 | // } 21 | // }; 22 | 23 | 24 | // 25 | // 26 | // Permanently Delete the Chat data from Mongo AND Stream Chat 27 | // 28 | // 29 | 30 | import Chat from 'models/chat'; 31 | import StreamClient from 'utils/stream'; 32 | 33 | exports.destroy = async (req, res) => { 34 | try { 35 | const data = { ...req.body, ...req.params }; 36 | const { serialized } = req; 37 | 38 | // if (serialized.role !== 'admin') { 39 | // return res.status(403).json({ 40 | // status: 'Invalid permissions to view or modify this resource.' 41 | // }); 42 | // } 43 | console.log('chat', data.chat) 44 | const { client } = await StreamClient(); 45 | await Chat.findByIdAndRemove(data.chat); 46 | const channel = client.channel('commerce', data.chat); 47 | await channel.delete(); 48 | // await Chat.updateOne({ _id: data.chat }, { $push: { status: { type: 'Archived', timestamp: Date.now() } } }); 49 | 50 | res.sendStatus(204); 51 | } catch (error) { 52 | console.error(error); 53 | res.status(500).json({ error: error.message }); 54 | } 55 | }; 56 | 57 | -------------------------------------------------------------------------------- /dashboard/src/hooks/useAgents.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | 3 | // Utils // 4 | import request from "utils/request"; 5 | 6 | // Hooks // 7 | import useAuth from "hooks/useAuth"; 8 | 9 | export default () => { 10 | const [loading, setLoading] = useState(false); 11 | const [tabs, setTabs] = useState(["All"]); 12 | const [agents, setAgents] = useState( 13 | JSON.parse(localStorage.getItem("agents")) || [] 14 | ); 15 | const [{ user }] = useAuth(); 16 | 17 | const refetchAgents = useCallback(async () => { 18 | const agents = await request(`v1/agents?refs.organization._id=${user.refs.organization._id}`, "get", null, user.tokens.api); 19 | localStorage.setItem("agents", JSON.stringify(agents)); 20 | setAgents(agents); 21 | }, [user]); 22 | 23 | const getAgents = useCallback(async () => { 24 | try { 25 | setLoading(true); 26 | const agents = await request( 27 | `v1/agents?refs.organization._id=${user.refs.organization._id}`, 28 | "get", 29 | null, 30 | user.tokens.api 31 | ); 32 | setAgents(agents); 33 | localStorage.setItem("agents", JSON.stringify(agents)); 34 | setTabs([ 35 | ...new Set([ 36 | "All", 37 | ...agents 38 | .reduce((acc, { role }) => { 39 | return [...acc, role]; 40 | }, []) 41 | .sort() 42 | ]) 43 | ]); 44 | setLoading(false); 45 | } catch (error) { 46 | // TODO: Error Handling 47 | console.log(error); 48 | } 49 | }, [user.tokens.api, user.refs.organization._id]); 50 | useEffect(() => { 51 | getAgents(); 52 | }, [getAgents]); 53 | 54 | return [agents, tabs, { loading, refetchAgents }]; 55 | }; 56 | -------------------------------------------------------------------------------- /api/src/controllers/v1/auth/login.action.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { StreamChat } from 'stream-chat'; 3 | import bcrypt from 'bcrypt'; 4 | import jwt from 'jsonwebtoken'; 5 | 6 | import Agent from 'models/agent'; 7 | import StreamClient from 'utils/stream'; 8 | 9 | exports.login = async (req, res) => { 10 | try { 11 | // extract json from body 12 | const data = req.body; 13 | 14 | // if the agent does not exist, create a new agent 15 | let agent = await Agent.findOne({ email: data.email }).lean({ 16 | autopopulate: true 17 | }); 18 | 19 | // if the agent does not exist 20 | if (!agent) { 21 | // return the response 22 | return res.sendStatus(404); 23 | } 24 | 25 | // validate that the provided password matches the hashed password stored in the database 26 | const match = await bcrypt.compare(data.password, agent.password); 27 | 28 | // if the password does not match, throw a 403 forbidden error status code 29 | if (!match) { 30 | return res.sendStatus(403); 31 | } 32 | 33 | // sanitize / remove password 34 | delete agent.password; 35 | 36 | const { key, secret } = await StreamClient(); 37 | 38 | const client = new StreamChat(key, secret); 39 | const streamToken = client.createToken(agent._id.toString()); 40 | 41 | // jwt token generation (for api) 42 | const apiToken = jwt.sign( 43 | { 44 | sub: agent._id, 45 | role: agent.role 46 | }, 47 | process.env.AUTH_SECRET 48 | ); 49 | 50 | // return the response with user data, token, and api key 51 | return res.json({ 52 | ...agent, 53 | tokens: { 54 | api: apiToken, 55 | stream: streamToken 56 | } 57 | }); 58 | } catch (error) { 59 | console.error(error); 60 | res.status(500).json({ error: error.message }); 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /dashboard/src/screens/Dashboard/views/Plugins/Plugins.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Route } from 'react-router-dom'; 4 | import { Container } from "@comba.se/ui"; 5 | import { PluginsIcon } from "@comba.se/ui/Icons"; 6 | 7 | // Contexts // 8 | import PluginsContext from 'contexts/Plugins'; 9 | 10 | // Hooks // 11 | import useActivePlugins from 'hooks/useActivePlugins'; 12 | 13 | // Views // 14 | import PluginDetail from './views/PluginDetail'; 15 | 16 | // Components /// 17 | import ScreenRoot from 'shared/ScreenRoot'; 18 | import FullScreenHeader from 'components/FullScreenHeader'; 19 | import PluginsList from 'components/PluginsList'; 20 | 21 | const Root = styled(ScreenRoot)` 22 | flex: 1; 23 | padding-bottom: 40px; 24 | overflow-y: scroll; 25 | @media (min-width: ${({ theme }) => theme.breakpoints.sm}px) { 26 | overflow-y: scroll; 27 | } 28 | `; 29 | 30 | const renderPluginModal = props => ; 31 | 32 | export default ({ match }) => { 33 | const activePlugins = useActivePlugins(); 34 | return ( 35 | 36 | 37 | 43 | 44 | 45 | 46 | 50 | 51 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /dashboard/src/hooks/usePageSheet.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import getNestedProperty from 'utils/getNestedProperty'; 3 | 4 | export default (data, searchKey, tabKey) => { 5 | const [query, setQuery] = useState(''); 6 | const [activeTab, setActiveTab] = useState('All'); 7 | const [results, setResults] = useState(data); 8 | 9 | useEffect(() => { 10 | setResults( 11 | data.filter(item => { 12 | let searchTerm = ''; 13 | if (typeof searchKey !== 'string') { 14 | searchTerm = searchKey 15 | .map(key => getNestedProperty(item, key)) 16 | .join(' '); 17 | } else { 18 | searchTerm = getNestedProperty(item, searchKey); 19 | } 20 | 21 | if (activeTab !== 'All') { 22 | return ( 23 | searchTerm 24 | .toLowerCase() 25 | .includes(query.trim().toLowerCase()) && 26 | getNestedProperty(item, tabKey) === activeTab 27 | ); 28 | } else { 29 | if (typeof searchKey === 'string') { 30 | return searchTerm 31 | .toLowerCase() 32 | .includes(query.trim().toLowerCase()); 33 | } else { 34 | return searchTerm 35 | .toLowerCase() 36 | .includes(query.trim().toLowerCase()); 37 | } 38 | } 39 | }) 40 | ); 41 | }, [data, query, activeTab, searchKey, tabKey]); 42 | 43 | return [results, setQuery, activeTab, setActiveTab]; 44 | }; 45 | --------------------------------------------------------------------------------