├── .DS_Store ├── .coderabbit.yaml ├── .env ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── deploy.yml │ ├── generate_release_notes.yml │ └── node.js.yml ├── CONTRIBUTING.md ├── Dashboard.png ├── LICENSE ├── PULLREQUESTS.md ├── README.md ├── SECURITY.md ├── backend ├── .DS_Store ├── .env ├── .env.test ├── .eslintrc ├── .gitignore ├── .prettierrc ├── .sequelizerc ├── Dockerfile ├── config │ ├── config.js │ └── settings.js ├── eslint.config.mjs ├── index.js ├── migrations │ ├── 0000-1.0-users.js │ ├── 0001-1.0-helperlink.js │ ├── 0003-1.0-link.js │ ├── 0004-1.0-tokens.js │ ├── 0005-1.0-banner.js │ ├── 0006-1.0-hint.js │ ├── 0007-1.0-teams.js │ ├── 0008-1.0-popup.js │ ├── 0009-1.0-guidelog.js │ ├── 0010-1.0-invites.js │ ├── 0011-1.1-helperlink.js │ ├── 0012-1.1-tour.js │ ├── 0013-1.1-tour-popup.js │ └── 0014-1.1-popups-actionUrl-rename.js ├── package-lock.json ├── package.json ├── postman │ ├── HelperLinks.postman_collection.json │ ├── Links.postman_collection.json │ ├── Team.postman_collection.json │ └── Tours.postman_collection.json ├── posttest-script.sh ├── pretest-script.sh ├── public │ ├── scripts │ │ ├── bannerRender.js │ │ └── popupRender.js │ └── snippets │ │ └── popupSnippet.html ├── seeders │ └── seeders.js └── src │ ├── controllers │ ├── auth.controller.js │ ├── banner.controller.js │ ├── guide.controller.js │ ├── guidelog.controller.js │ ├── helperLink.controller.js │ ├── hint.controller.js │ ├── invite.controller.js │ ├── link.controller.js │ ├── onboard │ │ ├── bannerData.controller.js │ │ ├── index.js │ │ ├── popupData.controller.js │ │ └── tourData.controller.js │ ├── onboarding.controller.js │ ├── popup.controller.js │ ├── statistics.controller.js │ ├── team.controller.js │ ├── tour.controller.js │ └── user.controller.js │ ├── middleware │ ├── accessGuard.middleware.js │ ├── auth.middleware.js │ ├── fileSizeValidator.middleware.js │ ├── ipFilter.middleware.js │ ├── jsonError.middleware.js │ ├── onboard.middleware.js │ └── validation.middleware.js │ ├── models │ ├── Banner.js │ ├── GuideLog.js │ ├── HelperLink.js │ ├── Hint.js │ ├── Invite.js │ ├── Link.js │ ├── Popup.js │ ├── Team.js │ ├── Token.js │ ├── Tour.js │ ├── TourPopup.js │ ├── User.js │ └── index.js │ ├── routes │ ├── auth.routes.js │ ├── banner.routes.js │ ├── guide.routes.js │ ├── guidelog.routes.js │ ├── helperLink.routes.js │ ├── hint.routes.js │ ├── link.routes.js │ ├── mocks.routes.js │ ├── onboard.routes.js │ ├── popup.routes.js │ ├── script.routes.js │ ├── statistics.routes.js │ ├── team.routes.js │ ├── tour.routes.js │ └── user.routes.js │ ├── scripts │ └── popup.script.js │ ├── server.js │ ├── service │ ├── banner.service.js │ ├── email.service.js │ ├── guidelog.service.js │ ├── helperLink.service.js │ ├── hint.service.js │ ├── invite.service.js │ ├── link.service.js │ ├── popup.service.js │ ├── statistics.service.js │ ├── team.service.js │ ├── tour.service.js │ └── user.service.js │ ├── templates │ ├── invite.hbs │ ├── resetPassword.hbs │ └── signup.hbs │ ├── test │ ├── e2e │ │ ├── auth.test.mjs │ │ ├── banner.test.mjs │ │ ├── helperLink.test.mjs │ │ ├── hint.test.mjs │ │ ├── index.mjs │ │ ├── link.test.mjs │ │ ├── mocks.test.mjs │ │ ├── popup.test.mjs │ │ ├── statistics.test.mjs │ │ ├── team.test.mjs │ │ ├── tour.test.mjs │ │ └── user.test.mjs │ ├── mocks │ │ ├── banner.mock.js │ │ ├── guidelog.mock.js │ │ ├── helperLink.mock.js │ │ ├── hint.mock.js │ │ ├── popup.mock.js │ │ ├── tour.mock.js │ │ └── user.mock.js │ └── unit │ │ ├── controllers │ │ ├── auth.test.js │ │ ├── banner.test.js │ │ ├── helperLink.test.js │ │ ├── hint.test.js │ │ ├── invite.test.js │ │ ├── link.test.js │ │ ├── onboarding.test.js │ │ ├── popup.test.js │ │ ├── statistics.test.js │ │ ├── team.test.js │ │ ├── tour.test.js │ │ └── user.test.js │ │ └── services │ │ ├── banner.test.js │ │ ├── email.test.js │ │ ├── helperLink.test.js │ │ ├── hint.test.js │ │ ├── invite.test.js │ │ ├── link.test.js │ │ ├── popup.test.js │ │ ├── statistics.test.js │ │ ├── team.test.js │ │ ├── tour.test.js │ │ └── user.test.js │ └── utils │ ├── auth.helper.js │ ├── banner.helper.js │ ├── constants.helper.js │ ├── errors.helper.js │ ├── guide.helper.js │ ├── guidelog.helper.js │ ├── helperLink.helper.js │ ├── hint.helper.js │ ├── jwt.helper.js │ ├── link.helper.js │ ├── popup.helper.js │ ├── team.helper.js │ └── tour.helper.js ├── docker-compose.dev.yml ├── docker-compose.prod.yml ├── extension ├── app.js ├── background.js ├── icon.png ├── icon_128_128.png ├── manifest.json └── trash.png ├── frontend ├── .eslintrc ├── .gitignore ├── .husky │ └── pre-commit ├── .prettierrc ├── .storybook │ ├── main.js │ └── preview.js ├── Dockerfile.development ├── Dockerfile.production ├── README.md ├── default.conf ├── dist │ └── index.html ├── index.html ├── package-lock.json ├── package.json ├── public │ ├── no-background.jpg │ ├── svg │ │ └── favicon.svg │ └── vendetta.png ├── src │ ├── .prettierrc │ ├── App.jsx │ ├── assets │ │ ├── auth-screen-background.svg │ │ ├── icons │ │ │ ├── IconWrapper.jsx │ │ │ ├── google-icon.svg │ │ │ └── utilityIcons.jsx │ │ ├── logo │ │ │ ├── introflow_icon.svg │ │ │ ├── introflow_logo.svg │ │ │ └── introflow_logo_bw.svg │ │ └── theme.jsx │ ├── components │ │ ├── Announcements │ │ │ ├── Announcements.jsx │ │ │ └── Announcements.module.scss │ │ ├── Avatar │ │ │ ├── Avatar.jsx │ │ │ └── AvatarStyles.css │ │ ├── Button │ │ │ ├── Button.jsx │ │ │ ├── Button.stories.js │ │ │ ├── ButtonStyles.css │ │ │ ├── CreateActivityButton │ │ │ │ ├── CreateActivityButton.jsx │ │ │ │ └── CreateActivityButtonStyles.css │ │ │ └── GoogleSignInButton │ │ │ │ ├── GoogleSignInButton.css │ │ │ │ └── GoogleSignInButton.jsx │ │ ├── CheckIcon │ │ │ ├── CheckIcon.jsx │ │ │ └── CheckIcon.stories.js │ │ ├── Checkbox │ │ │ ├── Checkbox.jsx │ │ │ ├── CheckboxHRM.css │ │ │ ├── CheckboxHRM.jsx │ │ │ ├── CheckboxHRM.stories.js │ │ │ └── CheckboxStyles.css │ │ ├── ColorTextField │ │ │ ├── ColorTextField.jsx │ │ │ └── ColorTextField.module.scss │ │ ├── CustomLabelTag │ │ │ ├── CustomLabelTag.jsx │ │ │ └── CustomLabelTagStyles.css │ │ ├── CustomLink │ │ │ ├── CustomLink.jsx │ │ │ └── CustomLinkStyles.css │ │ ├── DataTable │ │ │ ├── DataTable.jsx │ │ │ └── TableStyles.css │ │ ├── DatePicker │ │ │ ├── DatePicker.jsx │ │ │ └── DatePickerStyles.css │ │ ├── DraggableListItem │ │ │ ├── DraggableListItem.jsx │ │ │ └── DraggableListItem.module.scss │ │ ├── DropdownList │ │ │ ├── DropdownList.css │ │ │ └── DropdownList.jsx │ │ ├── DropdownMenu │ │ │ ├── DropdownMenu.css │ │ │ ├── DropdownMenu.jsx │ │ │ ├── DropdownMenu.module.css │ │ │ └── DropdownMenuList │ │ │ │ ├── DropdownMenuList.css │ │ │ │ └── DropdownMenuList.jsx │ │ ├── EditorDesign │ │ │ └── EditorDesign.jsx │ │ ├── Fileupload │ │ │ ├── FileUpload.jsx │ │ │ └── FileUpload.module.scss │ │ ├── Footer.jsx │ │ ├── Header │ │ │ ├── Header.css │ │ │ ├── Header.jsx │ │ │ ├── Logo.jsx │ │ │ └── Title │ │ │ │ ├── Title.jsx │ │ │ │ └── TitleStyles.css │ │ ├── HintPageComponents │ │ │ ├── HintLeftAppearance │ │ │ │ ├── HintLeftAppearance.css │ │ │ │ └── HintLeftAppearance.jsx │ │ │ └── HintLeftContent │ │ │ │ ├── HintLeftContent.css │ │ │ │ └── HintLeftContent.jsx │ │ ├── LeftMenu │ │ │ ├── LeftMenu.jsx │ │ │ └── LeftMenu.module.css │ │ ├── Links │ │ │ ├── ColorInput │ │ │ │ ├── ColorInput.module.scss │ │ │ │ └── index.jsx │ │ │ ├── DraggableHelperLink │ │ │ │ ├── DraggableHelperLink.jsx │ │ │ │ ├── DraggableHelperLink.module.scss │ │ │ │ └── ListItemContainer.jsx │ │ │ ├── Popup │ │ │ │ ├── Popup.jsx │ │ │ │ └── Popup.module.scss │ │ │ └── Settings │ │ │ │ ├── Settings.jsx │ │ │ │ └── Settings.module.scss │ │ ├── LoadingPage │ │ │ ├── Loading.module.css │ │ │ ├── LoadingArea.jsx │ │ │ └── LoadingPage.jsx │ │ ├── Logo │ │ │ ├── Logo.jsx │ │ │ └── LogoStyles.module.css │ │ ├── Pagination │ │ │ └── TablePagination │ │ │ │ ├── PaginationTable.jsx │ │ │ │ └── TablePaginationActions.jsx │ │ ├── ParagraphCSS │ │ │ ├── ParagraphCSS.jsx │ │ │ └── ParagraphCSS.module.scss │ │ ├── PopUpMessages │ │ │ ├── PopUpMessages.jsx │ │ │ └── PopUpStyles.js │ │ ├── Private.jsx │ │ ├── ProgressBar │ │ │ ├── ProgressBar.jsx │ │ │ └── styles.css │ │ ├── RadioButton │ │ │ ├── RadioButton.jsx │ │ │ └── RadioButtonStyles.module.css │ │ ├── RichTextEditor │ │ │ ├── EditorLinkDialog │ │ │ │ └── LinkDialog.jsx │ │ │ ├── RichTextEditor.css │ │ │ ├── RichTextEditor.jsx │ │ │ ├── Tabs │ │ │ │ ├── EditorTabs.css │ │ │ │ └── EditorTabs.jsx │ │ │ └── Toolbar │ │ │ │ ├── EditorToolbar.css │ │ │ │ └── EditorToolbar.jsx │ │ ├── Switch │ │ │ ├── Switch.css │ │ │ └── Switch.jsx │ │ ├── Table │ │ │ ├── Table.jsx │ │ │ └── Table.module.css │ │ ├── TextFieldComponents │ │ │ ├── Chips │ │ │ │ └── ChipAdornment.jsx │ │ │ ├── CustomTextField │ │ │ │ ├── CustomTextField.jsx │ │ │ │ ├── CustomTextFieldStyles.css │ │ │ │ └── ReadMe.md │ │ │ ├── TextFieldComponents.css │ │ │ └── TextFieldComponents.jsx │ │ ├── Toast │ │ │ ├── Toast.jsx │ │ │ ├── Toast.module.scss │ │ │ ├── ToastItem.jsx │ │ │ └── constant.js │ │ ├── ToolTips │ │ │ └── ToolTips.jsx │ │ ├── Tour │ │ │ └── DraggableTourStep │ │ │ │ ├── DraggableTourStep.jsx │ │ │ │ └── DraggableTourStep.module.scss │ │ └── UserProfileSidebar │ │ │ ├── UserProfileSidebar.jsx │ │ │ └── UserProfileSidebar.module.css │ ├── data │ │ ├── createActivityButtonData.js │ │ ├── demoData.js │ │ ├── guideMainPageData.js │ │ ├── mockData.js │ │ └── stepData.js │ ├── index.css │ ├── index.jsx │ ├── products │ │ ├── Banner │ │ │ ├── BannerComponent.jsx │ │ │ └── BannerComponent.module.css │ │ ├── Hint │ │ │ ├── HintComponent.css │ │ │ └── HintComponent.jsx │ │ ├── LinkPreview │ │ │ ├── Preview.module.scss │ │ │ └── index.jsx │ │ └── Popup │ │ │ ├── PopupComponent.jsx │ │ │ └── PopupComponent.module.css │ ├── scenes │ │ ├── banner │ │ │ ├── BannerDefaultPage.jsx │ │ │ ├── BannerPageComponents │ │ │ │ ├── BannerLeftAppearance │ │ │ │ │ ├── BannerLeftAppearance.module.css │ │ │ │ │ ├── BannerLeftApperance.jsx │ │ │ │ │ └── BannerLeftApperance.module.scss │ │ │ │ ├── BannerLeftContent │ │ │ │ │ ├── BannerLeftContent.jsx │ │ │ │ │ └── BannerLeftContent.module.scss │ │ │ │ └── BannerPreview │ │ │ │ │ ├── BannerPreview.jsx │ │ │ │ │ ├── BannerPreview.module.scss │ │ │ │ │ └── BannerSkeleton.jsx │ │ │ └── CreateBannerPage.jsx │ │ ├── dashboard │ │ │ ├── Dashboard.jsx │ │ │ ├── Dashboard.module.scss │ │ │ └── HomePageComponents │ │ │ │ ├── CreateActivityButton │ │ │ │ ├── ActivityButtonStyles.js │ │ │ │ └── CreateActivityButton.jsx │ │ │ │ ├── CreateActivityButtonList │ │ │ │ ├── CreateActivityButtonList.jsx │ │ │ │ └── CreateActivityButtonList.module.scss │ │ │ │ ├── DateDisplay │ │ │ │ ├── DateDisplay.jsx │ │ │ │ └── DateDisplay.module.scss │ │ │ │ ├── Skeletons │ │ │ │ ├── BannerSkeleton.jsx │ │ │ │ ├── BaseSkeleton.jsx │ │ │ │ ├── BaseSkeletonStyles.js │ │ │ │ ├── HintSkeleton.jsx │ │ │ │ ├── Skeleton.module.scss │ │ │ │ └── TourSkeleton.jsx │ │ │ │ ├── StatisticCards │ │ │ │ ├── StatisticCards.jsx │ │ │ │ └── StatisticCards.module.scss │ │ │ │ ├── StatisticCardsList │ │ │ │ ├── StatisticCardsList.jsx │ │ │ │ └── StatisticCardsList.module.scss │ │ │ │ └── UserTitle │ │ │ │ ├── UserTitle.jsx │ │ │ │ └── UserTitle.module.scss │ │ ├── errors │ │ │ ├── 403.jsx │ │ │ ├── 404.jsx │ │ │ ├── Error.jsx │ │ │ ├── Error.module.scss │ │ │ └── constant.js │ │ ├── hints │ │ │ ├── CreateHintPage.jsx │ │ │ └── HintDefaultPage.jsx │ │ ├── home │ │ │ └── Home.jsx │ │ ├── links │ │ │ ├── LinkPage.module.scss │ │ │ ├── LinkPageComponents │ │ │ │ ├── LinkAppearance.jsx │ │ │ │ └── LinkContent.jsx │ │ │ ├── LinksDefaultPage.jsx │ │ │ └── NewLinksPopup.jsx │ │ ├── login │ │ │ ├── CheckYourEmailPage.jsx │ │ │ ├── CreateAccountPage.jsx │ │ │ ├── ForgotPasswordPage.jsx │ │ │ ├── Login.module.css │ │ │ ├── LoginPage.jsx │ │ │ ├── PassswordResetPage.jsx │ │ │ └── SetNewPassword.jsx │ │ ├── popup │ │ │ ├── CreatePopupPage.jsx │ │ │ ├── PopupDefaultPage.jsx │ │ │ └── PopupPageComponents │ │ │ │ ├── PopupAppearance │ │ │ │ ├── PopupAppearance.jsx │ │ │ │ └── PopupAppearance.module.scss │ │ │ │ └── PopupContent │ │ │ │ ├── PopupContent.jsx │ │ │ │ └── PopupContent.module.scss │ │ ├── progressSteps │ │ │ ├── ProgressSteps │ │ │ │ ├── ProgressSteps.jsx │ │ │ │ ├── ProgressSteps.module.scss │ │ │ │ ├── Step.jsx │ │ │ │ ├── StepIcon.jsx │ │ │ │ ├── StepLine.jsx │ │ │ │ └── TeamMemberList │ │ │ │ │ ├── TeamMemberList.css │ │ │ │ │ └── TeamMembersList.jsx │ │ │ ├── ProgressStepsMain.jsx │ │ │ └── ProgressStepsMain.module.scss │ │ ├── settings │ │ │ ├── CodeTab │ │ │ │ ├── CodeTab.jsx │ │ │ │ └── CodeTab.module.css │ │ │ ├── Modals │ │ │ │ ├── ChangeMemberRoleModal │ │ │ │ │ ├── ChangeMemberRoleModal.jsx │ │ │ │ │ └── ChangeMemberRoleModal.module.scss │ │ │ │ ├── DeleteConfirmationModal │ │ │ │ │ └── DeleteConfirmationModal.jsx │ │ │ │ ├── InviteTeamMemberModal │ │ │ │ │ ├── InviteTeamMemberModal.jsx │ │ │ │ │ └── InviteTeamMemberModal.module.scss │ │ │ │ ├── RemoveTeamMemberModal │ │ │ │ │ ├── RemoveTeamMemberModal.jsx │ │ │ │ │ └── RemoveTeamMemberModal.module.scss │ │ │ │ └── UploadImageModal │ │ │ │ │ ├── UploadModal.jsx │ │ │ │ │ └── UploadModal.module.scss │ │ │ ├── PasswordTab │ │ │ │ ├── PasswordTab.jsx │ │ │ │ └── PasswordTab.module.css │ │ │ ├── ProfileTab │ │ │ │ ├── ProfileTab.jsx │ │ │ │ └── ProfileTab.module.css │ │ │ ├── Settings.jsx │ │ │ ├── Settings.module.css │ │ │ └── TeamTab │ │ │ │ ├── TeamTab.jsx │ │ │ │ ├── TeamTab.module.css │ │ │ │ └── TeamTable │ │ │ │ ├── TeamTable.jsx │ │ │ │ └── TeamTable.module.css │ │ ├── statistics │ │ │ ├── UserStatisticsPage.jsx │ │ │ └── UserStatisticsPage.module.css │ │ └── tours │ │ │ ├── CreateTourPage.jsx │ │ │ ├── CreateToursPopup │ │ │ ├── CreateToursPopup.jsx │ │ │ └── CreateToursPopup.module.scss │ │ │ ├── TourDefaultPage.jsx │ │ │ ├── TourPageComponents │ │ │ ├── TourLeftContent │ │ │ │ ├── TourLeftContent.jsx │ │ │ │ └── TourLeftContent.module.scss │ │ │ └── TourleftAppearance │ │ │ │ ├── TourLeftAppearance.jsx │ │ │ │ └── TourLeftAppearance.module.scss │ │ │ └── TourPreview │ │ │ ├── TourPreview.jsx │ │ │ └── TourPreview.module.scss │ ├── services │ │ ├── apiClient.js │ │ ├── authProvider.jsx │ │ ├── bannerServices.js │ │ ├── guidelogService.js │ │ ├── helperLinkService.js │ │ ├── hintServices.js │ │ ├── inviteService.js │ │ ├── linkService.js │ │ ├── linksProvider.jsx │ │ ├── loginServices.js │ │ ├── popupServices.js │ │ ├── settingServices.js │ │ ├── statisticsService.js │ │ ├── teamServices.js │ │ └── tourServices.js │ ├── styles │ │ ├── globals.scss │ │ └── variables.css │ ├── templates │ │ ├── DefaultPageTemplate │ │ │ ├── DefaultPageTemplate.css │ │ │ ├── DefaultPageTemplate.jsx │ │ │ └── README.md │ │ ├── GuideMainPageTemplate │ │ │ ├── GuideMainPageComponents │ │ │ │ ├── ConfirmationPopup │ │ │ │ │ └── ConfirmationPopup.jsx │ │ │ │ ├── ContentArea │ │ │ │ │ └── ContentArea.jsx │ │ │ │ ├── ContentHeader │ │ │ │ │ └── ContentHeader.jsx │ │ │ │ ├── InfoTooltip │ │ │ │ │ └── InfoTooltip.jsx │ │ │ │ ├── List │ │ │ │ │ ├── List.jsx │ │ │ │ │ └── ListItem │ │ │ │ │ │ ├── ListItem.css │ │ │ │ │ │ └── ListItem.jsx │ │ │ │ └── TourDescriptionText │ │ │ │ │ └── TourDescriptionText.jsx │ │ │ ├── GuideMainPageTemplate.css │ │ │ └── GuideMainPageTemplate.jsx │ │ ├── GuideTemplate │ │ │ ├── GuideTemplate.jsx │ │ │ ├── GuideTemplate.module.scss │ │ │ ├── GuideTemplateContext.jsx │ │ │ └── Readme.md │ │ └── HomePageTemplate │ │ │ ├── HomePageTemplate.css │ │ │ └── HomePageTemplate.jsx │ ├── tests │ │ ├── components │ │ │ ├── Button │ │ │ │ └── Button.test.jsx │ │ │ ├── CustomLabelTag │ │ │ │ └── CustomLabelTag.test.jsx │ │ │ ├── Error │ │ │ │ └── Error.test.jsx │ │ │ ├── Toast │ │ │ │ ├── Toast.test.jsx │ │ │ │ └── ToastItem.test.jsx │ │ │ ├── customLinkComponent │ │ │ │ └── CustomLink.test.jsx │ │ │ └── textFieldComponent │ │ │ │ └── CustomTextField.test.jsx │ │ └── scenes │ │ │ ├── links │ │ │ ├── NewLinksPopup.test.jsx │ │ │ └── __snapshots__ │ │ │ │ └── NewLinksPopup.test.jsx.snap │ │ │ ├── login │ │ │ ├── CreateAccountPage.test.jsx │ │ │ ├── ForgotPasswordPage.test.jsx │ │ │ ├── LoginPage.test.jsx │ │ │ └── SetNewPassword.test.jsx │ │ │ └── popup │ │ │ └── CreatePopupPage.test.jsx │ └── utils │ │ ├── bannerHelper.js │ │ ├── constants.js │ │ ├── generalHelper.js │ │ ├── guideHelper.js │ │ ├── hintHelper.js │ │ ├── linkHelper.js │ │ ├── loginHelper.js │ │ ├── popupHelper.js │ │ ├── settingsHelper.js │ │ ├── toastEmitter.js │ │ └── tourHelper.js ├── styles.css └── vite.config.js ├── generate_release_notes.py ├── jsAgent ├── banner.js ├── hint.js ├── links.js ├── main.js ├── popup.js └── tour.js ├── package-lock.json └── package.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluewave-labs/guidefox/417a99627d91bc81ebe7ebe1f4f3cea068d44f7b/.DS_Store -------------------------------------------------------------------------------- /.coderabbit.yaml: -------------------------------------------------------------------------------- 1 | language: "en-CA" 2 | tone_instructions: "His palms are sweaty, knees weak, arms are heavy There’s vomit on his sweater already, mom’s spaghetti" 3 | early_access: false 4 | reviews: 5 | profile: "chill" 6 | request_changes_workflow: false 7 | high_level_summary: false 8 | poem: false 9 | review_status: false 10 | 11 | auto_review: 12 | enabled: true 13 | base_branches: [.*] 14 | drafts: false 15 | chat: 16 | auto_reply: false 17 | 18 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Used in docker-compose.yml db 2 | POSTGRES_USER=user123 3 | POSTGRES_PASSWORD=password123 4 | POSTGRES_DB=onboarding_db 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 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 | - Browser [e.g. Chrome, Safari] 28 | - Version [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Describe your changes 2 | 3 | Briefly describe the changes you made and their purpose. 4 | 5 | ## Issue number 6 | 7 | Mention the issue number(s) this PR addresses (e.g., #123). 8 | 9 | ## Please ensure all items are checked off before requesting a review: 10 | 11 | - [ ] I deployed the code locally. 12 | - [ ] I have performed a self-review of my code. 13 | - [ ] I have included the issue # in the PR. 14 | - [ ] I have labelled the PR correctly. 15 | - [ ] The issue I am working on is assigned to me. 16 | - [ ] I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application). 17 | - [ ] I made sure font sizes, color choices etc are all referenced from the theme. 18 | - [ ] My PR is granular and targeted to one specific feature. 19 | - [ ] I took a screenshot or a video and attached to this PR if there is a UI change. 20 | -------------------------------------------------------------------------------- /.github/workflows/generate_release_notes.yml: -------------------------------------------------------------------------------- 1 | name: Generate Release Notes 2 | 3 | on: 4 | push: 5 | branches: 6 | - staging 7 | 8 | jobs: 9 | generate_release_notes: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: '3.x' 19 | 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install requests 24 | 25 | - name: Generate release notes 26 | env: 27 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | run: python generate_release_notes.py 29 | 30 | - name: Commit and push release notes 31 | run: | 32 | git config --local user.email "action@github.com" 33 | git config --local user.name "GitHub Action" 34 | git add release_notes.md 35 | git commit -m "Add release notes for the latest deployment" 36 | git push 37 | -------------------------------------------------------------------------------- /Dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluewave-labs/guidefox/417a99627d91bc81ebe7ebe1f4f3cea068d44f7b/Dashboard.png -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you find a vulnerability, send it to hello@bluewavelabs.ca 4 | 5 | Please do not create an issue for security vulnerabilities. 6 | -------------------------------------------------------------------------------- /backend/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluewave-labs/guidefox/417a99627d91bc81ebe7ebe1f4f3cea068d44f7b/backend/.DS_Store -------------------------------------------------------------------------------- /backend/.env: -------------------------------------------------------------------------------- 1 | # For development environment, the project is ready to run after cloning and installing the dependencies. 2 | # Development database environment 3 | DB_USERNAME=user123 4 | DB_PASSWORD=password123 5 | DB_NAME=onboarding_db 6 | DB_HOST=db 7 | DB_PORT=5432 8 | 9 | EMAIL=bluewaveguidefox@gmail.com 10 | EMAIL_PASSWORD=passwor 11 | EMAIL_HOST=smtp.gmail.com 12 | EMAIL_PORT=465 13 | APP_PASSWORD=ukzwakckupguegiw 14 | EMAIL_ENABLE=true 15 | 16 | # Enable IP check for the API 17 | ENABLE_IP_CHECK=false 18 | # Allowed IP range for the API "baseIp/rangeStart-rangeEnd" (e.g. 192.168.1/1-255) separated by comma 19 | ALLOWED_IP_RANGE=11.22.33/10-200, 192.168.65/1-255 20 | # Allowed IP addresses for the API separated by comma 21 | ALLOWED_IPS=127.0.0.1, 11.22.33.44, 11.22.33.45, 11.22.33.46, 192.168.65.1 22 | 23 | # FRONTEND_URL=https://onboarding-demo.bluewavelabs.ca/ 24 | FRONTEND_URL=http://localhost:4173/ 25 | 26 | # JWT secret for running npm run dev in backend folder locally 27 | JWT_SECRET="NKrbO2lpCsOpVAlqAPsjZ0tZXzIoKru7gAmYZ7XlHn0=qqwqeq" 28 | NODE_ENV=development 29 | -------------------------------------------------------------------------------- /backend/.env.test: -------------------------------------------------------------------------------- 1 | 2 | POSTGRES_USER=user123 3 | POSTGRES_PASSWORD=password123 4 | POSTGRES_DB=onboarding_db_test 5 | 6 | TEST_DB_USERNAME=user123 7 | TEST_DB_PASSWORD=password123 8 | TEST_DB_NAME=onboarding_db_test 9 | TEST_DB_HOST=localhost 10 | TEST_DB_PORT=5432 11 | 12 | JWT_SECRET="NKrbO2lpCsOpVAlqAPsjZ0tZXzIoKru7gAmYZ7XlHn0=qqwqeq" 13 | -------------------------------------------------------------------------------- /backend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-unused-vars": "warn" 4 | } 5 | } -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | logs/ 4 | dist/ 5 | .nyc_output -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "arrowParens": "always", 8 | "singleAttributePerLine": false, 9 | "printWidth": 120 10 | } 11 | -------------------------------------------------------------------------------- /backend/.sequelizerc: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | 'config': path.resolve('config', 'config.js'), 5 | 'models-path': path.resolve('src', 'models'), 6 | 'migrations-path': path.resolve('migrations'), 7 | 'seeders-path': path.resolve('seeders') 8 | }; 9 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22-alpine3.21 2 | 3 | RUN apk update && apk add bash && rm -rf /var/cache/apk/* 4 | 5 | WORKDIR /app 6 | 7 | COPY package.json ./ 8 | 9 | RUN npm install 10 | 11 | COPY . . 12 | 13 | EXPOSE 3000 14 | 15 | # CMD if [ "$$NODE_ENV" = "production" ] ; then npm run prod ; elif [ "$$NODE_ENV" = "staging" ] ; then npm run staging ; else npm run dev ; fi 16 | -------------------------------------------------------------------------------- /backend/config/config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const envSuffix = process.env.NODE_ENV && process.env.NODE_ENV == 'test' ? `.${process.env.NODE_ENV}` : ''; 3 | const env = `.env${envSuffix}`; 4 | 5 | const dotenv = require('dotenv'); 6 | const result = dotenv.config({ path: `./${env}` }); 7 | 8 | if (result.error) { 9 | console.error(`Failed to load environment file: ${env}`); 10 | process.exit(1); 11 | } 12 | 13 | const { 14 | DB_HOST, 15 | DB_NAME, 16 | DB_PASSWORD, 17 | DB_PORT, 18 | DB_USERNAME, 19 | TEST_DB_HOST, 20 | TEST_DB_NAME, 21 | TEST_DB_PASSWORD, 22 | TEST_DB_PORT, 23 | TEST_DB_USERNAME, 24 | } = process.env; 25 | 26 | module.exports = { 27 | defaultTeamName: 'My Organisation', 28 | development: { 29 | username: DB_USERNAME, 30 | password: DB_PASSWORD, 31 | database: DB_NAME, 32 | host: DB_HOST, 33 | dialect: 'postgres', 34 | port: DB_PORT, 35 | logging: false, 36 | }, 37 | test: { 38 | username: TEST_DB_USERNAME, 39 | password: TEST_DB_PASSWORD, 40 | database: TEST_DB_NAME, 41 | host: TEST_DB_HOST, 42 | dialect: 'postgres', 43 | port: TEST_DB_PORT, 44 | logging: false, 45 | }, 46 | production: { 47 | username: DB_USERNAME, 48 | password: DB_PASSWORD, 49 | database: DB_NAME, 50 | host: DB_HOST, 51 | dialect: 'postgres', 52 | port: DB_PORT, 53 | logging: false, 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /backend/config/settings.js: -------------------------------------------------------------------------------- 1 | const constants = require('../src/utils/constants.helper'); 2 | const userRole = constants.ROLE; 3 | 4 | module.exports = { 5 | user: { 6 | role: { 7 | admin: userRole.ADMIN, 8 | member: userRole.MEMBER, 9 | }, 10 | roleEnum: [userRole.ADMIN, userRole.MEMBER], 11 | roleName: { 12 | [userRole.ADMIN]: 'admin', 13 | [userRole.MEMBER]: 'member', 14 | }, 15 | }, 16 | team: { 17 | permissions: { 18 | invite: [userRole.ADMIN], 19 | removeUser: [userRole.ADMIN], 20 | update: [userRole.ADMIN], 21 | changeRole: [userRole.ADMIN], 22 | setOrg: [userRole.ADMIN], 23 | serverUrl: [userRole.ADMIN], 24 | popups: [userRole.ADMIN], 25 | hints: [userRole.ADMIN], 26 | banners: [userRole.ADMIN], 27 | links: [userRole.ADMIN], 28 | tours: [userRole.ADMIN], 29 | helpers: [userRole.ADMIN], 30 | }, 31 | }, 32 | tour: { 33 | size: ['small', 'medium', 'large'], 34 | }, 35 | hint: { 36 | action: ['no action', 'open url', 'open url in a new tab'], 37 | repetition: ['show only once', 'show every visit'], 38 | tooltipPlacement: ['top', 'right', 'bottom', 'left'], 39 | }, 40 | popup: { 41 | action: ['no action', 'open url', 'open url in a new tab'], 42 | repetition: ['show only once', 'show every visit'], 43 | size: ['small', 'medium', 'large'], 44 | }, 45 | banner: { 46 | repetition: ['show only once', 'show every visit'], 47 | position: ['top', 'bottom'], 48 | action: ['no action', 'open url', 'open url in a new tab'], 49 | }, 50 | token: { 51 | type: ['auth', 'reset'], 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /backend/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import pluginJs from '@eslint/js'; 2 | import globals from 'globals'; 3 | 4 | /** @type {import('eslint').Linter.Config[]} */ 5 | export default [ 6 | { 7 | files: ['**/*.js'], 8 | languageOptions: { sourceType: 'commonjs' }, 9 | rules: { 'no-unused-vars': ['warn', { destructuredArrayIgnorePattern: '^_', ignoreRestSiblings: true }] }, 10 | }, 11 | { languageOptions: { globals: globals.node } }, 12 | pluginJs.configs.recommended, 13 | ]; 14 | -------------------------------------------------------------------------------- /backend/index.js: -------------------------------------------------------------------------------- 1 | const app = require("./src/server"); 2 | 3 | const PORT = process.env.PORT || 3000; 4 | app.listen(PORT, () => { 5 | console.log(`Server is running on port ${PORT}`); 6 | }); 7 | -------------------------------------------------------------------------------- /backend/migrations/0014-1.1-popups-actionUrl-rename.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.renameColumn('popups', 'actionUrl', 'actionButtonUrl'); 6 | }, 7 | 8 | down: async (queryInterface, Sequelize) => { 9 | await queryInterface.renameColumn('popups', 'actionButtonUrl', 'actionUrl'); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "onboarding", 3 | "version": "1.0.0", 4 | "description": "Onboarding app for Bluewave", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "pretest": "NODE_ENV=test bash pretest-script.sh", 9 | "posttest": "bash posttest-script.sh", 10 | "test": "NODE_ENV=test nyc mocha --extension js,mjs 'src/test/**/*.test.*'", 11 | "test:e2e": "npm run pretest && NODE_ENV=test mocha 'src/test/e2e/**/*.test.mjs'", 12 | "test:unit": "NODE_ENV=test mocha 'src/test/unit/**/*.test.js' --watch", 13 | "dev": "nodemon --legacy-watch index.js", 14 | "prod": "node index.js", 15 | "build": "echo 'No build script defined'" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC", 20 | "dependencies": { 21 | "bcryptjs": "^2.4.3", 22 | "compression": "^1.7.5", 23 | "cors": "^2.8.5", 24 | "dotenv": "^16.4.5", 25 | "express": "^4.19.2", 26 | "express-validator": "^7.1.0", 27 | "handlebars": "^4.7.8", 28 | "helmet": "^7.1.0", 29 | "jsonwebtoken": "^9.0.2", 30 | "nodemailer": "^6.9.15", 31 | "pg": "^8.13.1", 32 | "sequelize": "^6.37.3" 33 | }, 34 | "devDependencies": { 35 | "@eslint/js": "^9.17.0", 36 | "chai": "^4.5.0", 37 | "chai-http": "^5.1.1", 38 | "eslint": "^9.17.0", 39 | "globals": "^15.14.0", 40 | "mocha": "^10.8.2", 41 | "nodemon": "^3.1.0", 42 | "nyc": "^17.1.0", 43 | "sequelize-cli": "^6.6.2", 44 | "sinon": "^19.0.2", 45 | "wait-on": "^8.0.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /backend/posttest-script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # stop container, if it exists 4 | 5 | if [[ "$(docker ps -a -q -f name=test-postgres)" ]]; then 6 | docker stop test-postgres 7 | fi 8 | 9 | # remove container 10 | if [[ "$(docker ps -a -q -f name=test-postgres)" ]]; then 11 | docker rm -f test-postgres 12 | fi 13 | 14 | # reset NODE_ENV to default 15 | if [[ "$OSTYPE" == "msys" ]]; then 16 | if [ -n "$PSModulePath" ]; then 17 | # PowerShell 18 | echo '$env:NODE_ENV="development"' | powershell -Command - 19 | else 20 | # CMD 21 | cmd.exe /C "set NODE_ENV=development" 22 | fi 23 | else 24 | export NODE_ENV=development 25 | fi -------------------------------------------------------------------------------- /backend/pretest-script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # remove test-postgres container if it exists 4 | if [[ "$(docker ps -a -q -f name=test-postgres)" ]]; then 5 | docker rm -f test-postgres 6 | fi 7 | 8 | # run test-postgres container 9 | 10 | docker run --name test-postgres --env-file ./.env.test -p 5432:5432 -d postgres 11 | 12 | # wait for postgres to start 13 | echo "Waiting for PostgreSQL to be ready..." 14 | for i in {1..10}; do 15 | if docker exec test-postgres pg_isready -U user123 >/dev/null 2>&1; then 16 | break 17 | else 18 | echo "PostgreSQL is not ready. Retrying ($i/10)..." 19 | sleep 2 20 | fi 21 | done 22 | 23 | # create test database if it doesn't exist 24 | exists=$(docker exec test-postgres psql -U user123 -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname='onboarding_db_test'") 25 | if [[ -z "$exists" || "$exists" != *"1"* ]]; then 26 | npx sequelize-cli db:create --env test 27 | fi 28 | 29 | # run migrations 30 | npx sequelize-cli db:migrate --env test 31 | -------------------------------------------------------------------------------- /backend/public/scripts/bannerRender.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluewave-labs/guidefox/417a99627d91bc81ebe7ebe1f4f3cea068d44f7b/backend/public/scripts/bannerRender.js -------------------------------------------------------------------------------- /backend/public/snippets/popupSnippet.html: -------------------------------------------------------------------------------- 1 | 2 | 30 | -------------------------------------------------------------------------------- /backend/src/controllers/invite.controller.js: -------------------------------------------------------------------------------- 1 | const InviteService = require("../service/invite.service"); 2 | const { internalServerError } = require("../utils/errors.helper"); 3 | 4 | const inviteService = new InviteService(); 5 | 6 | const sendTeamInvite = async (req, res) => { 7 | const userId = req.user.id; 8 | const { invitedEmail, role } = req.body; 9 | try { 10 | await inviteService.sendInvite(userId, invitedEmail, role); 11 | return res.status(200).json({ 12 | status: 200, 13 | message: "Invite sent successfully", 14 | data: null, 15 | }); 16 | } catch (err) { 17 | const { statusCode, payload } = internalServerError( 18 | "SEND_INVITE_ERROR", 19 | err.message 20 | ); 21 | res.status(statusCode).json(payload); 22 | } 23 | }; 24 | 25 | const getAllInvites = async (req, res) => { 26 | try { 27 | const invites = await inviteService.getAllInvites(); 28 | return res.status(200).json({ 29 | invites, 30 | success: true, 31 | message: "Invites Retrieved Successfully", 32 | }); 33 | } catch (error) { 34 | return res.status(500).json({ 35 | success: false, 36 | message: error.message, 37 | }); 38 | } 39 | }; 40 | 41 | module.exports = { sendTeamInvite, getAllInvites, inviteService }; 42 | -------------------------------------------------------------------------------- /backend/src/controllers/onboard/bannerData.controller.js: -------------------------------------------------------------------------------- 1 | const bannerService = require("../service/banner.service.js"); 2 | const getBannerData = async (req, res) => { 3 | try { 4 | const { userId } = req.body; 5 | const bannerData = await bannerService.getBannerData(userId); 6 | res.status(200).json(bannerData); 7 | } catch (error) { 8 | res.status(500).json({ error: error.message }); 9 | } 10 | }; 11 | 12 | module.exports = { getBannerData }; 13 | -------------------------------------------------------------------------------- /backend/src/controllers/onboard/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // bannerData: require('./bannerData.controller'), 3 | popupData: require('./popupData.controller'), 4 | // tourData: require('./tourData.controller'), 5 | // hintData: require('./hintData.controller') 6 | }; 7 | -------------------------------------------------------------------------------- /backend/src/controllers/onboard/popupData.controller.js: -------------------------------------------------------------------------------- 1 | const popupService = require('../../service/popup.service'); 2 | 3 | const getPopupData = async (req, res) => { 4 | try { 5 | const { userId } = req.body; 6 | const popupData = await popupService.getPopups(userId); 7 | res.status(200).json(popupData); 8 | } catch (error) { 9 | res.status(500).json({ error: error.message }); 10 | } 11 | }; 12 | 13 | module.exports = { getPopupData }; 14 | -------------------------------------------------------------------------------- /backend/src/controllers/onboard/tourData.controller.js: -------------------------------------------------------------------------------- 1 | const tourService = require('../service/tour.service'); 2 | 3 | const getTourData = async (req, res) => { 4 | try { 5 | const { userId } = req.body; 6 | const tourData = await tourService.getTourData(userId); 7 | res.status(200).json(tourData); 8 | } catch (error) { 9 | res.status(500).json({ error: error.message }); 10 | } 11 | }; 12 | 13 | module.exports = { getTourData }; 14 | -------------------------------------------------------------------------------- /backend/src/controllers/statistics.controller.js: -------------------------------------------------------------------------------- 1 | const statisticsService = require("../service/statistics.service"); 2 | const { internalServerError } = require("../utils/errors.helper"); 3 | 4 | class StatisticsController { 5 | async getStatistics(req, res) { 6 | try { 7 | const statistics = await statisticsService.generateStatistics(); 8 | res.status(200).json(statistics); 9 | } catch (e) { 10 | console.log(e) 11 | const { statusCode, payload } = internalServerError( 12 | "GET_STATISTICS_ERROR", 13 | e.message 14 | ); 15 | res.status(statusCode).json(payload); 16 | } 17 | } 18 | } 19 | 20 | module.exports = new StatisticsController(); 21 | -------------------------------------------------------------------------------- /backend/src/middleware/accessGuard.middleware.js: -------------------------------------------------------------------------------- 1 | const accessGuard = (permissions) => { 2 | return (req, res, next) => { 3 | if(!req.user || !req.user.role || !permissions.includes(req.user.role)) { 4 | return res.status(403).json({ error: "User does not have required access level"}); 5 | } 6 | next(); 7 | } 8 | }; 9 | 10 | module.exports = accessGuard; 11 | -------------------------------------------------------------------------------- /backend/src/middleware/auth.middleware.js: -------------------------------------------------------------------------------- 1 | const { verifyToken } = require("../utils/jwt.helper"); 2 | const db = require("../models"); 3 | const Token = db.Token; 4 | const User = db.User; 5 | 6 | const authenticateJWT = async (req, res, next) => { 7 | const token = req.headers.authorization && req.headers.authorization.split(" ")[1]; 8 | if (!token) return res.status(401).json({ error: "No token provided" }); 9 | 10 | try { 11 | const decoded = verifyToken(token); 12 | if (!decoded) return res.status(401).json({ error: "Invalid token" }); 13 | 14 | const dbToken = await Token.findOne({ where: { token, userId: decoded.id, type: 'auth' } }); 15 | if (!dbToken) return res.status(401).json({ error: "Invalid token" }); 16 | 17 | const createdAt = new Date(dbToken.createdAt); 18 | const expiresAt = new Date(createdAt.getTime() + parseInt(process.env.JWT_EXPIRATION_TIME, 10)); 19 | if (new Date() > expiresAt) { 20 | await dbToken.destroy(); 21 | return res.status(401).json({ error: "Token has expired" }); 22 | } 23 | const user = await User.findOne({ where: { id: decoded.id } }); 24 | if(!user) { 25 | return res.status(404).json("User not found"); 26 | } 27 | req.user = { 28 | id: user.id, 29 | email: user.email, 30 | role: user.role 31 | }; 32 | next(); 33 | } catch (error) { 34 | console.error("Error authenticating token:", error); 35 | return res.status(500).json({ error: "Internal Server Error" }); 36 | } 37 | }; 38 | 39 | module.exports = authenticateJWT; 40 | -------------------------------------------------------------------------------- /backend/src/middleware/fileSizeValidator.middleware.js: -------------------------------------------------------------------------------- 1 | const { MAX_FILE_SIZE } = require('../utils/constants.helper'); 2 | 3 | const fileSizeValidator = (req, res, next) => { 4 | if(req.method !== 'POST' && req.method !== 'PUT') { 5 | return next(); 6 | } 7 | const contentLength = Number(req.headers['content-length']); 8 | 9 | if (isNaN(contentLength)) { 10 | return res.status(400).json({ 11 | error: 'Missing or invalid Content-Length header' 12 | }); 13 | } 14 | 15 | if (contentLength > MAX_FILE_SIZE) { 16 | return res.status(413).json({ 17 | error: `File size exceeds the limit of ${MAX_FILE_SIZE} bytes`, 18 | receivedSize: contentLength 19 | }); 20 | } 21 | 22 | next(); 23 | }; 24 | 25 | module.exports = fileSizeValidator; 26 | -------------------------------------------------------------------------------- /backend/src/middleware/jsonError.middleware.js: -------------------------------------------------------------------------------- 1 | function jsonErrorMiddleware(err, req, res, next) { 2 | if (err instanceof SyntaxError && err.status === 400 && 'body' in err) { 3 | console.error('JSON Syntax Error:', err); 4 | return res.status(400).json({ error: 'Invalid JSON format' }); 5 | } 6 | next(); 7 | } 8 | 9 | module.exports = jsonErrorMiddleware; 10 | -------------------------------------------------------------------------------- /backend/src/middleware/onboard.middleware.js: -------------------------------------------------------------------------------- 1 | const db = require('../models'); 2 | const { v4: uuidv4 } = require('uuid'); 3 | 4 | // Middleware to validate API ID 5 | const validateApiId = async (req, res, next) => { 6 | const { apiId } = req.query; // Assume API ID is sent in query params 7 | if (!apiId) { 8 | return res.status(400).json({ error: "API ID is required." }); 9 | } 10 | 11 | try { 12 | const user = await db.User.findOne({ where: { apiId } }); // API ID must be in User model 13 | if (!user) { 14 | return res.status(403).json({ error: "Invalid API ID." }); 15 | } 16 | 17 | req.user = user; // Attach the user to the request for future use 18 | next(); 19 | } catch (error) { 20 | return res.status(500).json({ error: "API ID validation failed." }); 21 | } 22 | }; 23 | 24 | // Middleware to generate client ID for each session 25 | const generateClientId = (req, res, next) => { 26 | if (!req.session.clientId) { 27 | req.session.clientId = uuidv4(); // Generate new client ID and store in session 28 | } 29 | next(); 30 | }; 31 | 32 | module.exports = { 33 | validateApiId, 34 | generateClientId 35 | }; 36 | -------------------------------------------------------------------------------- /backend/src/middleware/validation.middleware.js: -------------------------------------------------------------------------------- 1 | const { validationResult } = require("express-validator"); 2 | 3 | const handleValidationErrors = (req, res, next) => { 4 | const errors = validationResult(req); 5 | if (!errors.isEmpty()) { 6 | return res 7 | .status(400) 8 | .json({ errors: errors.array().map((err) => err.msg) }); 9 | } 10 | next(); 11 | }; 12 | 13 | module.exports={handleValidationErrors} -------------------------------------------------------------------------------- /backend/src/models/Invite.js: -------------------------------------------------------------------------------- 1 | const settings = require("../../config/settings"); 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | const Invite = sequelize.define( 5 | "Invite", 6 | { 7 | id: { 8 | type: DataTypes.INTEGER, 9 | primaryKey: true, 10 | autoIncrement: true, 11 | }, 12 | invitedBy: { 13 | type: DataTypes.INTEGER, 14 | allowNull: false, 15 | references: { 16 | model: "users", 17 | key: "id", 18 | }, 19 | }, 20 | invitedEmail: { 21 | type: DataTypes.STRING(100), 22 | unique: true, 23 | allowNull: false, 24 | }, 25 | role: { 26 | type: DataTypes.ENUM(settings.user.roleEnum), 27 | allowNull: false, 28 | }, 29 | createdAt: { 30 | type: DataTypes.DATE, 31 | allowNull: false, 32 | defaultValue: DataTypes.NOW, 33 | }, 34 | }, 35 | { 36 | tableName: "invites", 37 | timestamps: false, 38 | }, 39 | ); 40 | 41 | return Invite; 42 | }; 43 | -------------------------------------------------------------------------------- /backend/src/models/Link.js: -------------------------------------------------------------------------------- 1 | const { URL_REGEX } = require("../utils/link.helper"); 2 | 3 | /** 4 | * 5 | * @param {import('sequelize').Sequelize} sequelize 6 | * @param {import('sequelize').DataTypes} DataTypes 7 | * @returns 8 | */ 9 | module.exports = (sequelize, DataTypes) => { 10 | const Link = sequelize.define( 11 | "Link", 12 | { 13 | id: { 14 | type: DataTypes.INTEGER, 15 | primaryKey: true, 16 | autoIncrement: true, 17 | }, 18 | title: { 19 | type: DataTypes.STRING(255), 20 | allowNull: false, 21 | validate: { 22 | not: URL_REGEX, 23 | }, 24 | }, 25 | url: { 26 | type: DataTypes.STRING(255), 27 | allowNull: false, 28 | validate: { 29 | customValidation(value) { 30 | return URL_REGEX.test(value) 31 | } 32 | }, 33 | }, 34 | order: { 35 | type: DataTypes.INTEGER, 36 | allowNull: false, 37 | defaultValue: 1 38 | }, 39 | target: { 40 | type: DataTypes.BOOLEAN, 41 | defaultValue: true, 42 | }, 43 | helperId: { 44 | type: DataTypes.INTEGER, 45 | allowNull: false, 46 | references: { 47 | model: "helper_link", 48 | key: "id", 49 | }, 50 | }, 51 | }, 52 | { 53 | tableName: "link", 54 | timestamps: false, 55 | } 56 | ); 57 | 58 | Link.associate = (models) => { 59 | Link.belongsTo(models.HelperLink, { 60 | foreignKey: "helperId", 61 | as: "helper", 62 | onDelete: "CASCADE", 63 | }); 64 | }; 65 | 66 | return Link; 67 | }; 68 | -------------------------------------------------------------------------------- /backend/src/models/Team.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Team = sequelize.define( 3 | "Team", 4 | { 5 | id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true, 9 | }, 10 | name: { 11 | type: DataTypes.STRING(63), 12 | allowNull: false, 13 | }, 14 | serverUrl: { 15 | type: DataTypes.STRING(255), 16 | allowNull: true, 17 | defaultValue: '' 18 | }, 19 | agentUrl: { 20 | type: DataTypes.STRING(255), 21 | allowNull: true, 22 | defaultValue: '' 23 | }, 24 | createdAt: { 25 | type: DataTypes.DATE, 26 | allowNull: false, 27 | defaultValue: DataTypes.NOW, 28 | }, 29 | }, 30 | { 31 | tableName: "teams", 32 | timestamps: false, 33 | }, 34 | ); 35 | 36 | return Team; 37 | }; 38 | -------------------------------------------------------------------------------- /backend/src/models/Token.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const settings = require("../../config/settings"); 4 | 5 | module.exports = (sequelize, DataTypes) => { 6 | const Token = sequelize.define( 7 | "Token", 8 | { 9 | id: { 10 | type: DataTypes.INTEGER, 11 | primaryKey: true, 12 | autoIncrement: true, 13 | }, 14 | token: { 15 | type: DataTypes.STRING(511), 16 | allowNull: false, 17 | }, 18 | userId: { 19 | type: DataTypes.INTEGER, 20 | allowNull: false, 21 | references: { 22 | model: 'users', 23 | key: 'id', 24 | }, 25 | onDelete: 'CASCADE', 26 | }, 27 | type: { 28 | type: DataTypes.ENUM(settings.token.type), 29 | allowNull: false, 30 | }, 31 | expiresAt: { 32 | type: DataTypes.DATE, 33 | allowNull: true, // Only used for reset tokens 34 | }, 35 | createdAt: { 36 | type: DataTypes.DATE, 37 | allowNull: false, 38 | defaultValue: DataTypes.NOW, 39 | }, 40 | }, 41 | { 42 | tableName: "tokens", 43 | timestamps: false, 44 | } 45 | ); 46 | 47 | Token.associate = (models) => { 48 | Token.belongsTo(models.User, { 49 | foreignKey: "userId", 50 | onDelete: "CASCADE", 51 | }); 52 | }; 53 | 54 | return Token; 55 | }; 56 | -------------------------------------------------------------------------------- /backend/src/models/TourPopup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param {import('sequelize').Sequelize} sequelize 4 | * @param {import('sequelize').DataTypes} DataTypes 5 | * @returns 6 | */ 7 | module.exports = (sequelize, DataTypes) => { 8 | const TourPopup = sequelize.define( 9 | 'TourPopup', 10 | { 11 | id: { 12 | type: DataTypes.INTEGER, 13 | primaryKey: true, 14 | autoIncrement: true, 15 | }, 16 | title: { 17 | type: DataTypes.STRING, 18 | allowNull: false, 19 | }, 20 | header: { 21 | type: DataTypes.STRING, 22 | allowNull: false, 23 | }, 24 | description: { 25 | type: DataTypes.TEXT, 26 | allowNull: false, 27 | }, 28 | targetElement: { 29 | type: DataTypes.STRING, 30 | allowNull: false, 31 | }, 32 | order: { 33 | type: DataTypes.INTEGER, 34 | allowNull: false, 35 | }, 36 | tourId: { 37 | type: DataTypes.INTEGER, 38 | allowNull: false, 39 | references: { 40 | model: 'tours', 41 | key: 'id', 42 | }, 43 | }, 44 | }, 45 | { 46 | tableName: 'tour_popup', 47 | timestamps: false, 48 | } 49 | ); 50 | 51 | TourPopup.associate = (models) => { 52 | TourPopup.belongsTo(models.Tour, { 53 | foreignKey: 'tourId', 54 | as: 'tour', 55 | onDelete: 'CASCADE', 56 | }); 57 | }; 58 | 59 | return TourPopup; 60 | }; 61 | -------------------------------------------------------------------------------- /backend/src/models/User.js: -------------------------------------------------------------------------------- 1 | const settings = require("../../config/settings"); 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | const User = sequelize.define( 5 | "User", 6 | { 7 | id: { 8 | type: DataTypes.INTEGER, 9 | primaryKey: true, 10 | autoIncrement: true, 11 | }, 12 | name: { 13 | type: DataTypes.STRING(63), 14 | allowNull: false, 15 | }, 16 | surname: { 17 | type: DataTypes.STRING(63), 18 | }, 19 | email: { 20 | type: DataTypes.STRING(255), 21 | allowNull: false, 22 | unique: true, 23 | validate: { 24 | isEmail: true, 25 | }, 26 | }, 27 | password: { 28 | type: DataTypes.STRING(127), 29 | allowNull: false, 30 | }, 31 | role: { 32 | type: DataTypes.ENUM(settings.user.roleEnum), 33 | defaultValue: settings.user.role.admin, 34 | allowNull: false, 35 | }, 36 | picture: { 37 | type: DataTypes.TEXT, 38 | allowNull: true, 39 | }, 40 | createdAt: { 41 | type: DataTypes.DATE, 42 | allowNull: false, 43 | defaultValue: DataTypes.NOW, 44 | }, 45 | }, 46 | { 47 | tableName: "users", 48 | timestamps: false, // Disable timestamp 49 | }, 50 | ); 51 | 52 | return User; 53 | }; 54 | -------------------------------------------------------------------------------- /backend/src/routes/auth.routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const authenticateJWT = require("../middleware/auth.middleware"); 3 | const {login, register, logout, resetPassword, forgetPassword} = require('../controllers/auth.controller') 4 | const { loginValidation, registerValidation, forgetPasswordValidation, resetPasswordValidation, } = require("../utils/auth.helper"); 5 | const { handleValidationErrors } = require('../middleware/validation.middleware') 6 | const router = express.Router(); 7 | 8 | router.post("/register", registerValidation, handleValidationErrors, register); 9 | router.post("/login", loginValidation, handleValidationErrors, login); 10 | router.post("/logout", authenticateJWT, logout); 11 | router.post("/forget-password", forgetPasswordValidation, handleValidationErrors, forgetPassword); 12 | router.post("/reset-password", resetPasswordValidation, handleValidationErrors, resetPassword); 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /backend/src/routes/guide.routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const guideController = require("../controllers/guide.controller.js"); 3 | 4 | const router = express.Router(); 5 | 6 | router.post("/get_guides_by_url", guideController.getGuidesByUrl); 7 | router.post("/get_incomplete_guides_by_url", guideController.getIncompleteGuidesByUrl); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /backend/src/routes/guidelog.routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const guidelogController = require("../controllers/guidelog.controller"); 3 | const {addGuideLogValidation, getLogByUserValidation} = require('../utils/guidelog.helper') 4 | const {handleValidationErrors} = require('../middleware/validation.middleware') 5 | const router = express.Router(); 6 | 7 | router.post("/add_guide_log", addGuideLogValidation, handleValidationErrors, guidelogController.addGuideLog); 8 | router.get("/all_guide_logs", guidelogController.getAllLogs); 9 | router.get("/guide_logs_by_user", getLogByUserValidation, handleValidationErrors, guidelogController.getLogsByUser); 10 | router.get("/complete_guide_logs", getLogByUserValidation, handleValidationErrors, guidelogController.getCompleteGuideLogs); 11 | 12 | module.exports = router; -------------------------------------------------------------------------------- /backend/src/routes/helperLink.routes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const helper = require('../controllers/helperLink.controller'); 3 | const authenticateJWT = require('../middleware/auth.middleware'); 4 | const settings = require('../../config/settings'); 5 | const accessGuard = require('../middleware/accessGuard.middleware'); 6 | const { helperValidator } = require('../utils/helperLink.helper'); 7 | const { handleValidationErrors } = require('../middleware/validation.middleware'); 8 | const { idParamValidator } = require('../utils/link.helper'); 9 | const helperController = helper.controller; 10 | 11 | const router = express.Router(); 12 | const teamPermissions = settings.team.permissions; 13 | 14 | router.use(authenticateJWT); 15 | 16 | router.get('/get_helpers', helperController.getHelpersByUserId); 17 | router.get('/get_helpers_with_links', helperController.getAllHelpersWithLinks); 18 | router.get('/all_helpers', helperController.getAllHelpers); 19 | router.get('/get_helper/:id', idParamValidator, handleValidationErrors, helperController.getHelperById); 20 | 21 | router.use(accessGuard(teamPermissions.helpers)); 22 | 23 | router.post('/add_helper', helperValidator, handleValidationErrors, helperController.addHelper); 24 | router.delete('/delete_helper/:id', idParamValidator, handleValidationErrors, helperController.deleteHelper); 25 | router.put('/edit_helper/:id', idParamValidator, helperValidator, handleValidationErrors, helperController.editHelper); 26 | 27 | module.exports = router; 28 | -------------------------------------------------------------------------------- /backend/src/routes/hint.routes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const hintController = require('../controllers/hint.controller'); 3 | const authenticateJWT = require('../middleware/auth.middleware'); 4 | const settings = require('../../config/settings'); 5 | const accessGuard = require('../middleware/accessGuard.middleware'); 6 | const { hintValidator, paramIdValidator, bodyUrlValidator } = require('../utils/hint.helper'); 7 | const { handleValidationErrors } = require('../middleware/validation.middleware'); 8 | 9 | const router = express.Router(); 10 | const teamPermissions = settings.team.permissions; 11 | 12 | router.get('/get_hint_by_url', bodyUrlValidator, handleValidationErrors, hintController.getHintByUrl); 13 | 14 | router.use(authenticateJWT); 15 | 16 | router.get('/all_hints', hintController.getAllHints); 17 | router.get('/hints', hintController.getHints); 18 | router.get('/get_hint/:hintId', paramIdValidator, handleValidationErrors, hintController.getHintById); 19 | 20 | router.use(accessGuard(teamPermissions.hints)); 21 | 22 | router.post('/add_hint', hintValidator, handleValidationErrors, hintController.addHint); 23 | router.delete('/delete_hint/:hintId', paramIdValidator, handleValidationErrors, hintController.deleteHint); 24 | router.put('/edit_hint/:hintId', paramIdValidator, hintValidator, handleValidationErrors, hintController.updateHint); 25 | 26 | module.exports = router; 27 | -------------------------------------------------------------------------------- /backend/src/routes/link.routes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const linkController = require('../controllers/link.controller'); 3 | const authenticateJWT = require('../middleware/auth.middleware'); 4 | const settings = require('../../config/settings'); 5 | const accessGuard = require('../middleware/accessGuard.middleware'); 6 | const { linkValidator, queryValidator, idParamValidator, bodyUrlValidator } = require('../utils/link.helper'); 7 | const { handleValidationErrors } = require('../middleware/validation.middleware'); 8 | 9 | const router = express.Router(); 10 | const teamPermissions = settings.team.permissions; 11 | 12 | router.get('/get_link_by_url', bodyUrlValidator, handleValidationErrors, linkController.getLinkByUrl); 13 | 14 | router.use(authenticateJWT); 15 | 16 | router.get('/get_links', queryValidator, handleValidationErrors, linkController.getLinksByHelperId); 17 | router.get('/all_links', linkController.getAllLinks); 18 | router.get('/get_link/:id', idParamValidator, handleValidationErrors, linkController.getLinksById); 19 | 20 | router.use(accessGuard(teamPermissions.links)); 21 | 22 | router.post('/add_link', linkValidator, handleValidationErrors, linkController.addLink); 23 | router.put('/edit_link/:id', idParamValidator, linkValidator, handleValidationErrors, linkController.editLink); 24 | router.delete('/delete_link/:id', idParamValidator, handleValidationErrors, linkController.deleteLink); 25 | 26 | module.exports = router; 27 | -------------------------------------------------------------------------------- /backend/src/routes/mocks.routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { popup, onboard } = require("./../controllers/onboarding.controller"); 3 | 4 | const router = express.Router(); 5 | router.get("/popup", popup); 6 | router.get("/onboard", onboard); 7 | router.post("/onboard", onboard); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /backend/src/routes/onboard.routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { validateApiId, generateClientId } = require("../middleware/onboard.middleware"); 3 | const onboardControllers = require("../controllers/onboard"); 4 | 5 | const router = express.Router(); 6 | 7 | router.use(validateApiId); 8 | router.use(generateClientId); 9 | 10 | // router.get("/banner", onboardControllers.bannerData.getBannerData); 11 | router.get("/popup", onboardControllers.popupData.getPopupData); 12 | // router.get("/tour", onboardControllers.tourData.getTourData); 13 | // router.get("/hint", onboardControllers.hintData.getHintData); 14 | 15 | module.exports = router; 16 | -------------------------------------------------------------------------------- /backend/src/routes/popup.routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const popupController = require("../controllers/popup.controller"); 3 | const authenticateJWT = require("../middleware/auth.middleware"); 4 | const settings = require('../../config/settings'); 5 | const accessGuard = require('../middleware/accessGuard.middleware'); 6 | const { addOrUpdatePopupValidation, getPopupByUrlValidation, deleteOrGetPopupByIdValidation } = require("../utils/popup.helper"); 7 | const { handleValidationErrors } = require("../middleware/validation.middleware"); 8 | 9 | const router = express.Router(); 10 | const teamPermissions = settings.team.permissions; 11 | 12 | router.post("/add_popup", authenticateJWT, accessGuard(teamPermissions.popups),addOrUpdatePopupValidation, handleValidationErrors, popupController.addPopup); 13 | router.delete("/delete_popup/:id", authenticateJWT, accessGuard(teamPermissions.popups), deleteOrGetPopupByIdValidation, handleValidationErrors, popupController.deletePopup); 14 | router.put("/edit_popup/:id", authenticateJWT, accessGuard(teamPermissions.popups), addOrUpdatePopupValidation, handleValidationErrors,popupController.editPopup); 15 | router.get("/all_popups", authenticateJWT, popupController.getAllPopups); 16 | router.get("/popups", authenticateJWT, popupController.getPopups); 17 | router.get("/get_popup/:id", authenticateJWT,deleteOrGetPopupByIdValidation, handleValidationErrors ,popupController.getPopupById); 18 | router.get("/get_popup_by_url", getPopupByUrlValidation, handleValidationErrors, popupController.getPopupByUrl); 19 | 20 | module.exports = router; 21 | -------------------------------------------------------------------------------- /backend/src/routes/script.routes.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluewave-labs/guidefox/417a99627d91bc81ebe7ebe1f4f3cea068d44f7b/backend/src/routes/script.routes.js -------------------------------------------------------------------------------- /backend/src/routes/statistics.routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const statisticsController = require("../controllers/statistics.controller"); 3 | const authenticateJWT = require("../middleware/auth.middleware"); 4 | 5 | const router = express.Router(); 6 | router.use(authenticateJWT) 7 | router.get("/", statisticsController.getStatistics); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /backend/src/routes/tour.routes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const tourController = require('../controllers/tour.controller'); 3 | const authenticateJWT = require('../middleware/auth.middleware'); 4 | const settings = require('../../config/settings'); 5 | const accessGuard = require('../middleware/accessGuard.middleware'); 6 | const { tourValidator, paramsIdValidator, bodyUrlValidator } = require('../utils/tour.helper'); 7 | const { handleValidationErrors } = require('../middleware/validation.middleware'); 8 | 9 | const router = express.Router(); 10 | const teamPermissions = settings.team.permissions.tours; 11 | 12 | router.get('/get_tour_by_url', bodyUrlValidator, handleValidationErrors, tourController.getTourByUrl); 13 | router.get('/get_tour/:id', tourController.getTourById); 14 | router.use(authenticateJWT); 15 | 16 | router.get('/all_tours', tourController.getAllTours); 17 | router.get('/tours', tourController.getToursByUserId); 18 | 19 | 20 | router.use(accessGuard(teamPermissions)); 21 | 22 | router.post('/add_tour', tourValidator, handleValidationErrors, tourController.createTour); 23 | router.delete('/delete_tour/:id', paramsIdValidator, handleValidationErrors, tourController.deleteTour); 24 | router.put('/edit_tour/:id', paramsIdValidator, tourValidator, handleValidationErrors, tourController.updateTour); 25 | 26 | module.exports = router; 27 | -------------------------------------------------------------------------------- /backend/src/routes/user.routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { 3 | getUsersList, 4 | getCurrentUser, 5 | updateUserDetails, 6 | checkAtLeastOneField, 7 | validateProfileUpdate, 8 | handleValidationErrors, 9 | deleteUser, 10 | hasUsers 11 | } = require("../controllers/user.controller"); 12 | const authenticateJWT = require("../middleware/auth.middleware"); 13 | 14 | const router = express.Router(); 15 | 16 | router.get("/users-list", getUsersList); 17 | router.get("/current-user", authenticateJWT, getCurrentUser); 18 | router.put("/update", authenticateJWT, checkAtLeastOneField, validateProfileUpdate, handleValidationErrors, updateUserDetails); 19 | router.delete("/delete", authenticateJWT, deleteUser); 20 | router.get("/has-users", hasUsers); 21 | 22 | module.exports = router; 23 | -------------------------------------------------------------------------------- /backend/src/scripts/popup.script.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluewave-labs/guidefox/417a99627d91bc81ebe7ebe1f4f3cea068d44f7b/backend/src/scripts/popup.script.js -------------------------------------------------------------------------------- /backend/src/templates/invite.hbs: -------------------------------------------------------------------------------- 1 |

Hello,

2 |

You’ve been invited to join Guidefox - a platform designed to help you build better user experiences.

3 |

To get started, simply click the link below to create your account:

4 |

Join Guidefox

5 |

Once you’ve created your account, you’ll be able to log in and start exploring all the features we’ve prepared for you.

6 |

We’re excited to have you with us and can’t wait for you to experience Guidefox!

7 |

--
The Guidefox Team

8 | -------------------------------------------------------------------------------- /backend/src/templates/resetPassword.hbs: -------------------------------------------------------------------------------- 1 |

Hello {{name}},

2 |

We received a request to reset your password for your Guidefox account. If you didn’t request a password reset, you can safely ignore this email.

3 |

To reset your password, click the link below:

4 | Reset My Password 5 |

Thank you for using Guidefox!

6 |

--
The Guidefox Team

7 | 8 | -------------------------------------------------------------------------------- /backend/src/templates/signup.hbs: -------------------------------------------------------------------------------- 1 |

Welcome to Our Service

2 |

Hi {{name}},

3 |

Thank you for signing up. We're excited to have you on board!

4 | -------------------------------------------------------------------------------- /backend/src/test/e2e/index.mjs: -------------------------------------------------------------------------------- 1 | import { use } from "chai"; 2 | import chaiHttp from "chai-http"; 3 | 4 | const chai = use(chaiHttp); 5 | 6 | export default chai; 7 | -------------------------------------------------------------------------------- /backend/src/test/unit/services/statistics.test.js: -------------------------------------------------------------------------------- 1 | const { describe, it, beforeEach, afterEach } = require("mocha"); 2 | const sinon = require("sinon"); 3 | const { expect } = require("chai"); 4 | const db = require("../../../models/index.js"); 5 | const mocks = require("../../mocks/guidelog.mock.js"); 6 | const statisticsService = require("../../../service/statistics.service.js"); 7 | 8 | const GuideLog = db.GuideLog; 9 | 10 | describe("Test statistics service", () => { 11 | const GuideLogMock = {}; 12 | afterEach(sinon.restore); 13 | it("should return statistics", async () => { 14 | const guideLogs = mocks.guideLogList; 15 | GuideLogMock.findAll = sinon.stub(GuideLog, "findAll").resolves(guideLogs); 16 | const statistics = await statisticsService.generateStatistics({ 17 | userId: 1, 18 | }); 19 | 20 | expect(GuideLogMock.findAll.called).to.equal(true); 21 | expect(statistics).to.be.an("array"); 22 | expect(statistics).to.have.lengthOf(6); 23 | statistics.forEach((statistic) => { 24 | expect(statistic).to.have.property("guideType"); 25 | expect(statistic).to.have.property("views"); 26 | expect(statistic).to.have.property("change"); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /backend/src/utils/constants.helper.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | module.exports = Object.freeze({ 4 | JWT_EXPIRES_IN_1H: '1h', 5 | JWT_EXPIRES_IN_20M: '20m', 6 | TOKEN_LIFESPAN: 3600 * 1000, 7 | API_BASE_URL: process.env.API_BASE_URL || 'localhost:3000/api/', 8 | FRONTEND_URL: process.env.FRONTEND_URL, 9 | MAX_FILE_SIZE: 3 * 1024 * 1024, 10 | ROLE: { 11 | ADMIN: '1', 12 | MEMBER: '2' 13 | }, 14 | MAX_ORG_NAME_LENGTH: 100, 15 | ORG_NAME_REGEX: /^[a-zA-Z0-9\s\-_&.]+$/, 16 | URL_PROTOCOL_REGEX: /^(https?:\/\/)/, 17 | URL_DOMAIN_REGEX: /^https?:\/\/([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/, 18 | }); 19 | -------------------------------------------------------------------------------- /backend/src/utils/errors.helper.js: -------------------------------------------------------------------------------- 1 | const { STATUS_CODES } = require("http"); 2 | const GENERAL_ERROR_CODE = "099"; 3 | 4 | function errorResponse(statusCode, errorCode, message = null) { 5 | const payload = { error: STATUS_CODES[statusCode] || "Unknown error" }; 6 | if (message) { 7 | payload.message = message; 8 | } 9 | if (errorCode) { 10 | payload.errorCode = errorCode; 11 | } 12 | return { 13 | statusCode, 14 | payload, 15 | }; 16 | } 17 | 18 | function badRequest(message, errorCode = GENERAL_ERROR_CODE) { 19 | return errorResponse(400, errorCode, message); 20 | } 21 | 22 | function internalServerError(errorCode, message = null) { 23 | return errorResponse(500, errorCode, message); 24 | } 25 | 26 | module.exports = { 27 | errorResponse, 28 | badRequest, 29 | internalServerError, 30 | }; 31 | -------------------------------------------------------------------------------- /backend/src/utils/guidelog.helper.js: -------------------------------------------------------------------------------- 1 | const { body } = require('express-validator') 2 | 3 | const GuideType = { 4 | POPUP: 0, 5 | HINT: 1, 6 | BANNER: 2, 7 | LINK: 3, 8 | TOUR: 4, 9 | CHECKLIST: 5 10 | } 11 | const VALID_POPUP_TYPES = Object.values(GuideType); 12 | const addGuideLogValidation = [ 13 | body('guideType').notEmpty().withMessage('guideType is required').isIn(VALID_POPUP_TYPES).withMessage('Invalid guideType'), 14 | body('userId').notEmpty().withMessage('userId is required').isString().trim().withMessage('userId must be a non-empty string'), 15 | body('completed').notEmpty().isBoolean().withMessage('completed must be a boolean value'), 16 | body('guideId').notEmpty().withMessage('guideId is required').isNumeric().trim().withMessage('guideId must be a non-empty integer'), 17 | ] 18 | 19 | const getLogByUserValidation = [ 20 | body('userId').notEmpty().withMessage('userId is required').isString().trim().withMessage('userId must be a non-empty string') 21 | ] 22 | 23 | module.exports = { 24 | addGuideLogValidation, 25 | getLogByUserValidation, 26 | GuideType 27 | } -------------------------------------------------------------------------------- /backend/src/utils/jwt.helper.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | require("dotenv").config(); 3 | const { JWT_EXPIRES_IN_1H } = require('./constants.helper'); 4 | 5 | const generateToken = (payload, expiresIn = JWT_EXPIRES_IN_1H) => { 6 | return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn }); 7 | }; 8 | 9 | const verifyToken = (token) => { 10 | try { 11 | return jwt.verify(token, process.env.JWT_SECRET); 12 | } catch (error) { 13 | return null; 14 | } 15 | }; 16 | 17 | module.exports = { generateToken, verifyToken }; 18 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | services: 2 | onboarding_backend: 3 | image: onboarding/onboarding:v0 4 | build: ./backend 5 | volumes: 6 | - ./backend:/app 7 | - /app/node_modules 8 | ports: 9 | - "3000:3000" 10 | depends_on: 11 | - db 12 | env_file: 13 | - ./backend/.env 14 | environment: 15 | - NODE_ENV=${NODE_ENV:-development} 16 | command: > 17 | npm run dev; 18 | 19 | db: 20 | image: postgres:latest 21 | environment: 22 | - POSTGRES_USER=${POSTGRES_USER} 23 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} 24 | - POSTGRES_DB=${POSTGRES_DB} 25 | ports: 26 | - "5432:5432" 27 | volumes: 28 | - pgdata:/var/lib/postgresql/data 29 | frontend: 30 | build: 31 | context: ./frontend/ 32 | dockerfile: Dockerfile.development 33 | image: onboarding-frontend-dev:latest 34 | volumes: 35 | - ./frontend:/app 36 | - /app/node_modules 37 | ports: 38 | - "4173:4173" 39 | environment: 40 | - NODE_ENV=${NODE_ENV:-development} 41 | 42 | mailhog: 43 | image: mailhog/mailhog 44 | ports: 45 | - "1025:1025" 46 | - "8025:8025" 47 | 48 | volumes: 49 | pgdata: 50 | -------------------------------------------------------------------------------- /docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | services: 2 | onboarding_backend: 3 | image: onboarding/onboarding:v0 4 | build: ./backend 5 | volumes: 6 | - ./backend:/app 7 | - /app/node_modules 8 | ports: 9 | - "3000:3000" 10 | depends_on: 11 | - db 12 | env_file: 13 | - ./backend/.env 14 | environment: 15 | - NODE_ENV=${NODE_ENV:-production} 16 | command: > 17 | bash -c " 18 | if ! npx sequelize-cli db:migrate; then 19 | echo 'Migration failed, attempting to create the database...' 20 | if ! npx sequelize-cli db:create; then 21 | echo 'Database might already exist or creation failed.' 22 | fi 23 | echo 'Retrying migration...' 24 | npx sequelize-cli db:migrate 25 | fi 26 | npm run prod 27 | " 28 | db: 29 | image: postgres:latest 30 | environment: 31 | - POSTGRES_USER=${POSTGRES_USER} 32 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} 33 | - POSTGRES_DB=${POSTGRES_DB} 34 | ports: 35 | - "5432:5432" 36 | volumes: 37 | - pgdata:/var/lib/postgresql/data 38 | frontend: 39 | image: onboarding-frontend-prod:latest 40 | build: 41 | context: ./frontend/ 42 | dockerfile: Dockerfile.production 43 | ports: 44 | - "81:80" 45 | environment: 46 | - NODE_ENV=${NODE_ENV:-production} 47 | 48 | mailhog: 49 | image: mailhog/mailhog 50 | ports: 51 | - "1025:1025" 52 | - "8025:8025" 53 | 54 | volumes: 55 | pgdata: 56 | -------------------------------------------------------------------------------- /extension/background.js: -------------------------------------------------------------------------------- 1 | chrome.action.onClicked.addListener((tab) => { 2 | chrome.scripting.executeScript({ 3 | target: {tabId: tab.id}, 4 | func: setup, 5 | args: [tab], 6 | }); 7 | }); 8 | 9 | function setup() { 10 | addHighlight(); 11 | window.addEventListener('mousemove', throttle(updateHighlight)); 12 | window.addEventListener('click', grabSelector, {capture: true}); 13 | window.addEventListener('keydown', checkTerminateKeys); 14 | }; 15 | -------------------------------------------------------------------------------- /extension/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluewave-labs/guidefox/417a99627d91bc81ebe7ebe1f4f3cea068d44f7b/extension/icon.png -------------------------------------------------------------------------------- /extension/icon_128_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluewave-labs/guidefox/417a99627d91bc81ebe7ebe1f4f3cea068d44f7b/extension/icon_128_128.png -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Bluewave Element Selector", 3 | "description": "Select an element and getting", 4 | "version": "1.1.0", 5 | "manifest_version": 3, 6 | "permissions": ["activeTab", "scripting", "storage"], 7 | "background": { 8 | "service_worker": "background.js" 9 | }, 10 | "content_scripts": [ 11 | { 12 | "matches": [""], 13 | "js": ["app.js"] 14 | } 15 | ], 16 | "commands": { 17 | "_execute_action": { 18 | "suggested_key": { 19 | "default": "Ctrl+Shift+S", 20 | "mac": "Command+Shift+S" 21 | } 22 | } 23 | }, 24 | "action": { 25 | "default_title": "Bluewave Element Selector", 26 | "default_icon": "icon.png" 27 | } 28 | } -------------------------------------------------------------------------------- /extension/trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluewave-labs/guidefox/417a99627d91bc81ebe7ebe1f4f3cea068d44f7b/extension/trash.png -------------------------------------------------------------------------------- /frontend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 2021, 4 | "sourceType": "module" 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:storybook/recommended", 10 | "prettier" 11 | ], 12 | "plugins": ["prettier"], 13 | "rules": { 14 | "prettier/prettier": "error", 15 | "no-undef": "off", 16 | "react/react-in-jsx-scope": "off" 17 | }, 18 | "settings": { 19 | "react": { 20 | "version": "detect" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 4 | 5 | # dependencies 6 | /node_modules 7 | /.pnp 8 | .pnp.js 9 | 10 | # testing 11 | /coverage 12 | 13 | # production 14 | /build 15 | /dist 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | *storybook.log -------------------------------------------------------------------------------- /frontend/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | # frontend/.husky/pre-commit 2 | cd frontend 3 | npx lint-staged 4 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf" 3 | } 4 | -------------------------------------------------------------------------------- /frontend/.storybook/main.js: -------------------------------------------------------------------------------- 1 | /** @type { import('@storybook/react-vite').StorybookConfig } */ 2 | const config = { 3 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], 4 | addons: [ 5 | "@storybook/addon-onboarding", 6 | "@storybook/addon-links", 7 | "@storybook/addon-essentials", 8 | "@chromatic-com/storybook", 9 | "@storybook/addon-interactions", 10 | ], 11 | framework: { 12 | name: "@storybook/react-vite", 13 | options: {}, 14 | }, 15 | }; 16 | export default config; 17 | -------------------------------------------------------------------------------- /frontend/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | /** @type { import('@storybook/react').Preview } */ 2 | import "../src/styles/variables.css"; 3 | const preview = { 4 | parameters: { 5 | controls: { 6 | matchers: { 7 | color: /(background|color)$/i, 8 | date: /Date$/i, 9 | }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default preview; 15 | -------------------------------------------------------------------------------- /frontend/Dockerfile.development: -------------------------------------------------------------------------------- 1 | FROM node:22-alpine3.21 2 | 3 | RUN apk update && apk add bash && rm -rf /var/cache/apk/* 4 | 5 | WORKDIR /app 6 | 7 | COPY package.json ./ 8 | 9 | RUN npm install 10 | 11 | COPY . . 12 | 13 | ENV NODE_OPTIONS="--max-old-space-size=4096" 14 | 15 | EXPOSE 4173 16 | 17 | CMD ["npm", "run", "dev"] 18 | -------------------------------------------------------------------------------- /frontend/Dockerfile.production: -------------------------------------------------------------------------------- 1 | FROM node:22-alpine3.21 AS builder 2 | WORKDIR /app 3 | COPY package*.json ./ 4 | ENV NODE_OPTIONS="--max-old-space-size=4096" 5 | RUN npm ci 6 | COPY . . 7 | RUN npm run build 8 | 9 | FROM nginx:alpine 10 | COPY --from=builder /app/dist /usr/share/nginx/html 11 | COPY default.conf /etc/nginx/conf.d/default.conf 12 | #COPY nginx.conf /etc/nginx/conf.d/default.conf 13 | 14 | EXPOSE 80 15 | CMD [ "nginx", "-g", "daemon off;" ] 16 | -------------------------------------------------------------------------------- /frontend/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | server_name localhost; 5 | 6 | #access_log /var/log/nginx/host.access.log main; 7 | 8 | location / { 9 | root /usr/share/nginx/html; 10 | index index.html index.htm; 11 | try_files $uri $uri/ /index.html; 12 | } 13 | 14 | #error_page 404 /404.html; 15 | 16 | # redirect server error pages to the static page /50x.html 17 | # 18 | error_page 500 502 503 504 /50x.html; 19 | location = /50x.html { 20 | root /usr/share/nginx/html; 21 | } 22 | 23 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80 24 | # 25 | #location ~ \.php$ { 26 | # proxy_pass http://127.0.0.1; 27 | #} 28 | 29 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 30 | # 31 | #location ~ \.php$ { 32 | # root html; 33 | # fastcgi_pass 127.0.0.1:9000; 34 | # fastcgi_index index.php; 35 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 36 | # include fastcgi_params; 37 | #} 38 | 39 | # deny access to .htaccess files, if Apache's document root 40 | # concurs with nginx's one 41 | # 42 | #location ~ /\.ht { 43 | # deny all; 44 | #} 45 | } 46 | 47 | -------------------------------------------------------------------------------- /frontend/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Guidefox 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Guidefox 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /frontend/public/no-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluewave-labs/guidefox/417a99627d91bc81ebe7ebe1f4f3cea068d44f7b/frontend/public/no-background.jpg -------------------------------------------------------------------------------- /frontend/public/svg/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/public/vendetta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluewave-labs/guidefox/417a99627d91bc81ebe7ebe1f4f3cea068d44f7b/frontend/public/vendetta.png -------------------------------------------------------------------------------- /frontend/src/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "arrowParens": "always", 8 | "htmlWhitespaceSensitivity": "strict" 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/IconWrapper.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | const IconWrapper = ({ children, label, ...props }) => ( 4 | 14 | {children} 15 | 16 | ); 17 | 18 | IconWrapper.propTypes = { 19 | children: PropTypes.node, 20 | label: PropTypes.string.isRequired, 21 | }; 22 | 23 | export default IconWrapper; 24 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/google-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/src/assets/logo/introflow_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/src/components/Announcements/Announcements.jsx: -------------------------------------------------------------------------------- 1 | import { useState, React } from 'react'; 2 | import styles from './Announcements.module.scss'; 3 | 4 | const Announcements = () => { 5 | const [showFirstSection, setShowFirstSection] = useState(false); 6 | 7 | return ( 8 |
9 |

We've just released a new update

10 |

Check out all new dashboard view. Pages and now load faster

11 | 12 | {showFirstSection && ( 13 |
14 | 15 | 16 |
17 | )} 18 | 19 | {!showFirstSection && ( 20 |
21 |

Subscribe to updates

22 |
23 | 28 | 29 |
30 |
31 | )} 32 |
33 | ); 34 | }; 35 | 36 | export default Announcements; 37 | -------------------------------------------------------------------------------- /frontend/src/components/Announcements/Announcements.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/globals.scss' as *; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | border: 1px solid var(--second-text-color); 7 | padding: 1em 2em; 8 | border-radius: 1em; 9 | box-shadow: 0px 1px 2px 0px #1018280d; 10 | 11 | h2 { 12 | @include text-style(header-text, semibold); 13 | } 14 | 15 | h3 { 16 | @include text-style(regular); 17 | } 18 | } 19 | 20 | .buttons { 21 | display: flex; 22 | flex-direction: row; 23 | flex-wrap: wrap; 24 | gap: 10px; 25 | } 26 | 27 | .changelog, 28 | .dismiss { 29 | border-radius: 8px; 30 | border: 1px solid var(--second-text-color); 31 | padding: 8px 14px; 32 | box-shadow: 0px 1px 2px 0px #1018280d; 33 | } 34 | 35 | .changelog { 36 | background-color: var(--main-purple); 37 | color: white; 38 | } 39 | 40 | .dismiss { 41 | background-color: white; 42 | margin-right: 10px; 43 | } 44 | 45 | .emailInput { 46 | width: 320px; 47 | height: 2rem; 48 | border: 1px solid var(--light-gray); 49 | } 50 | -------------------------------------------------------------------------------- /frontend/src/components/Avatar/Avatar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classNames from 'classnames'; 4 | import './AvatarStyles.css'; 5 | 6 | const Avatar = ({ src, alt, size = 'medium', className }) => { 7 | const defaultClasses = classNames( 8 | 'avatar-container', 9 | { [`avatar-${size}`]: size }, 10 | className 11 | ); 12 | 13 | //Validation check so that picture string follows base64 constraints 14 | const validSrc = 15 | src && 16 | (src.startsWith('data:image') || 17 | src.startsWith('/') || 18 | src.startsWith('http')) 19 | ? src 20 | : '/vendetta.png'; 21 | 22 | return {alt}; 23 | }; 24 | 25 | Avatar.propTypes = { 26 | src: PropTypes.string.isRequired, 27 | alt: PropTypes.string, 28 | size: PropTypes.oneOf(['small', 'medium', 'large']), 29 | className: PropTypes.string, 30 | }; 31 | 32 | export default Avatar; 33 | -------------------------------------------------------------------------------- /frontend/src/components/Avatar/AvatarStyles.css: -------------------------------------------------------------------------------- 1 | .avatar-container { 2 | display: block; 3 | justify-content: center; 4 | align-items: center; 5 | border-radius: 50%; 6 | overflow: hidden; 7 | box-sizing: border-box; 8 | border-radius: 50%; 9 | margin-right: 10px; 10 | border: 0.5px solid #000000; 11 | object-fit: cover; 12 | } 13 | 14 | .avatar-small { 15 | width: 24px; 16 | height: 24px; 17 | } 18 | 19 | .avatar-medium { 20 | width: 40px; 21 | height: 40px; 22 | } 23 | 24 | .avatar-large { 25 | width: 48px; 26 | height: 48px; 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/components/Button/Button.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Button as MuiButton } from '@mui/material'; 4 | import './ButtonStyles.css'; 5 | import CircularProgress from '@mui/material/CircularProgress'; 6 | 7 | const Button = ({ 8 | text = '', 9 | onClick = () => {}, 10 | variant = 'contained', 11 | style = null, 12 | sx = null, 13 | disabled = false, 14 | buttonType = 'primary', 15 | type = 'button', 16 | loading = false, 17 | startIcon = null, 18 | endIcon = null, 19 | }) => { 20 | const classname = 'button ' + buttonType; 21 | return ( 22 | 34 | {loading ? : text} 35 | 36 | ); 37 | }; 38 | 39 | Button.propTypes = { 40 | text: PropTypes.string, 41 | onClick: PropTypes.func, 42 | variant: PropTypes.oneOf(['text', 'outlined', 'contained']), 43 | style: PropTypes.object, 44 | sx: PropTypes.object, 45 | disabled: PropTypes.bool, 46 | buttonType: PropTypes.oneOf([ 47 | 'primary', 48 | 'secondary', 49 | 'secondary-grey', 50 | 'secondary-purple', 51 | 'error', 52 | ]), 53 | type: PropTypes.oneOf(['button', 'submit', 'reset']), 54 | loading: PropTypes.bool, 55 | startIcon: PropTypes.element, 56 | endIcon: PropTypes.element, 57 | }; 58 | 59 | export default Button; 60 | -------------------------------------------------------------------------------- /frontend/src/components/Button/CreateActivityButton/CreateActivityButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '../Button'; 3 | import './CreateActivityButtonStyles.css'; 4 | import CheckIcon from '../../CheckIcon/CheckIcon'; 5 | import PropTypes from 'prop-types'; 6 | import { 7 | activityData, 8 | ACTIVITY_TYPES, 9 | } from '../../../data/createActivityButtonData'; 10 | 11 | const CreateActivityButton = ({ type = ACTIVITY_TYPES.HINTS, onClick }) => { 12 | const { heading, listItem, buttonText } = activityData[type]; 13 | 14 | return ( 15 |
16 |
17 |

{heading}

18 |
    19 | {listItem.map((item, index) => ( 20 |
    21 | 22 |
  • 23 | {item} 24 |
  • 25 |
    26 | ))} 27 |
28 |
29 |
30 |
38 |
39 | ); 40 | }; 41 | 42 | CreateActivityButton.propTypes = { 43 | type: PropTypes.oneOf(Object.values(ACTIVITY_TYPES)).isRequired, 44 | onClick: PropTypes.func.isRequired, 45 | }; 46 | 47 | export default CreateActivityButton; 48 | -------------------------------------------------------------------------------- /frontend/src/components/Button/CreateActivityButton/CreateActivityButtonStyles.css: -------------------------------------------------------------------------------- 1 | @import '../../../styles/variables.css'; 2 | 3 | .bannerStyle { 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | flex-direction: column; 8 | } 9 | 10 | .heading { 11 | color: var(--main-text-color); 12 | font-weight: 600; 13 | margin-bottom: 15px; 14 | font-size: var(--font-header); 15 | } 16 | 17 | .list { 18 | display: flex; 19 | align-items: center; 20 | gap: 15px; 21 | padding: 5px; 22 | } 23 | 24 | .listText { 25 | color: var(--main-text-color); 26 | font-weight: 400; 27 | font-size: var(--font-regular); 28 | margin-bottom: 10px; 29 | list-style-type: none; 30 | } 31 | 32 | .bannerContents { 33 | display: flex; 34 | flex-direction: column; 35 | align-items: center; 36 | } 37 | 38 | .button { 39 | margin-top: 10px; 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/components/Button/GoogleSignInButton/GoogleSignInButton.css: -------------------------------------------------------------------------------- 1 | .google-sign-in-button { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | width: 100%; 6 | padding: 5px; 7 | margin-bottom: 30px; 8 | border: 1px solid var(--light-border-color); 9 | border-radius: 8px; 10 | color: var(--main-text-color); 11 | box-shadow: 0px 1px 2px 0px #1018280d; 12 | cursor: pointer; 13 | font-size: var(--font-regular); 14 | background-color: #fff; 15 | } 16 | 17 | .google-icon { 18 | margin-right: 8px; 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/components/Button/GoogleSignInButton/GoogleSignInButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import GoogleIconSvg from '../../../assets/icons/google-icon.svg'; 4 | import './GoogleSignInButton.css'; 5 | 6 | const GoogleSignInButton = ({ 7 | text = 'Sign in with Google', 8 | onClick = () => {}, 9 | }) => { 10 | return ( 11 | 15 | ); 16 | }; 17 | 18 | GoogleSignInButton.propTypes = { 19 | text: PropTypes.string, 20 | onClick: PropTypes.func, 21 | }; 22 | 23 | export default GoogleSignInButton; 24 | -------------------------------------------------------------------------------- /frontend/src/components/CheckIcon/CheckIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import CheckCircleIcon from '@mui/icons-material/CheckCircle'; 4 | import CheckCircleOutlinedIcon from '@mui/icons-material/CheckCircleOutlined'; 5 | 6 | const CheckIcon = ({ size = 'medium', outline = true, color = 'purple' }) => { 7 | const getColorVariable = (color) => { 8 | switch (color) { 9 | case 'purple': 10 | return 'var(--main-purple)'; 11 | case 'green': 12 | return 'var(--checkIcon-green)'; 13 | case 'black': 14 | return 'var(--third-text-color)'; 15 | default: 16 | return color; // Fallback to the passed color 17 | } 18 | }; 19 | 20 | const mappedColor = getColorVariable(color); 21 | 22 | return ( 23 |
24 | {outline ? ( 25 | 26 | ) : ( 27 | 28 | )} 29 |
30 | ); 31 | }; 32 | 33 | CheckIcon.propTypes = { 34 | size: PropTypes.oneOf(['small', 'medium', 'large']), 35 | outline: PropTypes.bool, 36 | color: PropTypes.oneOf(['purple', 'green', 'black']), 37 | }; 38 | 39 | export default CheckIcon; 40 | -------------------------------------------------------------------------------- /frontend/src/components/CheckIcon/CheckIcon.stories.js: -------------------------------------------------------------------------------- 1 | import CheckIcon from './CheckIcon'; 2 | 3 | //Storybook display settings 4 | export default { 5 | title: 'Visuals/CheckIcon', 6 | component: CheckIcon, 7 | argTypes: { 8 | type: { 9 | options: ['outline', 'solid'], 10 | control: { type: 'radio' }, 11 | }, 12 | size: { 13 | options: ['small', 'medium', 'large'], 14 | control: { type: 'radio' }, 15 | }, 16 | color: { 17 | options: ['purple', 'black', 'green'], 18 | control: { type: 'radio' }, 19 | }, 20 | }, 21 | parameters: { 22 | layout: 'centered', 23 | }, 24 | tags: ['autodocs'], 25 | }; 26 | 27 | //Stories for each Check circle type 28 | export const PurpleOutline = { 29 | args: { 30 | type: 'outline', 31 | size: 'medium', 32 | color: 'purple', 33 | }, 34 | }; 35 | 36 | export const BlackOutline = { 37 | args: { 38 | type: 'outline', 39 | size: 'medium', 40 | color: 'black', 41 | }, 42 | }; 43 | 44 | export const GreenOutline = { 45 | args: { 46 | type: 'outline', 47 | size: 'medium', 48 | color: 'green', 49 | }, 50 | }; 51 | 52 | export const PurpleSolid = { 53 | args: { 54 | type: 'solid', 55 | size: 'medium', 56 | color: 'purple', 57 | }, 58 | }; 59 | 60 | export const BlackSolid = { 61 | args: { 62 | type: 'solid', 63 | size: 'medium', 64 | color: 'black', 65 | }, 66 | }; 67 | 68 | export const GreenSolid = { 69 | args: { 70 | type: 'solid', 71 | size: 'medium', 72 | color: 'green', 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /frontend/src/components/Checkbox/CheckboxHRM.css: -------------------------------------------------------------------------------- 1 | /* Checkboxes and radio buttons */ 2 | .checkbox, 3 | .radio { 4 | accent-color: var(--main-purple); 5 | } 6 | 7 | .checkbox { 8 | border-radius: 4px; 9 | padding: 1px; 10 | } 11 | 12 | .checkbox:active, 13 | .radio:active { 14 | accent-color: var(--main-purple); 15 | outline: 5px solid #9e77ed3d; 16 | } 17 | 18 | /* Small checkboxes and radio buttons */ 19 | .small { 20 | width: 16px; 21 | height: 16px; 22 | border-radius: 4px; 23 | } 24 | 25 | /* Large checkboxes and radio buttons */ 26 | .large { 27 | width: 20px; 28 | height: 20px; 29 | border-radius: 6px; 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/components/Checkbox/CheckboxHRM.stories.js: -------------------------------------------------------------------------------- 1 | import Checkbox from './CheckboxHRM'; 2 | 3 | //Storybook display settings 4 | export default { 5 | title: 'Interactables/Checkbox', 6 | component: Checkbox, 7 | argTypes: { 8 | enabled: { 9 | control: { type: 'boolean' }, 10 | }, 11 | }, 12 | parameters: { 13 | layout: 'centered', 14 | }, 15 | tags: ['autodocs'], 16 | }; 17 | 18 | //Stories for each checkbox type 19 | export const Box = { 20 | args: { 21 | type: 'checkbox', 22 | id: 'test', 23 | name: 'name', 24 | value: 'value', 25 | onChange: () => {}, 26 | }, 27 | }; 28 | 29 | export const Radio = { 30 | args: { 31 | type: 'radio', 32 | id: 'test', 33 | name: 'name', 34 | value: 'value', 35 | onChange: () => {}, 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /frontend/src/components/Checkbox/CheckboxStyles.css: -------------------------------------------------------------------------------- 1 | .checkbox { 2 | display: flex; 3 | align-items: center; 4 | cursor: pointer; 5 | } 6 | .checkbox .MuiCheckbox-root { 7 | border: none; 8 | outline: none; 9 | } 10 | .checkbox-label { 11 | font-size: var(--font-header); 12 | } 13 | 14 | .primary .MuiCheckbox-root { 15 | color: var(--main-purple); 16 | } 17 | 18 | .secondary .MuiCheckbox-root { 19 | color: #808080e5; 20 | } 21 | 22 | .primary .MuiCheckbox-root.Mui-checked { 23 | color: var(--main-purple); 24 | } 25 | 26 | .secondary .MuiCheckbox-root.Mui-checked { 27 | color: #808080e5; 28 | } 29 | 30 | .primary .MuiCheckbox-root.MuiCheckbox-indeterminate { 31 | color: var(--main-purple); 32 | } 33 | 34 | .secondary .MuiCheckbox-root.MuiCheckbox-indeterminate { 35 | color: #808080e5; 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/components/ColorTextField/ColorTextField.jsx: -------------------------------------------------------------------------------- 1 | import { React } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { MuiColorInput } from 'mui-color-input'; 4 | import styles from './ColorTextField.module.scss'; 5 | 6 | const ColorTextField = ({ 7 | name = '', 8 | onChange = () => null, 9 | onBlur = () => null, 10 | value = null, 11 | error = false, 12 | }) => { 13 | return ( 14 | 24 | ); 25 | }; 26 | 27 | ColorTextField.propTypes = { 28 | name: PropTypes.string, 29 | onBlur: PropTypes.func, 30 | onChange: PropTypes.func, 31 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 32 | error: PropTypes.bool, 33 | }; 34 | 35 | export default ColorTextField; 36 | -------------------------------------------------------------------------------- /frontend/src/components/ColorTextField/ColorTextField.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/globals.scss' as *; 2 | 3 | .colorTextField { 4 | input { 5 | font-size: var(--font-regular); 6 | height: 34px; 7 | padding-top: 0; 8 | padding-bottom: 0; 9 | } 10 | 11 | button { 12 | height: 17px; 13 | width: 17px; 14 | box-shadow: none; 15 | border: 1.23px solid var(--grey-border); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/components/CustomLabelTag/CustomLabelTag.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './CustomLabelTagStyles.css'; 4 | 5 | const VALID_BACKGROUND_COLORS = [ 6 | 'orange', 7 | 'gray', 8 | 'purple', 9 | 'green', 10 | 'seen', 11 | 'waiting', 12 | 'new', 13 | ]; 14 | 15 | const CustomLabelTag = ({ 16 | text, 17 | backgroundColor = 'white', 18 | textColor = '', 19 | className = '', 20 | }) => { 21 | const labelClass = `label label-${backgroundColor} ${className}`; 22 | 23 | const style = { 24 | color: textColor, 25 | }; 26 | 27 | return ( 28 | 29 | {['seen', 'waiting', 'new'].includes(backgroundColor) && ( 30 | 31 | )} 32 | {text} 33 | 34 | ); 35 | }; 36 | 37 | CustomLabelTag.propTypes = { 38 | text: PropTypes.string.isRequired, 39 | backgroundColor: PropTypes.oneOf(VALID_BACKGROUND_COLORS), 40 | textColor: PropTypes.string, 41 | className: PropTypes.string, 42 | }; 43 | 44 | export default CustomLabelTag; 45 | -------------------------------------------------------------------------------- /frontend/src/components/CustomLabelTag/CustomLabelTagStyles.css: -------------------------------------------------------------------------------- 1 | .label { 2 | display: inline-flex; 3 | align-items: center; 4 | padding: 0.25rem 0.75rem; 5 | border-radius: 0.3125rem; 6 | font-size: 0.875rem; 7 | font-weight: bold; 8 | margin: 0.5rem 0; 9 | box-sizing: border-box; 10 | } 11 | 12 | .label-orange { 13 | background-color: var(--label-orange-bg); 14 | color: var(--label-orange-color); 15 | border: 1px solid var(--label-orange-border); 16 | } 17 | 18 | .label-gray { 19 | background-color: var(--light-gray); 20 | color: var(--main-text-color); 21 | border: 1px solid var(--light-border-color); 22 | } 23 | 24 | .label-purple { 25 | background-color: var(--light-purple); 26 | color: var(--main-purple); 27 | border: 1px solid var(--main-purple); 28 | } 29 | 30 | .label-green { 31 | background-color: var(--label-green-bg); 32 | color: var(--checkIcon-green); 33 | border: 1px solid var(--label-green-border); 34 | } 35 | 36 | .label-seen, 37 | .label-waiting, 38 | .label-new { 39 | border: 1px solid var(--light-border-color); 40 | } 41 | 42 | .dot { 43 | width: 0.5rem; 44 | height: 0.5rem; 45 | border-radius: 50%; 46 | margin-right: 0.25rem; 47 | } 48 | 49 | .label-seen .dot { 50 | background-color: var(--main-text-color); 51 | } 52 | 53 | .label-waiting .dot { 54 | background-color: var(--border-error-solid); 55 | } 56 | 57 | .label-new .dot { 58 | background-color: var(--label-new-dot); 59 | } 60 | 61 | @media (max-width: 768px) { 62 | .label { 63 | font-size: 0.75rem; 64 | padding: 0.25rem 0.5rem; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /frontend/src/components/CustomLink/CustomLink.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Link from '@mui/material/Link'; 4 | import './CustomLinkStyles.css'; 5 | 6 | const CustomLink = ({ 7 | text = 'Default Text', 8 | url = '', 9 | className = '', 10 | underline = 'none', 11 | onClick, 12 | }) => { 13 | const handleClick = (event) => { 14 | if (onClick) { 15 | event.preventDefault(); 16 | onClick(event); 17 | } 18 | }; 19 | 20 | return ( 21 | 27 | {text} 28 | 29 | ); 30 | }; 31 | 32 | CustomLink.propTypes = { 33 | text: PropTypes.string, 34 | url: PropTypes.string, 35 | className: PropTypes.string, 36 | underline: PropTypes.oneOf(['none', 'hover', 'always']), 37 | onClick: PropTypes.func, 38 | }; 39 | 40 | export default CustomLink; 41 | -------------------------------------------------------------------------------- /frontend/src/components/CustomLink/CustomLinkStyles.css: -------------------------------------------------------------------------------- 1 | .custom-link { 2 | text-decoration: none; 3 | } 4 | 5 | .custom-link.tertiary { 6 | position: relative; 7 | display: inline-block; 8 | } 9 | 10 | .custom-link.tertiary::after { 11 | content: ''; 12 | position: absolute; 13 | left: 0; 14 | bottom: -2px; 15 | width: 100%; 16 | border-bottom: 1px dashed var(--main-purple); 17 | transition: border-bottom 0.3s ease-in-out; 18 | } 19 | 20 | .custom-link.tertiary:hover { 21 | background-color: #f1f2ff; 22 | } 23 | 24 | .custom-link.tertiary:hover::after { 25 | border-bottom: 1px solid var(--main-purple); 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/components/DraggableListItem/DraggableListItem.module.scss: -------------------------------------------------------------------------------- 1 | .grabHandle { 2 | cursor: grab; 3 | display: flex; 4 | align-items: center; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/components/DropdownList/DropdownList.css: -------------------------------------------------------------------------------- 1 | .select { 2 | width: 241px; 3 | height: 34px; 4 | font-size: var(--font-regular) !important; 5 | border-radius: 8px !important; 6 | } 7 | 8 | .select:hover .MuiOutlinedInput-notchedOutline { 9 | border-color: var(--main-purple) !important; 10 | } 11 | 12 | .menuItem { 13 | font-size: var(--font-regular) !important; 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/components/DropdownMenu/DropdownMenu.css: -------------------------------------------------------------------------------- 1 | .dropdown-menu { 2 | position: absolute; 3 | bottom: calc(8vh + 10px); 4 | min-width: fit-content; 5 | z-index: 1; 6 | } 7 | 8 | .dropdown-menu .MuiListItem-root { 9 | cursor: pointer; 10 | } 11 | 12 | .dropdown-menu .MuiListItem-root:hover { 13 | background-color: #f9fafb; 14 | } 15 | 16 | .dropdown-item .MuiTypography-root { 17 | width: 100%; 18 | font-size: var(--font-regular); 19 | font-weight: 400; 20 | line-height: 24px; 21 | color: var(--main-text-color); 22 | display: inline; 23 | justify-content: center; 24 | } 25 | 26 | .dropdown-item { 27 | padding: 4px 12px !important; 28 | } 29 | 30 | .dropdown-list { 31 | padding-top: 4px !important; 32 | padding-bottom: 4px !important; 33 | } 34 | 35 | .dropdown-menu .MuiSvgIcon-root { 36 | color: var(--second-text-color); 37 | } 38 | 39 | .dropdown-menu .MuiListItemIcon-root { 40 | min-width: 2rem; 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/components/DropdownMenu/DropdownMenu.module.css: -------------------------------------------------------------------------------- 1 | .dropdownButton { 2 | background-color: transparent; 3 | border: none; 4 | cursor: pointer; 5 | padding: 0.4em 0.4em; 6 | margin: 1px; 7 | position: relative; 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/components/DropdownMenu/DropdownMenuList/DropdownMenuList.css: -------------------------------------------------------------------------------- 1 | .dropdownMenu { 2 | position: absolute; 3 | min-width: fit-content; 4 | z-index: 1; 5 | } 6 | 7 | .dropdownMenu-up { 8 | bottom: 3rem; 9 | } 10 | 11 | .dropdownMenu-down { 12 | top: 3rem; 13 | } 14 | 15 | .dropdownMenu-left { 16 | right: 3rem; 17 | bottom: 0; 18 | } 19 | 20 | .dropdownMenu-right { 21 | left: 3rem; 22 | bottom: 0; 23 | } 24 | 25 | .dropdownMenu .MuiListItem-root { 26 | cursor: pointer; 27 | } 28 | 29 | .dropdownMenu .MuiListItem-root:hover { 30 | background-color: #f9fafb; 31 | } 32 | 33 | .dropdownItem .MuiTypography-root { 34 | width: 100%; 35 | font-size: 13px; 36 | font-weight: 400; 37 | line-height: 24px; 38 | color: var(--main-text-color); 39 | display: inline; 40 | justify-content: center; 41 | } 42 | 43 | .dropdownItem { 44 | padding: 4px 12px !important; 45 | } 46 | 47 | .dropdownList { 48 | padding-top: 4px !important; 49 | padding-bottom: 4px !important; 50 | } 51 | 52 | .dropdownMenu .MuiSvgIcon-root { 53 | color: var(--second-text-color); 54 | } 55 | 56 | .dropdownMenu .MuiListItemIcon-root { 57 | min-width: 2rem; 58 | } 59 | -------------------------------------------------------------------------------- /frontend/src/components/DropdownMenu/DropdownMenuList/DropdownMenuList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | List, 5 | ListItemButton, 6 | ListItemText, 7 | Paper, 8 | ListItemIcon, 9 | } from '@mui/material'; 10 | import './DropdownMenuList.css'; 11 | 12 | const DropdownMenuList = ({ menuItems, direction = 'up' }) => { 13 | if (!menuItems || menuItems.length === 0) { 14 | return null; 15 | } 16 | 17 | const className = 'dropdownMenu-' + direction; 18 | 19 | return ( 20 | 21 | 22 | {menuItems.map(({ text, icon, onClick }, index) => ( 23 | onClick(e, index)} 27 | > 28 | {icon && {icon}} 29 | 30 | 31 | ))} 32 | 33 | 34 | ); 35 | }; 36 | 37 | DropdownMenuList.propTypes = { 38 | menuItems: PropTypes.array.isRequired, 39 | direction: PropTypes.oneOf(['up', 'down', 'left', 'right']), 40 | }; 41 | 42 | export default DropdownMenuList; 43 | -------------------------------------------------------------------------------- /frontend/src/components/EditorDesign/EditorDesign.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, IconButton, Toolbar } from '@mui/material'; 3 | import { 4 | FormatBold, 5 | FormatItalic, 6 | FormatUnderlined, 7 | Code, 8 | FormatQuote, 9 | Link, 10 | Image, 11 | FormatListBulleted, 12 | FormatListNumbered, 13 | } from '@mui/icons-material'; 14 | 15 | const EditorDesign = () => { 16 | return ( 17 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ); 57 | }; 58 | 59 | export default EditorDesign; 60 | -------------------------------------------------------------------------------- /frontend/src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Footer = () => { 4 | const currentYear = new Date().getFullYear(); 5 | 6 | return
©{currentYear} BlueWave labs Onboarding application
; 7 | }; 8 | 9 | export default Footer; 10 | -------------------------------------------------------------------------------- /frontend/src/components/Header/Header.css: -------------------------------------------------------------------------------- 1 | .top-banner { 2 | background-color: #ffffff; 3 | display: flex; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 10px 20px; 7 | height: 8vh; 8 | top: 0; 9 | left: 0; 10 | box-shadow: 0px 3px 3px -3px var(--shadow-one); 11 | box-shadow: 0px 4px 24px -4px var(--shadow-two); 12 | } 13 | 14 | .user-info { 15 | display: flex; 16 | align-items: center; 17 | margin-right: 20px; 18 | } 19 | 20 | .user-details { 21 | display: flex; 22 | flex-direction: column; 23 | } 24 | 25 | .user-name { 26 | font-size: var(--font-regular); 27 | font-weight: 600; 28 | line-height: 20px; 29 | color: var(--main-text-color); 30 | } 31 | 32 | .user-role { 33 | margin: 0; 34 | font-size: var(--font-regular); 35 | font-weight: 400; 36 | line-height: 20px; 37 | color: var(--main-text-color); 38 | } 39 | 40 | .logo { 41 | color: #ff834e; 42 | font-size: 20px; 43 | font-weight: 600; 44 | } 45 | 46 | .dropdown-button { 47 | background-color: transparent; 48 | border: none; 49 | cursor: pointer; 50 | padding: 0.4em 0.4em; 51 | margin: 1px; 52 | } 53 | -------------------------------------------------------------------------------- /frontend/src/components/Header/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import './Header.css'; 3 | import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined'; 4 | import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; 5 | import Avatar from '../Avatar/Avatar'; 6 | import DropdownMenu from './DropdownMenu/DropdownMenu'; 7 | import { useAuth } from '../../services/authProvider'; 8 | 9 | const Header = () => { 10 | const [isDropdownOpen, setIsDropdownOpen] = useState(false); 11 | const { userInfo } = useAuth(); 12 | 13 | const handleDropdownClick = () => { 14 | setIsDropdownOpen(!isDropdownOpen); 15 | }; 16 | 17 | return ( 18 |
19 |
GuideFox
20 |
21 | 22 |
23 |
{userInfo.fullName}
24 |
{userInfo.role}
25 |
26 | 36 |
37 |
38 | ); 39 | }; 40 | 41 | export default Header; 42 | -------------------------------------------------------------------------------- /frontend/src/components/Header/Logo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './LogoStyles.css'; 4 | 5 | const Logo = ({ logo }) => { 6 | if (!logo || !logo.src) { 7 | return null; 8 | } 9 | 10 | return ( 11 |
12 | {logo.alt} 17 |
18 | ); 19 | }; 20 | 21 | Logo.propTypes = { 22 | logo: PropTypes.shape({ 23 | src: PropTypes.string.isRequired, 24 | alt: PropTypes.string.isRequired, 25 | className: PropTypes.string, 26 | }).isRequired, 27 | }; 28 | 29 | export default Logo; 30 | -------------------------------------------------------------------------------- /frontend/src/components/Header/Title/Title.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Box, Typography } from '@mui/material'; 4 | import Button from '../../Button/Button'; 5 | import './TitleStyles.css'; 6 | 7 | const Title = ({ 8 | title, 9 | buttonText, 10 | onButtonClick, 11 | titleStyle, 12 | buttonStyle, 13 | children, 14 | }) => { 15 | return ( 16 |
17 | 18 | 23 | {title} 24 | 25 |
29 | ); 30 | }; 31 | 32 | Title.propTypes = { 33 | title: PropTypes.string.isRequired, 34 | buttonText: PropTypes.string.isRequired, 35 | onButtonClick: PropTypes.func.isRequired, 36 | titleStyle: PropTypes.object, 37 | buttonStyle: PropTypes.object, 38 | children: PropTypes.node, 39 | }; 40 | 41 | Title.defaultProps = { 42 | titleStyle: {}, 43 | buttonStyle: {}, 44 | }; 45 | 46 | export default Title; 47 | -------------------------------------------------------------------------------- /frontend/src/components/Header/Title/TitleStyles.css: -------------------------------------------------------------------------------- 1 | .titleContainer { 2 | padding: 60px; 3 | } 4 | .titleBanner { 5 | font-size: 1.5rem; 6 | font-weight: bold; 7 | line-height: 1.5; 8 | margin: 1.33em 0; 9 | } 10 | .titleHeader { 11 | display: flex; 12 | justify-content: space-between; 13 | align-items: center; 14 | margin-bottom: 1rem; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/components/HintPageComponents/HintLeftAppearance/HintLeftAppearance.css: -------------------------------------------------------------------------------- 1 | .hint-appearance-container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: flex-start; 5 | flex: 1; 6 | color: var(--main-text-color); 7 | } 8 | 9 | .hint-state-name { 10 | margin-top: 1.5rem; 11 | margin-bottom: 0.5rem; 12 | font-weight: 400; 13 | font-size: var(--font-regular); 14 | } 15 | 16 | .hint-appearance-color { 17 | display: flex; 18 | align-items: center; 19 | width: 241px; 20 | > div { 21 | width: 100%; 22 | } 23 | } 24 | 25 | .hint-appearance-error { 26 | position: absolute; 27 | font-family: var(--font-family-inter); 28 | font-size: var(--font-size-sm); 29 | font-weight: 400; 30 | line-height: 1.45; 31 | margin-top: 0px; 32 | color: var(--error-color); 33 | } 34 | 35 | .hint-appearance-item { 36 | position: relative; 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/components/HintPageComponents/HintLeftContent/HintLeftContent.css: -------------------------------------------------------------------------------- 1 | .left-content-container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: flex-start; 5 | flex: 1; 6 | gap: 5; 7 | color: var(--main-text-color); 8 | } 9 | 10 | .hint-label { 11 | font-weight: 400; 12 | font-size: var(--font-regular); 13 | margin-top: 0.5rem; 14 | } 15 | 16 | .error-message { 17 | font-family: var(--font-family-inter); 18 | font-size: var(--font-size-sm); 19 | font-weight: 400; 20 | line-height: 1.45; 21 | margin-top: -4px; 22 | color: var(--error-color); 23 | } 24 | 25 | .switch-style { 26 | margin-top: 2rem; 27 | display: flex; 28 | align-items: center; 29 | gap: 10px; 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/components/Links/DraggableHelperLink/DraggableHelperLink.module.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | cursor: pointer; 6 | margin-bottom: 1rem; 7 | 8 | &__container { 9 | border: 0.78px solid #ebebeb; 10 | border-radius: 7.8px; 11 | padding: 1.25rem; 12 | max-width: 320px; 13 | } 14 | 15 | &__secondaryButton { 16 | border: none; 17 | border-radius: 1rem; 18 | height: 30px; 19 | width: 30px; 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | z-index: 10; 24 | cursor: pointer; 25 | background-color: white; 26 | 27 | &:hover { 28 | background-color: var(--header-background); 29 | } 30 | 31 | &:disabled { 32 | cursor: not-allowed; 33 | } 34 | } 35 | 36 | &__title { 37 | margin-left: 0.5rem; 38 | flex-grow: 1; 39 | font-family: Inter; 40 | font-size: 0.875rem; 41 | font-weight: 400; 42 | line-height: 1.43; 43 | color: #344054; 44 | } 45 | 46 | &__icon { 47 | width: 12px; 48 | height: 13px; 49 | } 50 | 51 | & .MuiIconButton-root { 52 | color: #344054; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /frontend/src/components/Links/DraggableHelperLink/ListItemContainer.jsx: -------------------------------------------------------------------------------- 1 | import { List } from '@mui/material'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import s from './DraggableHelperLink.module.scss'; 5 | 6 | const ListItemContainer = ({ children }) => { 7 | return ( 8 |
9 | {children} 10 |
11 | ); 12 | }; 13 | 14 | ListItemContainer.propTypes = { 15 | children: PropTypes.node.isRequired, 16 | }; 17 | 18 | export default ListItemContainer; 19 | -------------------------------------------------------------------------------- /frontend/src/components/Links/Popup/Popup.module.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | width: 400; 7 | background-color: #fff; 8 | box-shadow: 0px 8px 8px -4px #10182808; 9 | box-shadow: 0px 20px 24px -4px #10182814; 10 | padding: 24px; 11 | border-radius: 12px; 12 | 13 | &__title { 14 | margin: 0 0 4px; 15 | font-family: Inter; 16 | font-size: 1rem; 17 | font-weight: 600; 18 | line-height: 1.75; 19 | } 20 | 21 | &__desc { 22 | margin: 0 0 24px; 23 | font-family: Inter; 24 | font-size: 0.813rem; 25 | font-weight: 400; 26 | line-height: 1.54; 27 | } 28 | 29 | &__btn { 30 | &--container { 31 | display: flex; 32 | justify-content: flex-end; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/components/LoadingPage/Loading.module.css: -------------------------------------------------------------------------------- 1 | @import url(../../styles/variables.css); 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | width: 100vw; 7 | height: 100vh; 8 | flex-grow: 1; 9 | } 10 | 11 | .loading-container { 12 | display: flex; 13 | flex-grow: 1; 14 | } 15 | 16 | .loading { 17 | display: flex; 18 | margin: auto; 19 | justify-content: center; 20 | } 21 | 22 | .loading::after { 23 | content: ''; 24 | width: 50px; 25 | height: 50px; 26 | border: 10px solid #dddddd; 27 | border-top-color: var(--main-purple); 28 | border-radius: 50%; 29 | animation: loading 1s ease infinite; 30 | } 31 | 32 | @keyframes loading { 33 | to { 34 | transform: rotate(1turn); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/components/LoadingPage/LoadingArea.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Loading.module.css'; 3 | 4 | const LoadingArea = () => { 5 | return
; 6 | }; 7 | 8 | export default LoadingArea; 9 | -------------------------------------------------------------------------------- /frontend/src/components/LoadingPage/LoadingPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Loading.module.css'; 3 | import LeftMenu from '../LeftMenu/LeftMenu'; 4 | 5 | const LoadingPage = () => { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 |
13 | ); 14 | }; 15 | 16 | export default LoadingPage; 17 | -------------------------------------------------------------------------------- /frontend/src/components/Logo/Logo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styles from './LogoStyles.module.css'; 4 | 5 | const Logo = ({ 6 | isSidebar = false, 7 | logoText = 'Guide', 8 | highlightText = 'Fox', 9 | }) => { 10 | const containerClass = isSidebar ? styles.sidebar : styles.logoContainer; 11 | 12 | return ( 13 |
14 | {logoText} 15 | {highlightText} 16 |
17 | ); 18 | }; 19 | 20 | Logo.propTypes = { 21 | isSidebar: PropTypes.bool, 22 | logoText: PropTypes.string, 23 | highlightText: PropTypes.string, 24 | }; 25 | 26 | export default Logo; 27 | -------------------------------------------------------------------------------- /frontend/src/components/Logo/LogoStyles.module.css: -------------------------------------------------------------------------------- 1 | .logoContainer { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | padding: 14px; 6 | border: 16px solid transparent; 7 | box-sizing: border-box; 8 | margin-top: 78px; 9 | font-family: 'Avenir', sans-serif; 10 | font-weight: 400; 11 | font-size: 40px; 12 | line-height: 64px; 13 | color: var(--main-text-color); 14 | } 15 | 16 | .sidebar { 17 | display: flex; 18 | margin-left: 3px; 19 | align-items: center; 20 | max-width: 200px; 21 | margin-top: 1rem; 22 | margin-bottom: 0.75rem; 23 | padding: 0 16px; 24 | box-sizing: border-box; 25 | font-family: 'Avenir', sans-serif; 26 | font-weight: 400; 27 | font-size: 20px; 28 | color: var(--main-text-color); 29 | } 30 | 31 | .logoText { 32 | text-align: left; 33 | display: flex; 34 | } 35 | 36 | .logoTextPurple { 37 | text-align: left; 38 | display: flex; 39 | color: var(--main-purple); 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/components/ParagraphCSS/ParagraphCSS.jsx: -------------------------------------------------------------------------------- 1 | import styles from './ParagraphCSS.module.scss'; 2 | 3 | const ParagraphCSS = () => { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 | ); 11 | }; 12 | export default ParagraphCSS; 13 | -------------------------------------------------------------------------------- /frontend/src/components/ParagraphCSS/ParagraphCSS.module.scss: -------------------------------------------------------------------------------- 1 | .bannerOne, 2 | .bannerTwo, 3 | .bannerThree { 4 | background-color: #f3f3f3; 5 | border-radius: 5px; 6 | margin-bottom: 1rem; 7 | } 8 | 9 | .bannerOne { 10 | background-color: #d9d9d9; 11 | width: 85px; 12 | height: 13px; 13 | margin-top: 1rem; 14 | } 15 | 16 | .bannerTwo { 17 | width: 220px; 18 | height: 44px; 19 | } 20 | 21 | .bannerThree { 22 | width: 220px; 23 | height: 32px; 24 | } 25 | .preview { 26 | background-color: white; 27 | border-radius: 10px; 28 | border: 1.23px solid var(--grey-border); 29 | padding: 0.5rem 0.7rem; 30 | display: flex; 31 | justify-self: center; 32 | flex-direction: column; 33 | width: auto; 34 | margin-bottom: 24px; 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/components/PopUpMessages/PopUpStyles.js: -------------------------------------------------------------------------------- 1 | export const popupStyles = { 2 | paper: { 3 | padding: '16px', 4 | }, 5 | title: { 6 | padding: 0, 7 | }, 8 | content: { 9 | paddingLeft: 0, 10 | }, 11 | contentText: { 12 | fontSize: '13px', 13 | }, 14 | actions: { 15 | paddingBottom: 0, 16 | paddingRight: 0, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /frontend/src/components/Private.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { Navigate, useLocation } from 'react-router-dom'; 4 | import { useAuth } from '../services/authProvider'; 5 | import LoadingPage from './LoadingPage/LoadingPage'; 6 | 7 | const Private = ({ Component }) => { 8 | const { isLoggedIn, isFetching } = useAuth(); 9 | const location = useLocation(); 10 | 11 | if (isFetching) return ; 12 | 13 | return isLoggedIn ? ( 14 | 15 | ) : ( 16 | 17 | ); 18 | }; 19 | 20 | export default Private; 21 | -------------------------------------------------------------------------------- /frontend/src/components/ProgressBar/ProgressBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import '../ProgressBar/styles.css'; 4 | 5 | const ProgressBar = ({ value = 30 }) => { 6 | const [percent, setPercent] = useState(value); 7 | useEffect(() => { 8 | setPercent(Math.min(100, Math.max(value, 0))); 9 | }, value); 10 | 11 | return ( 12 |
13 |
14 |
15 |
16 | {value.toFixed()}% 17 |
18 | ); 19 | }; 20 | 21 | ProgressBar.propTypes = { 22 | value: PropTypes.number, 23 | }; 24 | 25 | export default ProgressBar; 26 | -------------------------------------------------------------------------------- /frontend/src/components/ProgressBar/styles.css: -------------------------------------------------------------------------------- 1 | .app { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | gap: 30px; 6 | } 7 | 8 | .progress { 9 | height: 20px; 10 | width: 500px; 11 | background-color: rgb(233, 233, 233); 12 | border: 1px solid rgba(0, 0, 0, 0); 13 | border-radius: 20px; 14 | overflow: hidden; 15 | } 16 | 17 | .progress div { 18 | height: 100%; 19 | background-color: #7f55d9; 20 | color: white; 21 | border-radius: 20px; 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/components/RadioButton/RadioButtonStyles.module.css: -------------------------------------------------------------------------------- 1 | .radioButton { 2 | display: flex; 3 | align-items: center; 4 | cursor: pointer; 5 | white-space: nowrap; 6 | margin-bottom: 13px; 7 | } 8 | 9 | .label { 10 | color: var(--main-text-color); 11 | margin-left: 8px; 12 | white-space: nowrap; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/components/RichTextEditor/EditorLinkDialog/LinkDialog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import CustomTextField from '../../TextFieldComponents/CustomTextField/CustomTextField'; 4 | import PopUpMessages from '../../PopUpMessages/PopUpMessages'; 5 | 6 | const LinkDialog = ({ 7 | open, 8 | url = '', 9 | isLinkActive = false, 10 | setUrl = () => {}, 11 | handleClose = () => {}, 12 | handleInsertLink = () => {}, 13 | handleOpenLink = () => {}, 14 | }) => { 15 | const title = isLinkActive ? 'Edit link' : 'Add link'; 16 | return ( 17 | 27 | setUrl(e.target.value)} 32 | /> 33 | 34 | ); 35 | }; 36 | 37 | LinkDialog.propTypes = { 38 | url: PropTypes.string, 39 | isLinkActive: PropTypes.bool, 40 | open: PropTypes.bool.isRequired, 41 | setUrl: PropTypes.func.isRequired, 42 | handleClose: PropTypes.func.isRequired, 43 | handleInsertLink: PropTypes.func.isRequired, 44 | handleOpenLink: PropTypes.func.isRequired, 45 | }; 46 | 47 | export default LinkDialog; 48 | -------------------------------------------------------------------------------- /frontend/src/components/RichTextEditor/RichTextEditor.css: -------------------------------------------------------------------------------- 1 | .editor-container { 2 | border: 1px solid var(--light-border-color); 3 | color: var(--main-text-color); 4 | font-size: var(--font-regular); 5 | border-radius: 8px; 6 | padding-top: 0.5rem; 7 | margin-top: 1rem; 8 | width: 400px; 9 | padding-left: 0.5rem; 10 | box-sizing: border-box; 11 | } 12 | 13 | .ProseMirror { 14 | height: 7.5rem; 15 | padding: 0 14px; 16 | overflow-y: auto; 17 | } 18 | 19 | .ProseMirror p { 20 | margin: 0.5rem 0; 21 | } 22 | 23 | .ProseMirror:focus { 24 | border: none; 25 | outline: none; 26 | } 27 | 28 | .editor-label { 29 | color: var(--second-text-color); 30 | font-weight: 600; 31 | margin-bottom: 1rem; 32 | margin-left: 0; 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/components/RichTextEditor/Tabs/EditorTabs.css: -------------------------------------------------------------------------------- 1 | .editor-tabs { 2 | border: 1px solid var(--light-border-color); 3 | border-radius: 5px; 4 | width: fit-content; 5 | min-height: 34px !important; 6 | margin-top: 15px; 7 | } 8 | 9 | .editor-tabs button { 10 | min-height: 34px !important; 11 | } 12 | 13 | .editor-tabs.css-11keaam-MuiTabs-root { 14 | position: static !important; 15 | } 16 | 17 | .tab[aria-selected='true'] { 18 | background-color: var(--main-purple); 19 | color: white !important; 20 | } 21 | 22 | .tab { 23 | text-transform: none !important; 24 | font-size: var(--font-regular) !important; 25 | font-weight: 400 !important; 26 | line-height: 20px !important; 27 | padding: 0 !important; 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/components/RichTextEditor/Tabs/EditorTabs.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Tabs, Tab } from '@mui/material'; 4 | import './EditorTabs.css'; 5 | 6 | const EditorTabs = ({ mode, setMode, sx }) => ( 7 | setMode(newValue)} 11 | TabIndicatorProps={{ 12 | style: { 13 | display: 'none', 14 | }, 15 | }} 16 | sx={sx} 17 | > 18 | 19 | 20 | 21 | ); 22 | 23 | EditorTabs.propTypes = { 24 | mode: PropTypes.string, 25 | setMode: PropTypes.func, 26 | sx: PropTypes.object, 27 | }; 28 | 29 | export default EditorTabs; 30 | -------------------------------------------------------------------------------- /frontend/src/components/RichTextEditor/Toolbar/EditorToolbar.css: -------------------------------------------------------------------------------- 1 | .toolbar-container { 2 | display: flex; 3 | gap: 1px; 4 | align-items: center; 5 | width: 70%; 6 | flex-wrap: wrap; 7 | justify-content: space-between; 8 | } 9 | 10 | .toolbar-container button { 11 | color: var(--second-text-color); 12 | } 13 | 14 | .toolbar-container button[disabled] { 15 | color: var(--light-border-color); 16 | } 17 | 18 | .toolbar-container .MuiButtonBase-root { 19 | min-width: 0 !important; 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/components/Switch/Switch.css: -------------------------------------------------------------------------------- 1 | /* The switch - the box around the slider */ 2 | .switch { 3 | position: relative; 4 | display: inline-block; 5 | width: 36px; 6 | height: 20px; 7 | } 8 | 9 | /* Hide default HTML checkbox */ 10 | .switch input { 11 | opacity: 0; 12 | width: 0; 13 | height: 0; 14 | } 15 | 16 | /* The slider */ 17 | .slider { 18 | position: absolute; 19 | cursor: pointer; 20 | top: 0; 21 | left: 0; 22 | right: 0; 23 | bottom: 0; 24 | background-color: #ccc; 25 | -webkit-transition: 0.4s; 26 | transition: 0.4s; 27 | } 28 | 29 | .slider:before { 30 | position: absolute; 31 | content: ''; 32 | height: 16px; 33 | width: 16px; 34 | left: 2px; 35 | bottom: 2px; 36 | background-color: white; 37 | -webkit-transition: 0.4s; 38 | transition: 0.4s; 39 | } 40 | 41 | input:checked + .slider { 42 | background-color: var(--main-purple); 43 | } 44 | 45 | /* 46 | input:focus + .slider { 47 | box-shadow: 0 0 1px var(--main-purple); 48 | } 49 | */ 50 | 51 | input:checked + .slider:before { 52 | -webkit-transform: translateX(15.6px); 53 | -ms-transform: translateX(15.6px); 54 | transform: translateX(15.6px); 55 | } 56 | 57 | input:disabled + .slider { 58 | background-color: #f2f4f7; 59 | } 60 | 61 | /* Rounded sliders */ 62 | .slider.round { 63 | border-radius: 34px; 64 | } 65 | 66 | .slider.round:before { 67 | border-radius: 50%; 68 | } 69 | -------------------------------------------------------------------------------- /frontend/src/components/Switch/Switch.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import './Switch.css'; 3 | 4 | /** 5 | * Switch components for both HRM and Onboarding applications. 6 | * 7 | * Props: 8 | * - id: Standard input id attribute 9 | * 10 | * - name: Standard input name attribute 11 | * 12 | * - value: Standard input value attribute 13 | */ 14 | export default function Switch({ id, name, value, enabled = true, onChange }) { 15 | return ( 16 | 28 | ); 29 | } 30 | 31 | Switch.propTypes = { 32 | enabled: PropTypes.bool, 33 | id: PropTypes.string, 34 | name: PropTypes.string, 35 | value: PropTypes.bool, 36 | onChange: PropTypes.func, 37 | }; 38 | -------------------------------------------------------------------------------- /frontend/src/components/Table/Table.module.css: -------------------------------------------------------------------------------- 1 | .tableHeader { 2 | background-color: rgba(250, 250, 250, 1); 3 | } 4 | 5 | .heading { 6 | color: var(--second-text-color) !important ; 7 | font-size: 12px; 8 | } 9 | 10 | .data { 11 | font-size: var(--font-regular); 12 | color: var(--second-text-color) !important; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/components/TextFieldComponents/Chips/ChipAdornment.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Box, Chip } from '@mui/material'; 4 | 5 | const ChipAdornment = ({ chips }) => ( 6 | 7 | {chips.map((chip, index) => ( 8 | 15 | ))} 16 | 17 | ); 18 | 19 | ChipAdornment.propTypes = { 20 | chips: PropTypes.arrayOf( 21 | PropTypes.shape({ 22 | label: PropTypes.string, 23 | onDelete: PropTypes.func, 24 | }) 25 | ).isRequired, 26 | }; 27 | 28 | export default ChipAdornment; 29 | -------------------------------------------------------------------------------- /frontend/src/components/TextFieldComponents/CustomTextField/CustomTextFieldStyles.css: -------------------------------------------------------------------------------- 1 | .textField .MuiSvgIcon-colorError { 2 | color: var(--border-error-solid); 3 | } 4 | 5 | .textField svg { 6 | color: var(--light-border-color); 7 | } 8 | 9 | .textField .MuiInputBase-input { 10 | color: var(--main-text-color); 11 | } 12 | 13 | .textField .MuiDivider-root { 14 | margin: 0 10px; 15 | } 16 | 17 | .textField .MuiOutlinedInput-root { 18 | font-size: var(--font-regular); 19 | box-shadow: 0px 1px 2px 0px #1018280d; 20 | } 21 | 22 | .textField .MuiOutlinedInput-root { 23 | &:hover fieldset { 24 | border-color: var(--main-purple); 25 | } 26 | &.Mui-focused fieldset { 27 | border-color: var(--main-purple); 28 | } 29 | border-radius: 8px !important; 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/components/TextFieldComponents/TextFieldComponents.css: -------------------------------------------------------------------------------- 1 | .textField-container { 2 | display: flex; 3 | flex-wrap: wrap; 4 | gap: 3; 5 | padding: 50px; 6 | } 7 | 8 | .textField-section-left, 9 | .textField-section-right { 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | flex: 1; 14 | gap: 20px; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/components/Toast/Toast.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import ToastItem from './ToastItem'; 3 | import { defaultToastOptions, MAXIMUM_TOAST_COUNT } from './constant'; 4 | import toastEmitter, { TOAST_EMITTER_KEY } from '../../utils/toastEmitter'; 5 | import styles from './Toast.module.scss'; 6 | 7 | const Toast = () => { 8 | const [toasts, setToasts] = useState([]); 9 | 10 | useEffect(() => { 11 | const handleNewToast = (toastMessage) => { 12 | const newToast = { 13 | id: `${Date.now()}-${Math.random()}`, 14 | message: toastMessage, 15 | duration: defaultToastOptions.duration, 16 | }; 17 | setToasts((prevToasts) => { 18 | const updatedToasts = [...prevToasts, newToast]; 19 | return updatedToasts.length > MAXIMUM_TOAST_COUNT 20 | ? updatedToasts.slice(1) 21 | : updatedToasts; 22 | }); 23 | }; 24 | 25 | toastEmitter.on(TOAST_EMITTER_KEY, handleNewToast); 26 | 27 | return () => { 28 | toastEmitter.off(TOAST_EMITTER_KEY, handleNewToast); 29 | }; 30 | }, []); 31 | 32 | const handleRemoveToast = (id) => { 33 | setToasts((prevToasts) => prevToasts.filter((toast) => toast.id !== id)); 34 | }; 35 | 36 | return ( 37 |
38 | {toasts.map((toast) => ( 39 | 44 | ))} 45 |
46 | ); 47 | }; 48 | 49 | export default Toast; 50 | -------------------------------------------------------------------------------- /frontend/src/components/Toast/Toast.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/globals.scss' as *; 2 | 3 | .toastContainer { 4 | position: fixed; 5 | bottom: 60px; 6 | right: 48px; 7 | z-index: 9999; 8 | display: flex; 9 | flex-direction: column; 10 | gap: 10px; 11 | pointer-events: none; 12 | } 13 | 14 | .toast { 15 | background-color: white; 16 | border: 1px solid var(--light-border-color); 17 | font-size: var(--font-regular); 18 | line-height: 20px; 19 | color: var(--third-text-color); 20 | padding: 16px; 21 | border-radius: 5px; 22 | opacity: 0; 23 | transform: translateX(100%); 24 | transition: all 0.5s ease-out; 25 | pointer-events: all; 26 | max-width: 534px; 27 | word-wrap: break-word; 28 | box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); 29 | display: flex; 30 | align-items: center; 31 | gap: 10px; 32 | } 33 | 34 | .text { 35 | margin: 0; 36 | } 37 | 38 | .slideIn { 39 | transform: translateX(0); 40 | opacity: 1; 41 | } 42 | 43 | .slideOut { 44 | transform: translateX(100%); 45 | opacity: 0; 46 | } 47 | 48 | .icon { 49 | cursor: pointer; 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/components/Toast/ToastItem.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classNames from 'classnames'; 4 | import CloseIcon from '@mui/icons-material/Close'; 5 | import styles from './Toast.module.scss'; 6 | 7 | const ToastItem = ({ toast, removeToast }) => { 8 | const [isVisible, setIsVisible] = useState(true); 9 | 10 | useEffect(() => { 11 | const timeoutId = setTimeout(() => { 12 | setIsVisible(false); 13 | setTimeout(() => { 14 | removeToast(toast.id); 15 | }, 500); 16 | }, toast.duration || 3000); 17 | 18 | return () => { 19 | clearTimeout(timeoutId); 20 | }; 21 | }, [toast, removeToast]); 22 | 23 | const handleClose = () => { 24 | setIsVisible(false); 25 | setTimeout(() => { 26 | removeToast(toast.id); 27 | }, 500); 28 | }; 29 | 30 | return ( 31 |
37 |

