├── .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 |
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 |
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 |
14 |
--------------------------------------------------------------------------------
/frontend/src/assets/logo/introflow_icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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
;
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 |
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 ;
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 |

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 |
26 |
27 | {children}
28 |
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 |
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 |
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 |
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 |
26 |
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 |
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": "      ",
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 |
--------------------------------------------------------------------------------