{toast.message}

38 | 43 |
44 | ); 45 | }; 46 | 47 | ToastItem.propTypes = { 48 | toast: PropTypes.shape({ 49 | id: PropTypes.string.isRequired, 50 | message: PropTypes.string.isRequired, 51 | duration: PropTypes.number, 52 | }).isRequired, 53 | removeToast: PropTypes.func.isRequired, 54 | }; 55 | 56 | export default ToastItem; 57 | -------------------------------------------------------------------------------- /frontend/src/components/Toast/constant.js: -------------------------------------------------------------------------------- 1 | export const defaultToastOptions = { 2 | duration: 3000, 3 | top: '110px', 4 | }; 5 | 6 | export const MAXIMUM_TOAST_COUNT = 8; 7 | -------------------------------------------------------------------------------- /frontend/src/components/ToolTips/ToolTips.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Tooltips = () => { 4 | return
Hello
; 5 | }; 6 | 7 | export default Tooltips; 8 | -------------------------------------------------------------------------------- /frontend/src/components/Tour/DraggableTourStep/DraggableTourStep.module.scss: -------------------------------------------------------------------------------- 1 | .stepContainer { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | padding: 8px 0px 8px 12px; 6 | background-color: var(--header-background); 7 | border: 1.23px solid var(--light-border-color); 8 | border-radius: 8px; 9 | 10 | &__isActive { 11 | border: 1.23px solid var(--main-purple); 12 | } 13 | 14 | &__button { 15 | border: none; 16 | border-radius: 1rem; 17 | height: 30px; 18 | width: 30px; 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | z-index: 10; 23 | cursor: pointer; 24 | background-color: var(--header-background); 25 | 26 | &:hover { 27 | background-color: var(--gray-200); 28 | } 29 | 30 | &:disabled { 31 | cursor: not-allowed; 32 | } 33 | } 34 | 35 | &__customInput { 36 | padding: 3px 0.5rem; 37 | border-radius: 8px; 38 | font-size: 13px; 39 | color: var(--main-text-color); 40 | cursor: text; 41 | outline: none; 42 | width: 100px; 43 | border: 1px solid transparent; 44 | background-color: var(--header-background); 45 | 46 | &:focus { 47 | border: 1px solid var(--light-border-color); 48 | background-color: white; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /frontend/src/components/UserProfileSidebar/UserProfileSidebar.module.css: -------------------------------------------------------------------------------- 1 | .user-info { 2 | display: flex; 3 | width: 100%; 4 | max-width: 100%; 5 | padding-bottom: 1rem; 6 | padding-left: 0.8rem; 7 | } 8 | 9 | .user-details-container { 10 | display: flex; 11 | } 12 | 13 | .user-details { 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | 18 | .user-name { 19 | font-size: var(--font-regular); 20 | font-weight: 600; 21 | line-height: 20px; 22 | color: var(--main-text-color); 23 | white-space: nowrap; 24 | overflow: hidden; 25 | text-overflow: ellipsis; 26 | max-width: 7rem; 27 | width: 100%; 28 | display: block; 29 | } 30 | 31 | .user-role { 32 | margin: 0; 33 | font-size: var(--font-regular); 34 | font-weight: 400; 35 | line-height: 20px; 36 | color: var(--main-text-color); 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/data/mockData.js: -------------------------------------------------------------------------------- 1 | export const mockDataUser = [ 2 | { 3 | id: 1, 4 | name: 'Jon Snow', 5 | email: 'jonsnow@gmail.com', 6 | }, 7 | { 8 | id: 2, 9 | name: 'Cersei Lannister', 10 | email: 'cerseilannister@gmail.com', 11 | }, 12 | { 13 | id: 3, 14 | name: 'Jaime Lannister', 15 | email: 'jaimelannister@gmail.com', 16 | }, 17 | { 18 | id: 4, 19 | name: 'Anya Stark', 20 | email: 'anyastark@gmail.com', 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /frontend/src/data/stepData.js: -------------------------------------------------------------------------------- 1 | export const stepData = [ 2 | { 3 | title: 'Your Details', 4 | explanation: 'Please provide your name and email.', 5 | }, 6 | { 7 | title: 'Company Details', 8 | explanation: 'A few details about your company.', 9 | }, 10 | { 11 | title: 'Invite Your Team', 12 | explanation: 'Start collaborating with your team.', 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap'); 2 | 3 | html, 4 | body, 5 | #root { 6 | height: 100%; 7 | width: 100%; 8 | font-family: 'Inter', sans-serif; 9 | background-image: none; 10 | margin: 0; 11 | padding: 0; 12 | display: flex; 13 | flex-direction: column; 14 | } 15 | 16 | .app { 17 | display: flex; 18 | flex-direction: column; 19 | height: 100%; 20 | flex-grow: 1; 21 | } 22 | 23 | .content { 24 | display: flex; 25 | flex-direction: row; 26 | flex-grow: 1; 27 | } 28 | 29 | .dashboard { 30 | flex-grow: 1; 31 | overflow-y: auto; 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App.jsx'; 4 | import './index.css'; 5 | import { BrowserRouter as Router } from 'react-router-dom'; 6 | import { lightTheme } from './assets/theme'; 7 | import { ThemeProvider } from '@mui/material'; 8 | import { AuthProvider } from './services/authProvider.jsx'; 9 | import { GuideTemplateProvider } from './templates/GuideTemplate/GuideTemplateContext'; 10 | import Toast from '@components/Toast/Toast.jsx'; 11 | 12 | ReactDOM.createRoot(document.getElementById('root')).render( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /frontend/src/products/Banner/BannerComponent.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: fit-content; 3 | display: inline-flex; 4 | gap: 1rem; 5 | border-radius: 4px; 6 | padding: 16px 30px; 7 | box-shadow: 0px 2px 7px 0px #0000001f; 8 | cursor: pointer; 9 | align-items: center; 10 | } 11 | 12 | .text { 13 | font-size: 16px; 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/products/Hint/HintComponent.css: -------------------------------------------------------------------------------- 1 | .preview-container { 2 | width: 100%; 3 | min-height: 250px; 4 | border: 1px solid var(--light-border-color); 5 | margin-top: 1rem; 6 | overflow: auto; 7 | display: flex; 8 | flex-direction: column; 9 | word-wrap: break-word; 10 | box-sizing: border-box; 11 | box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2); 12 | } 13 | 14 | .preview-container h3 { 15 | font-size: 20px; 16 | font-weight: 600; 17 | line-height: 30px; 18 | text-align: left; 19 | padding: 0 2rem; 20 | color: var(--preview-header-color); 21 | margin-bottom: 8px; 22 | margin-top: 24px; 23 | } 24 | 25 | .preview-content-container { 26 | color: var(--main-text-color); 27 | justify-content: space-between; 28 | display: flex; 29 | flex-direction: column; 30 | box-sizing: border-box; 31 | min-height: 170px; 32 | padding: 0 2rem; 33 | font-size: 13px; 34 | } 35 | 36 | .preview-content p { 37 | margin: 0; 38 | line-height: 24px; 39 | } 40 | 41 | .preview-content { 42 | word-wrap: break-word; 43 | overflow-wrap: break-word; 44 | } 45 | 46 | .preview-button-container { 47 | margin-top: 0.5rem; 48 | display: flex; 49 | justify-content: flex-end; 50 | } 51 | 52 | .preview-button-container button { 53 | margin: 0; 54 | } 55 | -------------------------------------------------------------------------------- /frontend/src/scenes/banner/BannerPageComponents/BannerLeftAppearance/BannerLeftAppearance.module.css: -------------------------------------------------------------------------------- 1 | .bannerAppearanceContainer { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: flex-start; 5 | flex: 1; 6 | color: var(--main-text-color); 7 | } 8 | 9 | .bannerStateName { 10 | margin-top: 1.5rem; 11 | margin-bottom: 0.5rem; 12 | font-weight: 400; 13 | font-size: var(--font-regular); 14 | } 15 | 16 | .bannerAppearanceColor { 17 | display: flex; 18 | align-items: center; 19 | width: 241px; 20 | } 21 | 22 | .bannerAppearanceColor > div { 23 | width: 100%; 24 | } 25 | 26 | .bannerAppearanceError { 27 | position: absolute; 28 | font-family: var(--font-family-inter); 29 | font-size: var(--font-size-sm); 30 | font-weight: 400; 31 | line-height: 1.45; 32 | margin-top: 2px; 33 | color: var(--error-color); 34 | } 35 | 36 | .bannerAppearanceItem { 37 | position: relative; 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/scenes/banner/BannerPageComponents/BannerLeftAppearance/BannerLeftApperance.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../styles/globals.scss' as *; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: flex-start; 7 | flex: 1; 8 | h2 { 9 | @include text-style(regular); 10 | margin-top: 1.5rem; 11 | margin-bottom: 10px; 12 | } 13 | .color { 14 | display: flex; 15 | align-items: center; 16 | width: 241px; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/scenes/banner/BannerPageComponents/BannerLeftContent/BannerLeftContent.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../styles/globals.scss' as *; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: flex-start; 7 | flex: 1; 8 | h2 { 9 | @include text-style(regular); 10 | margin-top: 1.5rem; 11 | margin-bottom: 0; 12 | } 13 | h3 { 14 | @include text-style(regular); 15 | } 16 | .radioContent { 17 | display: flex; 18 | font-size: 13px; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/scenes/banner/BannerPageComponents/BannerPreview/BannerPreview.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../styles/globals.scss' as *; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | margin-top: 1.5rem; 8 | margin-left: 3rem; 9 | margin-right: 3rem; 10 | flex: 2; 11 | min-width: 450px; 12 | 13 | .bannertext { 14 | border: none; 15 | background: transparent; 16 | width: 100%; 17 | text-align: center; 18 | } 19 | 20 | .bannertext:focus { 21 | outline: none; 22 | } 23 | 24 | .preview { 25 | border-radius: 10px; 26 | border: 1.23px solid var(--grey-border); 27 | width: 100%; 28 | padding: 1rem 1.5rem; 29 | } 30 | 31 | h2 { 32 | @include text-style(regular); 33 | margin-bottom: 0.7rem; 34 | margin-top: 0; 35 | } 36 | 37 | .banner { 38 | @include text-style(regular); 39 | background-color: var(--light-purple-background); 40 | display: flex; 41 | align-items: center; 42 | justify-content: space-between; 43 | padding: 0.7rem; 44 | border-radius: 5px; 45 | } 46 | 47 | .bannerOne, 48 | .bannerTwo, 49 | .bannerThree, 50 | .bannerFour { 51 | background-color: #f3f3f3; 52 | border-radius: 5px; 53 | margin-bottom: 1rem; 54 | } 55 | 56 | .bannerOne { 57 | width: 40%; 58 | height: 2rem; 59 | margin-top: 1rem; 60 | } 61 | 62 | .bannerTwo { 63 | width: 80%; 64 | height: 5.4rem; 65 | } 66 | 67 | .bannerThree { 68 | width: 95%; 69 | height: 4rem; 70 | } 71 | 72 | .bannerFour { 73 | width: 95%; 74 | height: 3rem; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /frontend/src/scenes/banner/BannerPageComponents/BannerPreview/BannerSkeleton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './BannerPreview.module.scss'; 3 | 4 | export default function BannerSkeleton() { 5 | return ( 6 | <> 7 |
8 |
9 |
10 |
11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/scenes/dashboard/Dashboard.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | padding: 3rem; 6 | gap: 1.5rem; 7 | 8 | .top { 9 | display: flex; 10 | justify-content: space-between; 11 | } 12 | 13 | .text { 14 | font-size: var(--font-header); 15 | font-weight: 400; 16 | line-height: 38px; 17 | color: #344054; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/scenes/dashboard/HomePageComponents/CreateActivityButton/ActivityButtonStyles.js: -------------------------------------------------------------------------------- 1 | export const activityButtonStyles = { 2 | display: 'flex', 3 | fontWeight: 400, 4 | aspectRatio: 1.75 / 1, 5 | backgroundColor: 'var(--gray-50)', 6 | width: '100%', 7 | border: '1px solid var(--grey-border)', 8 | borderRadius: 'var(--radius-button)', 9 | color: 'var(--gray-400)', 10 | padding: '1.5% 3%', 11 | flexDirection: 'column', 12 | alignItems: 'center', 13 | justifyContent: 'center', 14 | transition: 'box-shadow 0.3s ease', 15 | textTransform: 'none', 16 | '&:hover': { 17 | border: '1px solid var(--gray-250)', 18 | backgroundColor: 'var(--gray-100)', 19 | '.childSkeleton': { 20 | border: '1px solid var(--blue-300)', 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /frontend/src/scenes/dashboard/HomePageComponents/CreateActivityButton/CreateActivityButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Button from '@mui/material/Button'; 4 | import { activityButtonStyles } from './ActivityButtonStyles'; 5 | 6 | const CreateActivityButton = ({ 7 | placeholder = '', 8 | skeletonType, 9 | onClick = () => {}, 10 | }) => { 11 | return ( 12 | 20 | ); 21 | }; 22 | 23 | CreateActivityButton.propTypes = { 24 | skeletonType: PropTypes.node, 25 | placeholder: PropTypes.string, 26 | onClick: PropTypes.func.isRequired, 27 | }; 28 | 29 | export default CreateActivityButton; 30 | -------------------------------------------------------------------------------- /frontend/src/scenes/dashboard/HomePageComponents/CreateActivityButtonList/CreateActivityButtonList.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import CreateActivityButton from '../CreateActivityButton/CreateActivityButton'; 3 | import styles from './CreateActivityButtonList.module.scss'; 4 | 5 | const CreateActivityButtonList = ({ buttons }) => { 6 | return ( 7 |
8 | {buttons.map((button, index) => ( 9 | 10 | ))} 11 |
12 | ); 13 | }; 14 | 15 | CreateActivityButtonList.propTypes = { 16 | buttons: PropTypes.array.isRequired, 17 | }; 18 | 19 | export default CreateActivityButtonList; 20 | -------------------------------------------------------------------------------- /frontend/src/scenes/dashboard/HomePageComponents/CreateActivityButtonList/CreateActivityButtonList.module.scss: -------------------------------------------------------------------------------- 1 | .activityButtons { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: space-between; 5 | gap: 2rem; 6 | } 7 | 8 | @media (max-width: 1024px) { 9 | .activityButtons { 10 | gap: 1rem; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/scenes/dashboard/HomePageComponents/DateDisplay/DateDisplay.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './DateDisplay.module.scss'; 3 | 4 | const DateDisplay = () => { 5 | const currentDate = new Date(); 6 | 7 | const daysOfWeek = [ 8 | 'Sunday', 9 | 'Monday', 10 | 'Tuesday', 11 | 'Wednesday', 12 | 'Thursday', 13 | 'Friday', 14 | 'Saturday', 15 | ]; 16 | const currentDayOfWeek = daysOfWeek[currentDate.getDay()]; 17 | 18 | const options = { year: 'numeric', month: 'long', day: 'numeric' }; 19 | const currentDateFormatted = currentDate.toLocaleDateString( 20 | undefined, 21 | options 22 | ); 23 | 24 | return ( 25 |
26 | Today is {currentDayOfWeek}, {currentDateFormatted} 27 |
28 | ); 29 | }; 30 | 31 | export default DateDisplay; 32 | -------------------------------------------------------------------------------- /frontend/src/scenes/dashboard/HomePageComponents/DateDisplay/DateDisplay.module.scss: -------------------------------------------------------------------------------- 1 | .date { 2 | font-family: Inter; 3 | font-size: var(--font-regular); 4 | font-weight: 400; 5 | line-height: 24px; 6 | text-align: left; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/scenes/dashboard/HomePageComponents/Skeletons/BannerSkeleton.jsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '@mui/material'; 2 | import styles from './Skeleton.module.scss'; 3 | import { commonSkeletonStyles } from './BaseSkeletonStyles'; 4 | 5 | const BannerSkeleton = () => { 6 | return ( 7 |
8 | 19 | 26 | 33 | 34 | 48 |
49 | ); 50 | }; 51 | 52 | export default BannerSkeleton; 53 | -------------------------------------------------------------------------------- /frontend/src/scenes/dashboard/HomePageComponents/Skeletons/BaseSkeleton.jsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '@mui/material'; 2 | import PropTypes from 'prop-types'; 3 | import styles from './Skeleton.module.scss'; 4 | import { baseSkeletonStyles, commonSkeletonStyles } from './BaseSkeletonStyles'; 5 | 6 | const BaseSkeleton = ({ guideType, items = 4, children }) => { 7 | const guideTypeStyles = baseSkeletonStyles[guideType] || {}; 8 | 9 | return ( 10 |
11 | {[...Array(items)].map((_, index) => ( 12 | 20 | ))} 21 | 22 |
23 | 33 | {children} 34 |
35 |
36 | ); 37 | }; 38 | 39 | BaseSkeleton.propTypes = { 40 | guideType: PropTypes.string.isRequired, 41 | items: PropTypes.number, 42 | children: PropTypes.node, 43 | }; 44 | 45 | export default BaseSkeleton; 46 | -------------------------------------------------------------------------------- /frontend/src/scenes/dashboard/HomePageComponents/Skeletons/BaseSkeletonStyles.js: -------------------------------------------------------------------------------- 1 | export const baseSkeletonStyles = { 2 | popup: { 3 | position: 'absolute', 4 | top: '15%', 5 | left: '15%', 6 | width: '65%', 7 | height: '50%', 8 | }, 9 | helperLink: { 10 | position: 'absolute', 11 | bottom: '7%', 12 | right: '5%', 13 | width: '60%', 14 | height: '60%', 15 | transform: 'translate(40%, 40%)', 16 | }, 17 | hint: { 18 | position: 'absolute', 19 | top: '25%', 20 | left: '22%', 21 | width: '80%', 22 | height: '70%', 23 | }, 24 | tour: { 25 | position: 'absolute', 26 | top: '10%', 27 | left: '10%', 28 | width: '60%', 29 | height: '55%', 30 | }, 31 | }; 32 | 33 | export const commonSkeletonStyles = { 34 | bgcolor: 'var(--gray-200)', 35 | borderRadius: 'var(--radius-skeleton)', 36 | }; 37 | -------------------------------------------------------------------------------- /frontend/src/scenes/dashboard/HomePageComponents/Skeletons/HintSkeleton.jsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '@mui/material'; 2 | import BaseSkeleton from './BaseSkeleton'; 3 | 4 | const HintSkeleton = () => ( 5 | 6 | 19 | 20 | ); 21 | 22 | export default HintSkeleton; 23 | -------------------------------------------------------------------------------- /frontend/src/scenes/dashboard/HomePageComponents/Skeletons/Skeleton.module.scss: -------------------------------------------------------------------------------- 1 | .skeletonContainer { 2 | position: relative; 3 | width: 100%; 4 | height: 85%; 5 | background-color: white; 6 | border-radius: var(--radius-card); 7 | border: 1.23px solid var(--grey-border); 8 | padding: 5%; 9 | display: flex; 10 | justify-content: space-between; 11 | flex-direction: column; 12 | gap: 8px; 13 | margin-bottom: 5%; 14 | } 15 | 16 | .bannerSkeletonContainer { 17 | @extend .skeletonContainer; 18 | padding-top: 18%; 19 | justify-content: center; 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/scenes/dashboard/HomePageComponents/Skeletons/TourSkeleton.jsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '@mui/material'; 2 | import BaseSkeleton from './BaseSkeleton'; 3 | import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; 4 | 5 | const TourSkeleton = () => ( 6 | 7 | <> 8 | 19 | 32 | 33 | 34 | ); 35 | 36 | export default TourSkeleton; 37 | -------------------------------------------------------------------------------- /frontend/src/scenes/dashboard/HomePageComponents/StatisticCards/StatisticCards.jsx: -------------------------------------------------------------------------------- 1 | import ArrowDownwardRoundedIcon from '@mui/icons-material/ArrowDownwardRounded'; 2 | import ArrowUpwardRoundedIcon from '@mui/icons-material/ArrowUpwardRounded'; 3 | import PropTypes from 'prop-types'; 4 | import React from 'react'; 5 | import styles from './StatisticCards.module.scss'; 6 | 7 | const StatisticCard = ({ metricName, metricValue = 0, changeRate = 0 }) => { 8 | const getChangeRate = () => { 9 | if (changeRate === 0) return 'N/A'; 10 | return Math.abs(changeRate) + '%'; 11 | }; 12 | 13 | const getRateColor = () => { 14 | if (changeRate === 0) return 'inherit'; 15 | return changeRate >= 0 ? 'var(--green-400)' : 'var(--red-500)'; 16 | }; 17 | 18 | return ( 19 |
20 |
{metricName}
21 |
{metricValue}
22 |
23 | 24 | {changeRate !== 0 && 25 | (changeRate >= 0 ? ( 26 | 27 | ) : ( 28 | 29 | ))} 30 | {getChangeRate()} 31 | 32 |  vs last month 33 |
34 |
35 | ); 36 | }; 37 | 38 | StatisticCard.propTypes = { 39 | metricName: PropTypes.string.isRequired, 40 | metricValue: PropTypes.number.isRequired, 41 | changeRate: PropTypes.number.isRequired, 42 | }; 43 | 44 | export default StatisticCard; 45 | -------------------------------------------------------------------------------- /frontend/src/scenes/dashboard/HomePageComponents/StatisticCards/StatisticCards.module.scss: -------------------------------------------------------------------------------- 1 | @import url('../../../../styles/variables.css'); 2 | 3 | .statisticCard { 4 | border: 1px solid var(--light-gray); 5 | border-radius: 10px; 6 | padding: 24px; 7 | gap: 10px; 8 | display: flex; 9 | flex-direction: column; 10 | width: 100%; 11 | box-shadow: 0px 1px 2px 0px #1018280d; 12 | 13 | .metricName { 14 | font-size: var(--font-header); 15 | color: var(--gray-350); 16 | font-weight: 400; 17 | line-height: 24px; 18 | margin-bottom: 13px; 19 | } 20 | 21 | .metricValue { 22 | color: var(--gray-500); 23 | font-size: 36px; 24 | font-weight: 600; 25 | line-height: 44px; 26 | letter-spacing: -0.02em; 27 | } 28 | 29 | .changeRate { 30 | font-size: var(--font-regular); 31 | font-weight: 400; 32 | line-height: 20px; 33 | color: var(--third-text-color); 34 | align-items: center; 35 | flex-direction: row; 36 | display: flex; 37 | } 38 | .change { 39 | align-items: center; 40 | display: flex; 41 | font-family: Inter; 42 | font-size: 14px; 43 | font-weight: 600; 44 | line-height: 20px; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /frontend/src/scenes/dashboard/HomePageComponents/StatisticCardsList/StatisticCardsList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import StatisticCard from '../StatisticCards/StatisticCards'; 4 | import styles from './StatisticCardsList.module.scss'; 5 | 6 | const StatisticCardList = ({ metrics }) => { 7 | return ( 8 |
9 | {metrics.map((metric, index) => ( 10 | 16 | ))} 17 |
18 | ); 19 | }; 20 | 21 | StatisticCardList.propTypes = { 22 | metrics: PropTypes.array.isRequired, 23 | }; 24 | 25 | export default StatisticCardList; 26 | -------------------------------------------------------------------------------- /frontend/src/scenes/dashboard/HomePageComponents/StatisticCardsList/StatisticCardsList.module.scss: -------------------------------------------------------------------------------- 1 | .statisticCards { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: space-between; 5 | gap: 2rem; 6 | } 7 | 8 | @media (max-width: 1024px) { 9 | .statisticCards { 10 | gap: 1rem; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/scenes/dashboard/HomePageComponents/UserTitle/UserTitle.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styles from './UserTitle.module.scss'; 4 | 5 | const UserTitle = ({ name }) => { 6 | return
Hello, {name}
; 7 | }; 8 | 9 | UserTitle.propTypes = { 10 | name: PropTypes.string.isRequired, 11 | }; 12 | 13 | export default UserTitle; 14 | -------------------------------------------------------------------------------- /frontend/src/scenes/dashboard/HomePageComponents/UserTitle/UserTitle.module.scss: -------------------------------------------------------------------------------- 1 | .title { 2 | font-size: 24px; 3 | font-weight: 600; 4 | line-height: 38px; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/scenes/errors/403.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ErrorComponent } from './Error'; 3 | import { NAVIGATE_403_URL, TEXT_403 } from './constant'; 4 | import { useNavigate } from 'react-router-dom'; 5 | 6 | export const Error403 = () => { 7 | const navigate = useNavigate(); 8 | 9 | return ( 10 | navigate(NAVIGATE_403_URL)} 13 | /> 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/src/scenes/errors/404.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ErrorComponent } from './Error'; 3 | import { NAVIGATE_404_URL, TEXT_404 } from './constant'; 4 | import { useNavigate } from 'react-router'; 5 | 6 | export const Error404 = () => { 7 | const navigate = useNavigate(); 8 | 9 | return ( 10 | navigate(NAVIGATE_404_URL)} 13 | /> 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/src/scenes/errors/Error.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styles from './Error.module.scss'; 4 | import Button from '@components/Button/Button'; 5 | 6 | export const ErrorComponent = ({ text, errorAction }) => { 7 | const errorButtonStyle = { 8 | borderRadius: '8px', 9 | marginTop: '58px', 10 | fontSize: '13px', 11 | lineHeight: '24px', 12 | padding: '5px 27px', 13 | }; 14 | 15 | return ( 16 |
17 |
18 |
We cannot find this page
19 |
{text}
20 |
21 |
27 | ); 28 | }; 29 | 30 | ErrorComponent.propTypes = { 31 | text: PropTypes.string, 32 | errorAction: PropTypes.func, 33 | }; 34 | -------------------------------------------------------------------------------- /frontend/src/scenes/errors/Error.module.scss: -------------------------------------------------------------------------------- 1 | .errorContainer { 2 | background-color: var(--background-color); 3 | width: 100%; 4 | height: 100%; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | 10 | .info { 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | text-align: center; 15 | gap: 18px; 16 | 17 | .infoHeader { 18 | font-weight: 600; 19 | color: var(--main-text-color); 20 | font-size: var(--font-header); 21 | line-height: 30px; 22 | } 23 | 24 | .infoText { 25 | font-size: var(--font-regular); 26 | font-weight: 400; 27 | line-height: 23px; 28 | } 29 | } 30 | 31 | .navigateButton { 32 | margin-top: 58px; 33 | border-radius: 8px; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/scenes/errors/constant.js: -------------------------------------------------------------------------------- 1 | export const TEXT_404 = 'The URL doesn’t exist'; 2 | export const TEXT_403 = 'You don’t have access to it.'; 3 | 4 | export const NAVIGATE_404_URL = '/'; 5 | export const NAVIGATE_403_URL = '/'; 6 | -------------------------------------------------------------------------------- /frontend/src/scenes/home/Home.jsx: -------------------------------------------------------------------------------- 1 | import Dashboard from '../dashboard/Dashboard'; 2 | import React from 'react'; 3 | import { useAuth } from '../../services/authProvider'; 4 | 5 | const Home = () => { 6 | const { userInfo } = useAuth(); 7 | 8 | return ; 9 | }; 10 | 11 | export default Home; 12 | -------------------------------------------------------------------------------- /frontend/src/scenes/login/PassswordResetPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Login.module.css'; 3 | import ArrowBackIcon from '@mui/icons-material/ArrowBack'; 4 | import { useNavigate } from 'react-router-dom'; 5 | 6 | function PasswordResetPage() { 7 | const navigate = useNavigate(); 8 | return ( 9 |
10 |

Password reset

11 |

12 | Your password has been successfully reset. Click below to log in 13 | manually. 14 |

15 | 22 | 27 |
28 | ); 29 | } 30 | 31 | export default PasswordResetPage; 32 | -------------------------------------------------------------------------------- /frontend/src/scenes/popup/PopupPageComponents/PopupAppearance/PopupAppearance.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../styles/globals.scss' as *; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: flex-start; 7 | flex: 1; 8 | h2 { 9 | @include text-style(regular); 10 | margin-top: 1.4rem; 11 | margin-bottom: 0.8rem; 12 | } 13 | .color { 14 | display: flex; 15 | align-items: center; 16 | width: 241px; 17 | > div { 18 | width: 100%; 19 | } 20 | } 21 | } 22 | 23 | .popup-appearance-error { 24 | font-family: var(--font-family-inter); 25 | font-size: var(--font-size-sm); 26 | font-weight: 400; 27 | line-height: 1.45; 28 | margin-top: 0px; 29 | color: var(--error-color); 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/scenes/popup/PopupPageComponents/PopupContent/PopupContent.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../styles/globals.scss' as *; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: flex-start; 7 | flex: 1; 8 | h2 { 9 | @include text-style(regular); 10 | margin-top: 1.5rem; 11 | } 12 | h3 { 13 | @include text-style(regular); 14 | } 15 | .radioContent { 16 | display: flex; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/scenes/progressSteps/ProgressSteps/ProgressSteps.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styles from './ProgressSteps.module.scss'; 4 | import Step from './Step'; 5 | 6 | const ProgressSteps = ({ stepData, completed = 0 }) => { 7 | let initialStates; 8 | if (typeof stepData === 'number') { 9 | initialStates = Array(stepData).fill(false); 10 | } else { 11 | initialStates = stepData.map(() => false); 12 | } 13 | initialStates = initialStates.map((_, index) => index < completed); 14 | 15 | return ( 16 |
17 | {typeof stepData === 'number' 18 | ? initialStates.map((_, index) => ( 19 | 26 | )) 27 | : stepData.map((step, index) => ( 28 | 37 | ))} 38 |
39 | ); 40 | }; 41 | 42 | ProgressSteps.propTypes = { 43 | stepData: PropTypes.array, 44 | completed: PropTypes.number, 45 | }; 46 | 47 | export default ProgressSteps; 48 | -------------------------------------------------------------------------------- /frontend/src/scenes/progressSteps/ProgressSteps/ProgressSteps.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../styles/globals.scss' as *; 2 | 3 | .container { 4 | width: 100%; 5 | display: flex; 6 | flex-direction: row; 7 | 8 | h3 { 9 | font-weight: 600; 10 | font-size: 14px; 11 | margin: 0; 12 | color: var(--main-text-color); 13 | margin: 10px 0px; 14 | } 15 | 16 | h4 { 17 | font-weight: 450; 18 | font-size: 14px; 19 | margin: 0; 20 | color: var(--second-text-color); 21 | text-align: center; 22 | } 23 | } 24 | 25 | .step { 26 | display: flex; 27 | flex-direction: column; 28 | align-items: center; 29 | flex-grow: 1; 30 | } 31 | 32 | .check-circle-icon, 33 | .on-progress, 34 | .future-step { 35 | font-size: 40px !important; 36 | z-index: 999; 37 | background-color: white; 38 | } 39 | .check-circle-icon { 40 | color: var(--main-purple); 41 | } 42 | 43 | .on-progress { 44 | color: var(--main-purple); 45 | border: 2px solid rgba(var(--main-purple), 0.5); 46 | } 47 | 48 | .future-step { 49 | color: var(--light-gray); 50 | } 51 | 52 | .line { 53 | position: absolute; 54 | top: 50%; 55 | left: calc(50% + 17px); 56 | height: 2px; 57 | background-color: var(--light-gray); 58 | width: calc(100% - 33px); 59 | transform: translateY(-50%); 60 | z-index: -99; 61 | } 62 | 63 | .icon-container { 64 | position: relative; 65 | width: 100%; 66 | display: flex; 67 | justify-content: center; 68 | } 69 | -------------------------------------------------------------------------------- /frontend/src/scenes/progressSteps/ProgressSteps/Step.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styles from './ProgressSteps.module.scss'; 4 | import StepIcon from './StepIcon'; 5 | import StepLine from './StepLine'; 6 | 7 | const main_purple = 'var(--main-purple)'; 8 | 9 | const Step = ({ 10 | prevStep = true, 11 | currentStep = false, 12 | title = null, 13 | explanation = null, 14 | index, 15 | dataLength, 16 | }) => { 17 | return ( 18 |
19 |
20 | 21 | {index + 1 < dataLength && } 22 |
23 |

{title}

24 |

{explanation}

25 |
26 | ); 27 | }; 28 | 29 | Step.propTypes = { 30 | prevStep: PropTypes.bool, 31 | currentStep: PropTypes.bool, 32 | title: PropTypes.string, 33 | explanation: PropTypes.string, 34 | index: PropTypes.number.isRequired, 35 | dataLength: PropTypes.number.isRequired, 36 | }; 37 | export default Step; 38 | -------------------------------------------------------------------------------- /frontend/src/scenes/progressSteps/ProgressSteps/StepIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styles from './ProgressSteps.module.scss'; 4 | import { 5 | CheckCircle as CheckCircleIcon, 6 | TripOrigin as TripOriginIcon, 7 | } from '@mui/icons-material'; 8 | 9 | const StepIcon = ({ prevStep = false, currentStep = false }) => { 10 | return ( 11 | <> 12 | {currentStep ? ( 13 | 14 | ) : ( 15 | 18 | )} 19 | 20 | ); 21 | }; 22 | 23 | StepIcon.propTypes = { 24 | prevStep: PropTypes.bool, 25 | currentStep: PropTypes.bool, 26 | }; 27 | 28 | export default StepIcon; 29 | -------------------------------------------------------------------------------- /frontend/src/scenes/progressSteps/ProgressSteps/StepLine.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styles from './ProgressSteps.module.scss'; 4 | 5 | const light_gray = 'var(--light-gray)'; 6 | const main_purple = 'var(--main-purple)'; 7 | 8 | const StepLine = ({ currentStep = false }) => { 9 | return ( 10 |
14 | ); 15 | }; 16 | 17 | StepLine.propTypes = { 18 | currentStep: PropTypes.bool.isRequired, 19 | }; 20 | 21 | export default StepLine; 22 | -------------------------------------------------------------------------------- /frontend/src/scenes/progressSteps/ProgressSteps/TeamMemberList/TeamMemberList.css: -------------------------------------------------------------------------------- 1 | .member { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | justify-content: space-between; 6 | font-size: 12px; 7 | border-radius: 0.5rem; 8 | border: 1px solid var(--light-border-color); 9 | padding: 5px; 10 | background-color: var(--header-background); 11 | margin-bottom: 8px; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/scenes/progressSteps/ProgressSteps/TeamMemberList/TeamMembersList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import CloseOutlinedIcon from '@mui/icons-material/CloseOutlined'; 4 | import './TeamMemberList.css'; 5 | 6 | const TeamMembersList = ({ members, setMembers }) => { 7 | const handleDeleteMember = (index) => { 8 | const updatedMembers = [...members]; 9 | updatedMembers.splice(index, 1); 10 | setMembers(updatedMembers); 11 | }; 12 | 13 | return ( 14 | <> 15 | {members.map((member, index) => ( 16 |
17 | {member} 18 | handleDeleteMember(index)} 20 | style={{ 21 | color: '#98A2B3', 22 | fontSize: '12px', 23 | cursor: 'pointer', 24 | zIndex: 1000, 25 | }} 26 | /> 27 |
28 | ))} 29 | 30 | ); 31 | }; 32 | 33 | TeamMembersList.propTypes = { 34 | members: PropTypes.arrayOf(PropTypes.string).isRequired, 35 | setMembers: PropTypes.func.isRequired, // Ensures setMembers is a function 36 | }; 37 | 38 | export default TeamMembersList; 39 | -------------------------------------------------------------------------------- /frontend/src/scenes/settings/CodeTab/CodeTab.module.css: -------------------------------------------------------------------------------- 1 | .block { 2 | display: flex; 3 | gap: 15px; 4 | align-items: center; 5 | } 6 | 7 | .informativeBlock { 8 | display: flex; 9 | justify-content: space-between; 10 | align-items: center; 11 | } 12 | 13 | h2 { 14 | margin-bottom: 0; 15 | margin-top: 15px; 16 | } 17 | h2, 18 | p { 19 | font-size: var(--font-regular); 20 | } 21 | 22 | pre { 23 | font-size: var(--font-informative); 24 | background-color: white; 25 | border: 1px solid var(--light-border-color); 26 | box-shadow: 0px 1px 2px 0px #1018280d; 27 | padding: 12px; 28 | margin: 0; 29 | border-radius: 4px; 30 | } 31 | 32 | .buttons { 33 | cursor: pointer; 34 | color: var(--main-text-color); 35 | } 36 | 37 | .container { 38 | margin-right: calc((100% - 25px) / 10); 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/scenes/settings/Modals/ChangeMemberRoleModal/ChangeMemberRoleModal.module.scss: -------------------------------------------------------------------------------- 1 | .box { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | background-color: white; 7 | box-shadow: 0px 1px 2px 0px #1018280d; 8 | border-radius: 5px; 9 | padding: 1.5rem 3rem; 10 | box-sizing: content-box; 11 | font-size: 13px; 12 | width: 350px; 13 | color: var(--third-text-color); 14 | 15 | p:first-child { 16 | font-weight: 600; 17 | } 18 | 19 | .select { 20 | width: 100% !important; 21 | margin-bottom: 1.5rem; 22 | border-radius: 5px !important; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/scenes/settings/Modals/DeleteConfirmationModal/DeleteConfirmationModal.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { DialogContentText } from '@mui/material'; 3 | import PopUpMessages from '../../../../components/PopUpMessages/PopUpMessages'; 4 | 5 | const DeleteConfirmationModal = ({ open, handleClose, handleDelete }) => { 6 | return ( 7 | 17 | 18 | If you delete your account, you will no longer be able to sign in, and 19 | all of your data will be deleted. Deleting your account is permanent and 20 | non-recoverable action. 21 | 22 | 23 | ); 24 | }; 25 | 26 | DeleteConfirmationModal.propTypes = { 27 | open: PropTypes.bool, 28 | handleClose: PropTypes.func, 29 | handleDelete: PropTypes.func, 30 | }; 31 | 32 | export default DeleteConfirmationModal; 33 | -------------------------------------------------------------------------------- /frontend/src/scenes/settings/Modals/InviteTeamMemberModal/InviteTeamMemberModal.module.scss: -------------------------------------------------------------------------------- 1 | .box { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | background-color: white; 7 | box-shadow: 0px 1px 2px 0px #1018280d; 8 | border-radius: 5px; 9 | padding: 1.5rem 3rem; 10 | box-sizing: content-box; 11 | font-size: var(--font-regular); 12 | width: 350px; 13 | color: var(--third-text-color); 14 | 15 | p:first-child { 16 | font-weight: 600; 17 | } 18 | 19 | .select { 20 | width: 100% !important; 21 | margin-bottom: 1.5rem; 22 | border-radius: 5px !important; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/scenes/settings/Modals/RemoveTeamMemberModal/RemoveTeamMemberModal.module.scss: -------------------------------------------------------------------------------- 1 | .box { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | background-color: white; 7 | box-shadow: 0px 1px 2px 0px #1018280d; 8 | border-radius: 5px; 9 | padding: 1.5rem 3rem; 10 | box-sizing: content-box; 11 | font-size: 13px; 12 | width: 350px; 13 | color: var(--third-text-color); 14 | 15 | p:first-child { 16 | font-weight: 600; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/scenes/settings/Modals/UploadImageModal/UploadModal.module.scss: -------------------------------------------------------------------------------- 1 | .uploadBox { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | background-color: white; 7 | box-shadow: 0px 1px 2px 0px #1018280d; 8 | border-radius: 5px; 9 | padding: 1.5rem 3rem; 10 | box-sizing: content-box; 11 | font-size: var(--font-regular); 12 | width: 350px; 13 | color: var(--third-text-color); 14 | } 15 | 16 | .errorMessage { 17 | color: #d92d20; 18 | } 19 | 20 | .uploadContainer { 21 | display: flex; 22 | flex-direction: column; 23 | border: 1px dashed #ccc; 24 | border-radius: 5px; 25 | align-items: center; 26 | justify-content: center; 27 | text-align: center; 28 | min-height: 150px; 29 | padding: 20px; 30 | cursor: pointer; 31 | 32 | & > p { 33 | & > span { 34 | color: #1570ef; 35 | cursor: pointer; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/scenes/settings/PasswordTab/PasswordTab.module.css: -------------------------------------------------------------------------------- 1 | .form { 2 | padding-bottom: 20px; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .label { 8 | font-size: var(--font-regular); 9 | font-weight: 700; 10 | line-height: 20px; 11 | text-align: left; 12 | flex-grow: 1; 13 | } 14 | .block { 15 | display: flex; 16 | justify-content: space-between; 17 | align-items: center; 18 | } 19 | 20 | .alert { 21 | display: flex; 22 | justify-content: flex-end; 23 | } 24 | 25 | .alertMessage { 26 | display: flex; 27 | width: 450px; 28 | gap: 12px; 29 | padding: 16px; 30 | border: 1px solid #fec84b; 31 | color: #dc6803; 32 | font-size: 14px; 33 | box-sizing: border-box; 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/scenes/settings/ProfileTab/ProfileTab.module.css: -------------------------------------------------------------------------------- 1 | .form { 2 | padding-bottom: 20px; 3 | min-width: 780px; 4 | max-width: 864px; 5 | border-bottom: 1px solid #eeeeee; 6 | } 7 | 8 | .formElements { 9 | display: flex; 10 | justify-content: space-between; 11 | flex-direction: row; 12 | align-items: center; 13 | } 14 | 15 | .photoElements { 16 | display: flex; 17 | flex-direction: column; 18 | margin-top: 30px; 19 | } 20 | 21 | .photoAlign { 22 | display: flex; 23 | justify-content: space-between; 24 | align-items: center; 25 | } 26 | 27 | .supportText { 28 | margin: 0; 29 | color: var(--second-text-color); 30 | font-size: var(--font-regular); 31 | font-weight: 400; 32 | line-height: 20px; 33 | margin-top: 6px; 34 | } 35 | 36 | .photoOptions { 37 | min-width: 350px; 38 | display: flex; 39 | gap: 5px; 40 | align-items: center; 41 | } 42 | 43 | .saveButton { 44 | text-align: right; 45 | } 46 | 47 | .labelElements { 48 | display: flex; 49 | flex-direction: column; 50 | } 51 | 52 | .label { 53 | font-size: var(--font-regular); 54 | min-width: 400px; 55 | font-weight: 700; 56 | line-height: 20px; 57 | text-align: left; 58 | flex-grow: 1; 59 | margin: 0; 60 | } 61 | 62 | .textField { 63 | flex-grow: 1; 64 | text-align: right; 65 | } 66 | 67 | .update, 68 | .delete { 69 | border: none; 70 | background: none; 71 | cursor: pointer; 72 | } 73 | .update { 74 | color: var(--main-purple); 75 | } 76 | 77 | .delete { 78 | color: var(--second-text-color); 79 | } 80 | -------------------------------------------------------------------------------- /frontend/src/scenes/settings/Settings.module.css: -------------------------------------------------------------------------------- 1 | .settings { 2 | display: flex; 3 | justify-content: center; 4 | width: 70%; 5 | margin: 30px 30px; 6 | } 7 | .tabLabel { 8 | font-size: var(--font-regular) !important; 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/scenes/settings/TeamTab/TeamTab.module.css: -------------------------------------------------------------------------------- 1 | .organisation { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | border-bottom: 1px solid var(--grey-border); 6 | padding-bottom: 15px; 7 | height: 2rem; 8 | margin-top: 8px; 9 | } 10 | 11 | .orgNameContainer { 12 | display: flex; 13 | gap: 1rem; 14 | align-items: center; 15 | } 16 | 17 | .nameHeading { 18 | margin: 0; 19 | } 20 | 21 | .organisationName { 22 | margin: 0; 23 | display: flex; 24 | align-items: center; 25 | gap: 0.6rem; 26 | font-size: var(--font-regular); 27 | } 28 | 29 | .team { 30 | display: flex; 31 | justify-content: space-between; 32 | } 33 | 34 | .tabs { 35 | border: 1px solid var(--grey-border) !important; 36 | } 37 | 38 | h6 { 39 | font-size: var(--font-regular); 40 | font-weight: 600; 41 | } 42 | 43 | .pencil { 44 | cursor: pointer; 45 | } 46 | 47 | .boldTab { 48 | font-weight: bold; 49 | } 50 | 51 | .normalTab { 52 | font-weight: normal; 53 | } 54 | 55 | .tabIndicator { 56 | background-color: var(--header-background); 57 | padding-top: 0px; 58 | } 59 | -------------------------------------------------------------------------------- /frontend/src/scenes/settings/TeamTab/TeamTable/TeamTable.module.css: -------------------------------------------------------------------------------- 1 | .tableHeader { 2 | background-color: rgba(250, 250, 250, 1); 3 | } 4 | 5 | .heading { 6 | color: var(--second-text-color) !important ; 7 | font-size: 12px; 8 | } 9 | 10 | .nameCol { 11 | display: flex !important; 12 | flex-direction: column; 13 | font-size: var(--font-regular); 14 | } 15 | 16 | .data { 17 | font-size: var(--font-regular); 18 | color: var(--second-text-color) !important; 19 | } 20 | 21 | .role { 22 | align-items: center; 23 | display: flex; 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/scenes/statistics/UserStatisticsPage.jsx: -------------------------------------------------------------------------------- 1 | import CustomTable from '../../components/Table/Table'; 2 | import { getAllGuideLogs } from '../../services/guidelogService'; 3 | import { useEffect, useState, React } from 'react'; 4 | import styles from './UserStatisticsPage.module.css'; 5 | 6 | const UserStatisticsPage = () => { 7 | const [guideLogs, setGuideLogs] = useState([]); 8 | 9 | useEffect(() => { 10 | const fetchGuideLogs = async () => { 11 | try { 12 | const guideLogsData = await getAllGuideLogs(); 13 | setGuideLogs(guideLogsData); 14 | } catch (err) { 15 | console.error('Error fetching guide logs:', err); 16 | } 17 | }; 18 | fetchGuideLogs(); 19 | }, []); 20 | 21 | return ( 22 |
23 | 24 |
25 | ); 26 | }; 27 | 28 | export default UserStatisticsPage; 29 | -------------------------------------------------------------------------------- /frontend/src/scenes/statistics/UserStatisticsPage.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 3rem 3.5rem; 3 | width: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/scenes/tours/TourPageComponents/TourLeftContent/TourLeftContent.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../styles/globals.scss' as *; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: flex-start; 7 | flex: 1; 8 | h2 { 9 | @include text-style(regular); 10 | margin-top: 1.5rem; 11 | margin-bottom: 0; 12 | } 13 | h3 { 14 | @include text-style(regular); 15 | } 16 | 17 | .stepsList { 18 | display: flex; 19 | flex-direction: column; 20 | gap: 0.8rem; 21 | padding: 0px; 22 | min-height: 200px; 23 | max-height: 250px; 24 | overflow-y: scroll; 25 | overflow-x: hidden; 26 | min-width: 280px; 27 | padding-right: 10px; 28 | margin-right: -20px; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/scenes/tours/TourPageComponents/TourleftAppearance/TourLeftAppearance.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../styles/globals.scss' as *; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: flex-start; 7 | margin-top: 1.6rem; 8 | margin-right: 3rem; 9 | color: var(--main-text-color); 10 | flex: 1; 11 | h2 { 12 | @include text-style(regular); 13 | } 14 | .color { 15 | display: flex; 16 | align-items: center; 17 | color: var(--main-text-color); 18 | width: 241px; 19 | > div { 20 | width: 100%; 21 | } 22 | } 23 | } 24 | 25 | .error { 26 | font-family: var(--font-family-inter); 27 | font-size: var(--font-size-sm); 28 | font-weight: 400; 29 | line-height: 1.45; 30 | margin-top: 2px; 31 | color: var(--error-color); 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/services/apiClient.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { API_BASE_URL } from '../utils/constants'; 3 | 4 | export const apiClient = axios.create({ 5 | baseURL: API_BASE_URL, 6 | headers: { 7 | 'Content-Type': 'application/json', 8 | }, 9 | }); 10 | 11 | apiClient.interceptors.request.use((config) => { 12 | const token = localStorage.getItem('authToken'); 13 | if (token) { 14 | config.headers.Authorization = `Bearer ${token}`; 15 | } 16 | return config; 17 | }); 18 | 19 | apiClient.interceptors.response.use( 20 | (response) => response, 21 | (error) => { 22 | if (error.response && error.response.status === 401) { 23 | localStorage.removeItem('authToken'); 24 | } 25 | 26 | return Promise.reject(error); 27 | } 28 | ); 29 | -------------------------------------------------------------------------------- /frontend/src/services/guidelogService.js: -------------------------------------------------------------------------------- 1 | import { apiClient } from './apiClient'; 2 | 3 | export const getAllGuideLogs = async () => { 4 | try { 5 | const response = await apiClient.get('/guide_log/all_guide_logs'); 6 | return response.data; 7 | } catch (error) { 8 | console.error('Get Guide Logs error:', error.response.data.errors); 9 | throw error; 10 | } 11 | }; 12 | 13 | export const addGuideLog = async (logData) => { 14 | try { 15 | const response = await apiClient.post('/guide_log/add_guide_log', logData); 16 | return response.data; 17 | } catch (error) { 18 | console.error('Add Guide Logs error:', error.response.data.errors); 19 | throw error; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /frontend/src/services/hintServices.js: -------------------------------------------------------------------------------- 1 | import { apiClient } from './apiClient'; 2 | 3 | export const addHint = async (hintData) => { 4 | try { 5 | const response = await apiClient.post('/hint/add_hint', hintData); 6 | return response.data; 7 | } catch (error) { 8 | console.error('Add hint error: ', error); 9 | throw error; 10 | } 11 | }; 12 | 13 | export const editHint = async (hintId, hintData) => { 14 | try { 15 | const response = await apiClient.put(`/hint/edit_hint/${hintId}`, hintData); 16 | return response.data; 17 | } catch (error) { 18 | console.error(`Edit hint error for ID (${hintId}): `, error); 19 | throw error; 20 | } 21 | }; 22 | 23 | export const deleteHint = async (hintId) => { 24 | try { 25 | const response = await apiClient.delete(`hint/delete_hint/${hintId}`); 26 | return response.data; 27 | } catch (error) { 28 | console.error(`Delete hint error for ID (${hintId}): `, error); 29 | throw error; 30 | } 31 | }; 32 | 33 | export const getHints = async () => { 34 | try { 35 | const response = await apiClient.get('/hint/all_hints'); 36 | return response.data; 37 | } catch (error) { 38 | console.error('Get hints error: ', error); 39 | throw error; 40 | } 41 | }; 42 | 43 | export const getHintById = async (hintId) => { 44 | try { 45 | const response = await apiClient.get(`/hint/get_hint/${hintId}`); 46 | return response.data; 47 | } catch (error) { 48 | console.error(`Get hint by ID (${hintId}) error: `, error); 49 | throw error; 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /frontend/src/services/inviteService.js: -------------------------------------------------------------------------------- 1 | import { roles } from '../utils/constants'; 2 | import { apiClient } from './apiClient'; 3 | 4 | const baseEndpoint = 'team/'; 5 | 6 | export const sendInvites = async (memberEmails) => { 7 | if (!memberEmails?.length) { 8 | throw new Error('No email addresses provided'); 9 | } 10 | 11 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 12 | const invalidEmails = memberEmails.filter((email) => !emailRegex.test(email)); 13 | if (invalidEmails.length) { 14 | throw new Error(`Invalid email addresses: ${invalidEmails.join(', ')}`); 15 | } 16 | 17 | try { 18 | const response = await Promise.all( 19 | memberEmails.map(async (email) => { 20 | const response = await apiClient.post(`${baseEndpoint}/invite`, { 21 | invitedEmail: email, 22 | role: roles[1], 23 | }); 24 | return response.data; 25 | }) 26 | ); 27 | return response; 28 | } catch (err) { 29 | console.error('Error sending invites: ', err.response); 30 | throw err; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /frontend/src/services/popupServices.js: -------------------------------------------------------------------------------- 1 | import { apiClient } from './apiClient'; 2 | 3 | export const addPopup = async (popupData) => { 4 | try { 5 | const response = await apiClient.post('/popup/add_popup', popupData); 6 | return response.data; 7 | } catch (error) { 8 | console.error('Add Popup error:', error.response); 9 | throw error; 10 | } 11 | }; 12 | 13 | export const getPopups = async () => { 14 | try { 15 | const response = await apiClient.get('/popup/all_popups'); 16 | return response.data; 17 | } catch (error) { 18 | console.error('Get Popups error:', error); 19 | throw error; 20 | } 21 | }; 22 | 23 | export const getPopupById = async (popupId) => { 24 | try { 25 | const response = await apiClient.get(`/popup/get_popup/${popupId}`); 26 | return response.data; 27 | } catch (error) { 28 | console.error(`Get Popup by ID (${popupId}) error:`, error); 29 | throw error; 30 | } 31 | }; 32 | 33 | export const editPopup = async (popupId, popupData) => { 34 | try { 35 | const response = await apiClient.put( 36 | `/popup/edit_popup/${popupId}`, 37 | popupData 38 | ); 39 | return response.data; 40 | } catch (error) { 41 | console.error(`Edit Popup error for ID (${popupId}):`, error); 42 | throw error; 43 | } 44 | }; 45 | 46 | export const deletePopup = async (popupId) => { 47 | try { 48 | const response = await apiClient.delete(`/popup/delete_popup/${popupId}`); 49 | return response.data; 50 | } catch (error) { 51 | console.error(`Delete Popup error for ID (${popupId}):`, error); 52 | throw error; 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /frontend/src/services/statisticsService.js: -------------------------------------------------------------------------------- 1 | import { emitToastError } from '../utils/guideHelper'; 2 | import { apiClient } from './apiClient'; 3 | 4 | const getStatistics = async () => { 5 | try { 6 | const response = await apiClient.get(`/statistics/`); 7 | return response.data; 8 | } catch (error) { 9 | const errorMessage = 10 | error.response?.data?.errors?.[0]?.msg || error.message; 11 | emitToastError({ 12 | response: { data: { errors: [{ msg: errorMessage }] } }, 13 | }); 14 | return null; 15 | } 16 | }; 17 | 18 | export { getStatistics }; 19 | -------------------------------------------------------------------------------- /frontend/src/services/teamServices.js: -------------------------------------------------------------------------------- 1 | import { apiClient } from './apiClient'; 2 | 3 | const baseEndpoint = 'team/'; 4 | 5 | export const setOrganisation = async (name) => { 6 | try { 7 | const response = await apiClient.post(`${baseEndpoint}/set-organisation`, { 8 | name, 9 | }); 10 | return response.data; 11 | } catch (err) { 12 | console.error('Error setting organization: ', err.response); 13 | throw err; 14 | } 15 | }; 16 | 17 | export const getTeamCount = async () => { 18 | try { 19 | const response = await apiClient.get(`${baseEndpoint}/count`); 20 | return response.data; 21 | } catch (err) { 22 | console.error('Error getting team count: ', err); 23 | throw err; 24 | } 25 | }; 26 | 27 | export const addServerUrl = async (serverUrl, agentUrl) => { 28 | try { 29 | const response = await apiClient.put(`${baseEndpoint}/urls`, { 30 | serverUrl, 31 | agentUrl, 32 | }); 33 | return response.data; 34 | } catch (err) { 35 | console.error('Error setting server url: ', err); 36 | throw err; 37 | } 38 | }; 39 | 40 | export const getServerUrl = async () => { 41 | try { 42 | const response = await apiClient.get(`${baseEndpoint}/urls`); 43 | return response.data; 44 | } catch (err) { 45 | console.error('Error getting server url: ', err); 46 | throw err; 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /frontend/src/styles/globals.scss: -------------------------------------------------------------------------------- 1 | @use './variables.css'; 2 | 3 | $base-font-size: var(--font-header); 4 | 5 | @mixin text-style($type, $font-weight: regular) { 6 | color: var(--main-text-color); 7 | @if $type == header-text { 8 | font-size: var(--font-header); 9 | line-height: 24px; 10 | } @else if $type == informative { 11 | font-size: var(--font-informative); 12 | line-height: 11px; 13 | } @else { 14 | font-size: var(--font-regular); 15 | line-height: 13px; 16 | } 17 | 18 | @if $font-weight == semibold { 19 | font-weight: 600; 20 | } @else { 21 | font-weight: 400; 22 | } 23 | } 24 | 25 | @function px-to-rem($size) { 26 | @return $size / $base-font-size * 1rem; 27 | } 28 | 29 | /* Global Scrollbar Styles */ 30 | * { 31 | scrollbar-width: thin; 32 | scrollbar-color: var(--gray-300) var(--background-color); 33 | } 34 | 35 | *::-webkit-scrollbar { 36 | width: 8px; 37 | height: 8px; 38 | } 39 | 40 | *::-webkit-scrollbar-track { 41 | background-color: var(--background-color); 42 | } 43 | 44 | *::-webkit-scrollbar-thumb { 45 | background-color: var(--gray-300); 46 | border: 2px solid var(--background-color); 47 | } 48 | -------------------------------------------------------------------------------- /frontend/src/templates/DefaultPageTemplate/DefaultPageTemplate.css: -------------------------------------------------------------------------------- 1 | .fade-in { 2 | animation: fadeIn 0.4s; 3 | width: 100%; 4 | height: 100%; 5 | display: flex; 6 | } 7 | 8 | @keyframes fadeIn { 9 | 0% { 10 | opacity: 0; 11 | } 12 | 100% { 13 | opacity: 1; 14 | } 15 | } 16 | 17 | .placeholder-style { 18 | display: flex; 19 | flex-direction: column; 20 | width: 100%; 21 | justify-content: center; 22 | align-items: center; 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/templates/GuideMainPageTemplate/GuideMainPageComponents/ConfirmationPopup/ConfirmationPopup.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { DialogContentText } from '@mui/material'; 3 | import PopUpMessages from '../../../../components/PopUpMessages/PopUpMessages.jsx'; 4 | 5 | const ConfirmationPopup = ({ open, onConfirm, onCancel }) => { 6 | return ( 7 | 15 | 16 | Are you sure you want to perform this action? 17 | 18 | 19 | ); 20 | }; 21 | 22 | ConfirmationPopup.propTypes = { 23 | open: PropTypes.bool.isRequired, 24 | onConfirm: PropTypes.func.isRequired, 25 | onCancel: PropTypes.func.isRequired, 26 | }; 27 | 28 | export default ConfirmationPopup; 29 | -------------------------------------------------------------------------------- /frontend/src/templates/GuideMainPageTemplate/GuideMainPageComponents/ContentArea/ContentArea.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const ContentArea = ({ children }) => { 5 | return
{children}
; 6 | }; 7 | 8 | ContentArea.propTypes = { 9 | children: PropTypes.node, 10 | }; 11 | 12 | export default ContentArea; 13 | -------------------------------------------------------------------------------- /frontend/src/templates/GuideMainPageTemplate/GuideMainPageComponents/ContentHeader/ContentHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const ContentHeader = ({ title }) => { 5 | return

{title}

; 6 | }; 7 | 8 | ContentHeader.propTypes = { 9 | title: PropTypes.string, 10 | }; 11 | 12 | export default ContentHeader; 13 | -------------------------------------------------------------------------------- /frontend/src/templates/GuideMainPageTemplate/GuideMainPageComponents/InfoTooltip/InfoTooltip.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Tooltip, IconButton } from '@mui/material'; 4 | import InfoIcon from '@mui/icons-material/Info'; 5 | 6 | const InfoTooltip = ({ text }) => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | InfoTooltip.propTypes = { 17 | text: PropTypes.string, 18 | }; 19 | 20 | export default InfoTooltip; 21 | -------------------------------------------------------------------------------- /frontend/src/templates/GuideMainPageTemplate/GuideMainPageComponents/List/List.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ListItem from './ListItem/ListItem'; 4 | 5 | const List = ({ items }) => { 6 | return ( 7 | <> 8 | {items.map((item) => ( 9 | {}} 15 | onDelete={item.onDelete} 16 | onEdit={item.onEdit} 17 | onDuplicate={item.onDuplicate} 18 | /> 19 | ))} 20 | 21 | ); 22 | }; 23 | 24 | List.propTypes = { 25 | items: PropTypes.arrayOf(PropTypes.object), 26 | onSelectItem: PropTypes.func, 27 | }; 28 | 29 | export default List; 30 | -------------------------------------------------------------------------------- /frontend/src/templates/GuideMainPageTemplate/GuideMainPageComponents/TourDescriptionText/TourDescriptionText.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const TourDescriptionText = ({ description }) => { 5 | return

{description}

; 6 | }; 7 | 8 | TourDescriptionText.propTypes = { 9 | description: PropTypes.string, 10 | }; 11 | 12 | export default TourDescriptionText; 13 | -------------------------------------------------------------------------------- /frontend/src/templates/GuideMainPageTemplate/GuideMainPageTemplate.css: -------------------------------------------------------------------------------- 1 | .product-page-container { 2 | padding: 2% 3%; 3 | width: 100%; 4 | } 5 | 6 | .product-page-header { 7 | display: flex; 8 | justify-content: space-between; 9 | align-items: center; 10 | width: 100%; 11 | margin-bottom: 20px; 12 | } 13 | 14 | .product-page { 15 | display: flex; 16 | align-items: flex-start; 17 | } 18 | 19 | .content-area { 20 | flex: 3; 21 | margin-right: 2rem; 22 | } 23 | 24 | .tour-info-container { 25 | flex: 1; 26 | padding: 16px; 27 | border-radius: 8px; 28 | border: 1px solid var(--grey-border); 29 | } 30 | 31 | .tour-info-container h4 { 32 | margin-top: 0; 33 | color: var(--main-purple); 34 | font-size: 18px; 35 | font-weight: 500; 36 | line-height: 28px; 37 | text-align: left; 38 | } 39 | 40 | .tour-info-container p { 41 | margin-bottom: 0; 42 | color: #6b7280; 43 | font-size: var(--font-regular); 44 | font-weight: 400; 45 | line-height: 20px; 46 | text-align: left; 47 | } 48 | -------------------------------------------------------------------------------- /frontend/src/templates/GuideTemplate/GuideTemplateContext.jsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const GuideTemplateContext = createContext({ 5 | isOpen: false, 6 | openDialog: () => {}, 7 | closeDialog: () => {}, 8 | }); 9 | 10 | export const useDialog = () => { 11 | const context = useContext(GuideTemplateContext); 12 | if (context === undefined) { 13 | throw new Error('useDialog must be used within a GuideTemplateProvider'); 14 | } 15 | return context; 16 | }; 17 | 18 | export const GuideTemplateProvider = ({ children }) => { 19 | const [isOpen, setIsOpen] = useState(false); 20 | 21 | const openDialog = () => setIsOpen(true); 22 | const closeDialog = () => setIsOpen(false); 23 | 24 | return ( 25 | 26 | {children} 27 | 28 | ); 29 | }; 30 | 31 | GuideTemplateProvider.propTypes = { 32 | children: PropTypes.node, 33 | }; 34 | -------------------------------------------------------------------------------- /frontend/src/templates/GuideTemplate/Readme.md: -------------------------------------------------------------------------------- 1 | # Guide Template 2 | 3 | The Guide Template is designed for creating "Create Activity" pages, such as CreatePopupPage and CreateBannerPage. 4 | 5 | ## Overview 6 | 7 | - **LeftContent**: Displayed when the "Content" button is clicked. 8 | - **LeftAppearance**: Displayed when the "Appearance" button is clicked. 9 | - **RightContent**: Always visible on the page. 10 | - **onSave**: Specifies the action to be taken when the "Save" button is clicked. 11 | - **title**: Allows editing of the page title. 12 | - **handleButtonClick**: Manages which button is clicked (either "Appearance" or "Content"). 13 | -------------------------------------------------------------------------------- /frontend/src/templates/HomePageTemplate/HomePageTemplate.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | min-width: 1024px; 5 | width: 100vw; 6 | height: 100vh; 7 | flex-grow: 1; 8 | overflow-x: hidden; 9 | } 10 | 11 | .content-container { 12 | display: flex; 13 | flex-grow: 1; 14 | background-color: var(--background-color); 15 | } -------------------------------------------------------------------------------- /frontend/src/templates/HomePageTemplate/HomePageTemplate.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LeftMenu from '@components/LeftMenu/LeftMenu'; 3 | import './HomePageTemplate.css'; 4 | import { Outlet } from 'react-router-dom'; 5 | 6 | const HomePageTemplate = () => { 7 | return ( 8 |
9 |
10 | 11 | 12 |
13 |
14 | ); 15 | }; 16 | 17 | export default HomePageTemplate; 18 | -------------------------------------------------------------------------------- /frontend/src/tests/components/Error/Error.test.jsx: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from 'vitest'; 2 | import { render, screen, fireEvent } from '@testing-library/react'; 3 | import React from 'react'; 4 | import { ErrorComponent } from '../../../scenes/errors/Error'; 5 | 6 | describe('ErrorComponent', () => { 7 | const mockErrorAction = vi.fn(); 8 | 9 | it('renders the error message and text', () => { 10 | const errorMessage = 'Page not found'; 11 | 12 | render( 13 | 14 | ); 15 | 16 | expect(screen.getByText('We cannot find this page')).not.toBeNull(); 17 | expect(screen.getByText(errorMessage)).not.toBeNull(); 18 | }); 19 | 20 | it('calls errorAction when button is clicked', () => { 21 | render( 22 | 23 | ); 24 | 25 | const button = screen.getByRole('button'); 26 | 27 | fireEvent.click(button); 28 | 29 | expect(mockErrorAction).toHaveBeenCalled(); 30 | }); 31 | 32 | it('applies custom styles to the button', () => { 33 | render( 34 | 35 | ); 36 | 37 | const button = screen.getByRole('button'); 38 | 39 | expect(button.style.borderRadius).toBe('8px'); 40 | expect(button.style.marginTop).toBe('58px'); 41 | expect(button.style.fontSize).toBe('13px'); 42 | expect(button.style.lineHeight).toBe('24px'); 43 | expect(button.style.padding).toBe('5px 27px'); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /frontend/src/tests/components/Toast/Toast.test.jsx: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { render, screen, act } from '@testing-library/react'; 3 | import Toast from '@components/Toast/Toast'; 4 | import toastEmitter, { TOAST_EMITTER_KEY } from '../../../utils/toastEmitter'; 5 | 6 | describe('Toast', () => { 7 | it('renders toasts correctly when event is emitted', () => { 8 | render(); 9 | 10 | act(() => { 11 | toastEmitter.emit(TOAST_EMITTER_KEY, 'Test Toast'); 12 | }); 13 | 14 | expect(screen.getByText('Test Toast')).to.exist; 15 | }); 16 | 17 | it('limits visible toasts to the maximum allowed count', () => { 18 | render(); 19 | 20 | act(() => { 21 | for (let i = 1; i <= 10; i++) { 22 | toastEmitter.emit(TOAST_EMITTER_KEY, `Toast ${i}`); 23 | } 24 | }); 25 | 26 | expect(screen.getAllByText(/Toast/).length).toBe(8); 27 | }); 28 | 29 | it('cleans up event listener on unmount', () => { 30 | const { unmount } = render(); 31 | unmount(); 32 | expect(toastEmitter.listenerCount(TOAST_EMITTER_KEY)).toBe(0); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /frontend/src/tests/components/customLinkComponent/CustomLink.test.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { describe, it, expect } from 'vitest'; 3 | import CustomLink from '@components/CustomLink/CustomLink'; 4 | 5 | describe('CustomLink', () => { 6 | it('renders CustomLink with text and url', () => { 7 | render(); 8 | const linkElement = screen.getByText(/Link Text/i); 9 | expect(linkElement).not.toBeNull(); 10 | expect(linkElement.getAttribute('href')).toBe('https://example.com'); 11 | }); 12 | 13 | it('applies default text and url correctly', () => { 14 | render(); 15 | const linkElement = screen.getByText(/Default Text/i); 16 | expect(linkElement).not.toBeNull(); 17 | expect(linkElement.getAttribute('href')).toBe(''); 18 | }); 19 | 20 | it('applies custom class names correctly', () => { 21 | render( 22 | 27 | ); 28 | const linkElement = screen.getByText(/Link Text/i); 29 | expect(linkElement.classList.contains('custom-link')).toBe(true); 30 | expect(linkElement.classList.contains('tertiary')).toBe(true); 31 | }); 32 | 33 | it('applies underline prop correctly', () => { 34 | render( 35 | 40 | ); 41 | const linkElement = screen.getByText(/Link Text/i); 42 | expect(linkElement.classList.contains('MuiLink-underlineHover')).toBe(true); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /frontend/src/tests/scenes/links/__snapshots__/NewLinksPopup.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Test Helper Link popup > Tests if the popup is rendered correctly 1`] = ` 4 | 5 |
8 | 9 | `; 10 | -------------------------------------------------------------------------------- /frontend/src/utils/bannerHelper.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | 3 | const RELATIVE_URL_REGEX = /^\/([a-zA-Z0-9_-]+\/?)+$/; 4 | const validateUrl = (url) => { 5 | try { 6 | new URL(url); 7 | return true; 8 | } catch { 9 | return RELATIVE_URL_REGEX.test(url); 10 | } 11 | }; 12 | 13 | const newBannerSchema = Yup.object().shape({ 14 | action: Yup.string().oneOf( 15 | ['no action', 'open url', 'open url in a new tab'], 16 | 'Invalid value for action' 17 | ), 18 | url: Yup.string() 19 | .test('url', 'Invalid value for URL', validateUrl) 20 | .max(2000, 'URL must be at most 2000 characters'), 21 | actionUrl: Yup.string() 22 | .test('actionUrl', 'Invalid value for Action URL', validateUrl) 23 | .max(2000, 'URL must be at most 2000 characters'), 24 | }); 25 | 26 | const appearanceSchema = Yup.object().shape({ 27 | backgroundColor: Yup.string() 28 | .matches(/^#[0-9A-Fa-f]{6}$/, 'Invalid value for Background Color') 29 | .required('Background Color is required'), 30 | fontColor: Yup.string() 31 | .matches(/^#[0-9A-Fa-f]{6}$/, 'Invalid value for Font Color') 32 | .required('Font Color is required'), 33 | }); 34 | 35 | export { newBannerSchema, appearanceSchema }; 36 | -------------------------------------------------------------------------------- /frontend/src/utils/constants.js: -------------------------------------------------------------------------------- 1 | // API constants 2 | //local environment 3 | export const BASE_URL = 'localhost:3000'; 4 | export const API_BASE_URL = `http://${BASE_URL}/api/`; 5 | 6 | //staging environment 7 | // export const BASE_URL = 'onboarding-demo.bluewavelabs.ca'; 8 | // export const API_BASE_URL = `https://${BASE_URL}/api/`; 9 | // Other constants 10 | export const APP_TITLE = 'Bluewave Onboarding'; 11 | export const SUPPORT_EMAIL = 'support@bluewave.com'; 12 | 13 | export const roles = Object.freeze(['admin', 'member']); 14 | export const URL_REGEX = Object.freeze({ 15 | PROTOCOL: /^(https?:\/\/)/, 16 | DOMAIN: /^https?:\/\/([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/, 17 | }); 18 | -------------------------------------------------------------------------------- /frontend/src/utils/loginHelper.js: -------------------------------------------------------------------------------- 1 | 2 | import { getTeamCount } from '../services/teamServices'; 3 | import toastEmitter, { TOAST_EMITTER_KEY } from './toastEmitter'; 4 | 5 | export const handleAuthSuccess = (response, loginAuth, navigate, redirectTo = null) => { 6 | const { id, name, surname, email, role, picture } = response.user; 7 | const payload = { id, name, surname, email, role, picture }; 8 | // Emit toast notification 9 | toastEmitter.emit(TOAST_EMITTER_KEY, 'Login successful'); 10 | 11 | // Update authentication state 12 | loginAuth(payload); 13 | 14 | getTeamCount() 15 | .then(response => { 16 | const { teamExists } = response; 17 | if (!teamExists) { 18 | navigate('/progress-steps'); 19 | } else if(redirectTo){ 20 | navigate(redirectTo) 21 | } 22 | else { 23 | navigate('/'); 24 | } 25 | }) 26 | .catch(err => console.error(err)); 27 | }; 28 | -------------------------------------------------------------------------------- /frontend/src/utils/toastEmitter.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | const toastEmitter = new EventEmitter(); 4 | 5 | export const TOAST_EMITTER_KEY = 'showToast'; 6 | 7 | export default toastEmitter; 8 | -------------------------------------------------------------------------------- /frontend/src/utils/tourHelper.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | 3 | const RELATIVE_URL_REGEX = /^\/([a-zA-Z0-9_-]+\/?)+$/; 4 | const COLOR_REGEX = /^#[0-9A-Fa-f]{6}$/; 5 | 6 | const validateUrl = (url) => { 7 | try { 8 | new URL(url); 9 | return true; 10 | } catch { 11 | return RELATIVE_URL_REGEX.test(url); 12 | } 13 | }; 14 | 15 | export const appearanceSchema = Yup.object().shape({ 16 | headerColor: Yup.string() 17 | .required('Header color is required') 18 | .matches(COLOR_REGEX, 'Invalid value for header color'), 19 | 20 | textColor: Yup.string() 21 | .required('Text color is required') 22 | .matches(COLOR_REGEX, 'Invalid value for text color'), 23 | 24 | buttonBackgroundColor: Yup.string() 25 | .required('Button background color is required') 26 | .matches(COLOR_REGEX, 'Invalid value for button background color'), 27 | 28 | buttonTextColor: Yup.string() 29 | .required('Button text color is required') 30 | .matches(COLOR_REGEX, 'Invalid value for button text color'), 31 | 32 | size: Yup.string() 33 | .oneOf(['small', 'medium', 'large'], 'Invalid value for tour size') 34 | .required('Tour size is required'), 35 | 36 | finalButtonText: Yup.string().required('Final button text is required'), 37 | 38 | url: Yup.string() 39 | .test('is-valid-url', 'Invalid value for URL', validateUrl) 40 | .max(2000, 'URL must be at most 2000 characters'), 41 | }); 42 | -------------------------------------------------------------------------------- /frontend/styles.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluewave-labs/guidefox/417a99627d91bc81ebe7ebe1f4f3cea068d44f7b/frontend/styles.css -------------------------------------------------------------------------------- /frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | import { BASE_URL } from "./src/utils/constants"; 4 | 5 | export default defineConfig({ 6 | base: "/", 7 | plugins: [react()], 8 | server: { 9 | host: "0.0.0.0", 10 | port: 4173, 11 | allowedHosts: [BASE_URL], 12 | watch: { 13 | usePolling: true, 14 | }, 15 | }, 16 | css: { 17 | preprocessorOptions: { 18 | scss: { 19 | api: "modern-compiler", 20 | }, 21 | }, 22 | }, 23 | test: { 24 | globals: true, 25 | environment: "jsdom", 26 | include: ["src/tests/**/*.test.jsx"], 27 | css: { 28 | modules: { 29 | classNameStrategy: "non-scoped", 30 | }, 31 | }, 32 | server: { 33 | deps: { 34 | inline: ["mui-color-input"], 35 | }, 36 | }, 37 | }, 38 | resolve: { 39 | alias: { 40 | "@components": "/src/components", 41 | }, 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bluewave-onboarding", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bluewave-onboarding", 3 | "version": "1.0.0", 4 | "description": "![](https://img.shields.io/github/license/bluewave-labs/guidefox) ![](https://img.shields.io/github/repo-size/bluewave-labs/guidefox) ![](https://img.shields.io/github/commit-activity/w/bluewave-labs/guidefox) ![](https://img.shields.io/github/last-commit/bluewave-labs/guidefox) ![](https://img.shields.io/github/languages/top/bluewave-labs/guidefox) ![](https://img.shields.io/github/issues-pr/bluewave-labs/guidefox) ![](https://img.shields.io/github/issues/bluewave-labs/guidefox)", 5 | "scripts": { 6 | "docker-up:dev": "docker compose -f docker-compose.dev.yml up", 7 | "docker-up:prod": "docker compose -f docker-compose.prod.yml up", 8 | "docker-up:prod-d": "docker compose -f docker-compose.prod.yml up -d", 9 | "docker-build:dev": "docker compose -f docker-compose.dev.yml build", 10 | "docker-build:prod": "docker compose -f docker-compose.prod.yml build", 11 | "docker-down:dev": "docker compose -f docker-compose.dev.yml down", 12 | "docker-down:prod": "docker compose -f docker-compose.prod.yml down" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC" 17 | } 18 | --------------------------------------------------------------------------------