├── .dockerignore ├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 1-bug_report.yml │ └── config.yml └── workflows │ └── publish-docker-image.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── assets └── images │ ├── logo.svg │ └── screenshot.png ├── package.json ├── packages ├── answer-utils │ ├── .eslintrc │ ├── .prettierrc │ ├── package.json │ ├── src │ │ ├── answer-parser.ts │ │ ├── answer-to-api-object.ts │ │ ├── answer-to-html.ts │ │ ├── answer-to-json.ts │ │ ├── answer-to-plain.ts │ │ ├── apply-logic-to-fields.ts │ │ ├── calculate-action.ts │ │ ├── consts.ts │ │ ├── field-values-to-answer.ts │ │ ├── fields-to-validate-rules.ts │ │ ├── flatten-fields.ts │ │ ├── helper.ts │ │ ├── hidden-fields-to-html.ts │ │ ├── html-utils.ts │ │ ├── index.ts │ │ ├── validate-condition.ts │ │ ├── validate-payload.ts │ │ └── validate.ts │ ├── test │ │ ├── __snapshots__ │ │ │ ├── address.test.ts.snap │ │ │ ├── apply-logic-to-fields.test.ts.snap │ │ │ ├── convert-field-to-answer.test.ts.snap │ │ │ ├── email.test.ts.snap │ │ │ ├── flatten-fields.test.ts.snap │ │ │ ├── fullname.test.ts.snap │ │ │ ├── html-utils.test.ts.snap │ │ │ ├── multiple-choice.test.ts.snap │ │ │ ├── number.test.ts.snap │ │ │ ├── phone-number.test.ts.snap │ │ │ ├── rating.test.ts.snap │ │ │ ├── short-text.test.ts.snap │ │ │ ├── transform-answer.test.ts.snap │ │ │ ├── url.test.ts.snap │ │ │ └── yes-no.test.ts.snap │ │ ├── address.test.ts │ │ ├── apply-logic-to-fields.test.ts │ │ ├── convert-field-to-answer.test.ts │ │ ├── date.test.ts │ │ ├── email.test.ts │ │ ├── fixtures │ │ │ ├── fields.json │ │ │ └── values.json │ │ ├── flatten-fields.test.ts │ │ ├── fullname.test.ts │ │ ├── html-utils.test.ts │ │ ├── multiple-choice.test.ts │ │ ├── number.test.ts │ │ ├── phone-number.test.ts │ │ ├── rating.test.ts │ │ ├── short-text.test.ts │ │ ├── transform-answer.test.ts │ │ ├── url.test.ts │ │ ├── validate-condition.test.ts │ │ ├── validate-payload.test.ts │ │ ├── validate.test.ts │ │ └── yes-no.test.ts │ ├── tsconfig.json │ └── tsup.config.js ├── embed │ ├── .eslintrc │ ├── .prettierrc │ ├── example │ │ └── index.html │ ├── global.d.ts │ ├── package.json │ ├── pnpm-lock.yaml │ ├── postcss.config.js │ ├── rollup.config.mjs │ ├── src │ │ ├── assets │ │ │ ├── icon-close.svg │ │ │ ├── icon-loading.svg │ │ │ └── icon-message.svg │ │ ├── config.ts │ │ ├── full-page.ts │ │ ├── index.ts │ │ ├── modal.ts │ │ ├── popup.ts │ │ ├── standard.ts │ │ ├── style.scss │ │ ├── type.ts │ │ └── utils │ │ │ ├── common.ts │ │ │ ├── dom.ts │ │ │ └── index.ts │ └── tsconfig.json ├── form-renderer │ ├── .eslintrc │ ├── .prettierrc │ ├── global-env.d.ts │ ├── package.json │ ├── postcss.config.js │ ├── src │ │ ├── Renderer.tsx │ │ ├── blocks │ │ │ ├── Address.tsx │ │ │ ├── Block.tsx │ │ │ ├── ClosedMessage.tsx │ │ │ ├── Country.tsx │ │ │ ├── Date.tsx │ │ │ ├── DateRange.tsx │ │ │ ├── Email.tsx │ │ │ ├── EmptyState.tsx │ │ │ ├── FileUpload.tsx │ │ │ ├── Form.tsx │ │ │ ├── FullName.tsx │ │ │ ├── InputTable.tsx │ │ │ ├── LegalTerms.tsx │ │ │ ├── LongText.tsx │ │ │ ├── MultipleChoice.tsx │ │ │ ├── Number.tsx │ │ │ ├── OpinionScale.tsx │ │ │ ├── Payment.tsx │ │ │ ├── PhoneNumber.tsx │ │ │ ├── PictureChoice.tsx │ │ │ ├── Rating.tsx │ │ │ ├── ShortText.tsx │ │ │ ├── Signature.tsx │ │ │ ├── Statement.tsx │ │ │ ├── SuspendedMessage.tsx │ │ │ ├── ThankYou.tsx │ │ │ ├── Website.tsx │ │ │ ├── Welcome.tsx │ │ │ ├── YesNo.tsx │ │ │ └── hook.ts │ │ ├── components │ │ │ ├── Button.tsx │ │ │ ├── ChoiceRadio.tsx │ │ │ ├── ChoiceRadioGroup.tsx │ │ │ ├── Countdown.tsx │ │ │ ├── CountrySelect.tsx │ │ │ ├── DateInput.tsx │ │ │ ├── DateRangeInput.tsx │ │ │ ├── FileUploader.tsx │ │ │ ├── FlagIcon.tsx │ │ │ ├── FormField.tsx │ │ │ ├── Icons │ │ │ │ ├── CollapseIcon.tsx │ │ │ │ ├── CrownIcon.tsx │ │ │ │ ├── EmotionIcon.tsx │ │ │ │ ├── LikeIcon.tsx │ │ │ │ ├── LinkIcon.tsx │ │ │ │ ├── LogoIcon.tsx │ │ │ │ ├── StarIcon.tsx │ │ │ │ ├── ThumbsUpIcon.tsx │ │ │ │ ├── XIcon.tsx │ │ │ │ └── index.ts │ │ │ ├── Input.tsx │ │ │ ├── Layout.tsx │ │ │ ├── Loader.tsx │ │ │ ├── PhoneNumberInput.tsx │ │ │ ├── Popup.tsx │ │ │ ├── Radio.tsx │ │ │ ├── RadioGroup.tsx │ │ │ ├── Rate.tsx │ │ │ ├── SelectHelper.tsx │ │ │ ├── SignaturePad.tsx │ │ │ ├── Slide.tsx │ │ │ ├── Submit.tsx │ │ │ ├── TableInput.tsx │ │ │ ├── TemporaryError.tsx │ │ │ ├── Textarea.tsx │ │ │ ├── Tooltip.tsx │ │ │ └── index.ts │ │ ├── consts │ │ │ ├── country.ts │ │ │ ├── date.ts │ │ │ ├── fileUpload.ts │ │ │ ├── index.ts │ │ │ ├── other.ts │ │ │ ├── payment.ts │ │ │ └── rating.tsx │ │ ├── i18n.ts │ │ ├── index.ts │ │ ├── locales │ │ │ ├── cs.ts │ │ │ ├── de.ts │ │ │ ├── en.ts │ │ │ ├── es.ts │ │ │ ├── fr.ts │ │ │ ├── index.ts │ │ │ ├── ja.ts │ │ │ ├── pl.ts │ │ │ ├── pt-br.ts │ │ │ ├── tr.ts │ │ │ ├── zh-cn.ts │ │ │ └── zh-tw.ts │ │ ├── store.ts │ │ ├── style.scss │ │ ├── theme.ts │ │ ├── typings.ts │ │ ├── utils │ │ │ ├── common.ts │ │ │ ├── form.ts │ │ │ ├── hook.ts │ │ │ ├── index.ts │ │ │ ├── lru.ts │ │ │ ├── message.ts │ │ │ ├── script.ts │ │ │ └── timeout.ts │ │ └── views │ │ │ ├── Blocks.tsx │ │ │ ├── Branding.tsx │ │ │ ├── Footer.tsx │ │ │ ├── Header.tsx │ │ │ ├── Progress.tsx │ │ │ └── Sidebar.tsx │ ├── tailwind.config.js │ ├── tsconfig.json │ └── tsup.config.js ├── server │ ├── .env.example │ ├── .eslintrc │ ├── .npmrc │ ├── .prettierrc │ ├── nest-cli.json │ ├── package.json │ ├── resources │ │ ├── apps.json │ │ └── email-templates │ │ │ ├── README.md │ │ │ ├── account_deletion_alert.html │ │ │ ├── account_deletion_request.html │ │ │ ├── email_verification_request.html │ │ │ ├── password_change_alert.html │ │ │ ├── project_deletion_alert.html │ │ │ ├── project_deletion_request.html │ │ │ ├── schedule_account_deletion_alert.html │ │ │ ├── submission_notification.html │ │ │ ├── team_deletion_alert.html │ │ │ ├── team_deletion_request.html │ │ │ └── team_invitation.html │ ├── src │ │ ├── app.module.ts │ │ ├── common │ │ │ ├── decorator │ │ │ │ ├── auth.decorator.ts │ │ │ │ ├── data-mask-options.decorator.ts │ │ │ │ ├── form.decorator.ts │ │ │ │ ├── graphql.decorator.ts │ │ │ │ ├── index.ts │ │ │ │ ├── permission.decorator.ts │ │ │ │ ├── project.decorator.ts │ │ │ │ ├── team.decorator.ts │ │ │ │ └── user.decorator.ts │ │ │ ├── dto │ │ │ │ └── index.ts │ │ │ ├── filter │ │ │ │ ├── all-exceptions.filter.ts │ │ │ │ └── index.ts │ │ │ ├── graphql │ │ │ │ ├── app.graphql.ts │ │ │ │ ├── auth.graphql.ts │ │ │ │ ├── endpoint.graphql.ts │ │ │ │ ├── form.graphql.ts │ │ │ │ ├── index.ts │ │ │ │ ├── integration.graphql.ts │ │ │ │ ├── label.graphql.ts │ │ │ │ ├── payment.graphql.ts │ │ │ │ ├── project.graphql.ts │ │ │ │ ├── submission.graphql.ts │ │ │ │ ├── team.graphql.ts │ │ │ │ ├── template.graphql.ts │ │ │ │ ├── unsplash.graphql.ts │ │ │ │ └── user.graphql.ts │ │ │ ├── guard │ │ │ │ ├── auth.guard.ts │ │ │ │ ├── browser-id.guard.ts │ │ │ │ ├── endpoint-anonymous-id.guard.ts │ │ │ │ ├── index.ts │ │ │ │ ├── permission.guard.ts │ │ │ │ └── role.guard.ts │ │ │ ├── interceptor │ │ │ │ ├── data-mask.interceptor.ts │ │ │ │ └── index.ts │ │ │ └── middleware │ │ │ │ ├── form-body.middleware.ts │ │ │ │ ├── index.ts │ │ │ │ ├── json-body.middleware.ts │ │ │ │ └── raw-body.middleware.ts │ │ ├── config │ │ │ ├── bull │ │ │ │ └── index.ts │ │ │ ├── cookie │ │ │ │ └── index.ts │ │ │ ├── graphql │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── mongo │ │ │ │ └── index.ts │ │ │ ├── redis │ │ │ │ └── index.ts │ │ │ ├── smtp │ │ │ │ └── index.ts │ │ │ └── upload │ │ │ │ └── index.ts │ │ ├── controller │ │ │ ├── connect-stripe.controller.ts │ │ │ ├── dashboard.controller.ts │ │ │ ├── export-submissions.controller.ts │ │ │ ├── form.controller.ts │ │ │ ├── health.controller.ts │ │ │ ├── image.controller.ts │ │ │ ├── index.ts │ │ │ ├── payment-intent-webhook.controller.ts │ │ │ ├── social-login.controller.ts │ │ │ └── upload.controller.ts │ │ ├── environments │ │ │ └── index.ts │ │ ├── main.ts │ │ ├── model │ │ │ ├── app.model.ts │ │ │ ├── attachment.model.ts │ │ │ ├── form-analytic.model.ts │ │ │ ├── form-report.model.ts │ │ │ ├── form.model.ts │ │ │ ├── index.ts │ │ │ ├── integration-record.model.ts │ │ │ ├── integration.model.ts │ │ │ ├── module.ts │ │ │ ├── project-group.model.ts │ │ │ ├── project-member.model.ts │ │ │ ├── project.model.ts │ │ │ ├── submission-ip-limit.model.ts │ │ │ ├── submission.model.ts │ │ │ ├── team-activity.model.ts │ │ │ ├── team-invitation.model.ts │ │ │ ├── team-member.model.ts │ │ │ ├── team.model.ts │ │ │ ├── template.model.ts │ │ │ ├── user-social-account.model.ts │ │ │ └── user.model.ts │ │ ├── queue │ │ │ ├── base.queue.ts │ │ │ ├── form-report.queue.ts │ │ │ ├── index.ts │ │ │ ├── integration-email.queue.ts │ │ │ ├── integration-webhook.queue.ts │ │ │ ├── mail.queue.ts │ │ │ └── translate-form.queue.ts │ │ ├── resolver │ │ │ ├── app │ │ │ │ ├── app-detail.resolver.ts │ │ │ │ └── apps.resolver.ts │ │ │ ├── auth │ │ │ │ ├── login.resolver.ts │ │ │ │ ├── reset-password.resolver.ts │ │ │ │ ├── send-reset-password-email.resolver.ts │ │ │ │ └── sign-up.resolver.ts │ │ │ ├── endpoint │ │ │ │ ├── complete-submission.resolver.ts │ │ │ │ ├── form-password.resolver.ts │ │ │ │ └── open-form.resolver.ts │ │ │ ├── form │ │ │ │ ├── create-form-field.resolver.ts │ │ │ │ ├── create-form-hidden-field.resolver.ts │ │ │ │ ├── create-form.resolver.ts │ │ │ │ ├── delete-form-field.resolver.ts │ │ │ │ ├── delete-form-hidden-field.resolver.ts │ │ │ │ ├── delete-form.resolver.ts │ │ │ │ ├── duplicate-form.resolver.ts │ │ │ │ ├── form-analytic.resolver.ts │ │ │ │ ├── form-archive.resolver.ts │ │ │ │ ├── form-detail.resolver.ts │ │ │ │ ├── form-integrations.resolver.ts │ │ │ │ ├── form-report.resolver.ts │ │ │ │ ├── forms.resolver.ts │ │ │ │ ├── move-form-to-trash.resolver.ts │ │ │ │ ├── restore-form.resolver.ts │ │ │ │ ├── search-forms.resolver.ts │ │ │ │ ├── update-form-archive.resolver.ts │ │ │ │ ├── update-form-field.resolver.ts │ │ │ │ ├── update-form-hidden-field.resolver.ts │ │ │ │ ├── update-form-logics.resolver.ts │ │ │ │ ├── update-form-schemas.resolver.ts │ │ │ │ ├── update-form-theme.resolver.ts │ │ │ │ ├── update-form-variables.resolver.ts │ │ │ │ └── update-form.resolver.ts │ │ │ ├── index.ts │ │ │ ├── integration │ │ │ │ ├── delete-integration-settings.resolver.ts │ │ │ │ ├── update-integration-settings.resolver.ts │ │ │ │ └── update-integration-status.resolver.ts │ │ │ ├── payment │ │ │ │ ├── connect-stripe.resolver.ts │ │ │ │ ├── revoke-stripe-account.resolver.ts │ │ │ │ └── stripe-authorize-url.resolver.ts │ │ │ ├── project │ │ │ │ ├── add-project-member.resolver.ts │ │ │ │ ├── create-project.resolver.ts │ │ │ │ ├── delete-project-code.resolver.ts │ │ │ │ ├── delete-project-member.resolver.ts │ │ │ │ ├── delete-project.resolver.ts │ │ │ │ ├── empty-project-trash.resolver.ts │ │ │ │ ├── leave-project.resolver.ts │ │ │ │ ├── project-members.resolver.ts │ │ │ │ ├── projects.resolver.ts │ │ │ │ └── rename-project.resolver.ts │ │ │ ├── submission │ │ │ │ ├── delete-submission.resolver.ts │ │ │ │ ├── submission-answers.resolver.ts │ │ │ │ ├── submissions.resolver.ts │ │ │ │ └── update-submission-answer.resolver.ts │ │ │ ├── team │ │ │ │ ├── create-team.resolver.ts │ │ │ │ ├── dissolve-team-code.resolver.ts │ │ │ │ ├── dissolve-team.resolver.ts │ │ │ │ ├── invite-member.resolver.ts │ │ │ │ ├── join-team.resolver.ts │ │ │ │ ├── leave-team.resolver.ts │ │ │ │ ├── public-team-detail.resolver.ts │ │ │ │ ├── remove-team-member.resolver.ts │ │ │ │ ├── reset-team-invite-code.resolver.ts │ │ │ │ ├── team-members.resolver.ts │ │ │ │ ├── team-subscription.resolver.ts │ │ │ │ ├── teams.resolver.ts │ │ │ │ ├── transfer-team.resolver.ts │ │ │ │ └── update-team.resolver.ts │ │ │ ├── template │ │ │ │ ├── template-detail.resolver.ts │ │ │ │ ├── templates.resolver.ts │ │ │ │ └── use-template.resolver.ts │ │ │ ├── unsplash │ │ │ │ ├── unsplash-search.resolver.ts │ │ │ │ └── unsplash-track-download.resolver.ts │ │ │ └── user │ │ │ │ ├── cancel-user-deletion.resolver.ts │ │ │ │ ├── change-email-code.resolver.ts │ │ │ │ ├── email-verification-code.resolver.ts │ │ │ │ ├── update-email.resolver.ts │ │ │ │ ├── update-user-password.resolver.ts │ │ │ │ ├── update-user.resolver.ts │ │ │ │ ├── user-deletion-code.resolver.ts │ │ │ │ ├── user-detail.resolver.ts │ │ │ │ ├── verify-email.resolver.ts │ │ │ │ └── verify-user-deletion.resolver.ts │ │ ├── schedule │ │ │ ├── delete-form-in-trash.schedule.ts │ │ │ ├── delete-user-account.schedule.ts │ │ │ ├── index.ts │ │ │ └── reset-invite-code.schedule.ts │ │ ├── service │ │ │ ├── app.service.ts │ │ │ ├── auth.service.ts │ │ │ ├── endpoint.service.ts │ │ │ ├── export-file.service.ts │ │ │ ├── form-analytic.service.ts │ │ │ ├── form-report.service.ts │ │ │ ├── form.service.ts │ │ │ ├── index.ts │ │ │ ├── integration.service.ts │ │ │ ├── mail.service.ts │ │ │ ├── payment.service.ts │ │ │ ├── project.service.ts │ │ │ ├── redis.service.ts │ │ │ ├── schedule.service.ts │ │ │ ├── social-login.service.ts │ │ │ ├── submission-ip-limit.service.ts │ │ │ ├── submission.service.ts │ │ │ ├── team.service.ts │ │ │ ├── template.service.ts │ │ │ └── user.service.ts │ │ └── utils │ │ │ ├── anti-bot │ │ │ ├── akismet.ts │ │ │ ├── index.ts │ │ │ └── recaptcha.ts │ │ │ ├── crypto.ts │ │ │ ├── decorators │ │ │ ├── client.ts │ │ │ ├── device-id.ts │ │ │ ├── index.ts │ │ │ ├── ip.ts │ │ │ ├── lang.ts │ │ │ ├── raw-body.ts │ │ │ └── user-agent.ts │ │ │ ├── directives │ │ │ ├── index.ts │ │ │ └── lower.ts │ │ │ ├── disposable-email.ts │ │ │ ├── gravatar │ │ │ └── index.ts │ │ │ ├── handlebars.ts │ │ │ ├── index.ts │ │ │ ├── logger │ │ │ └── index.ts │ │ │ ├── lower-case-scalar.ts │ │ │ ├── map-to-object.ts │ │ │ ├── mongo.ts │ │ │ ├── promise-timeout.ts │ │ │ ├── random-number.ts │ │ │ ├── read-dir-sync.ts │ │ │ ├── request-parser.ts │ │ │ ├── smtp │ │ │ └── index.ts │ │ │ ├── social-login │ │ │ ├── apple.ts │ │ │ ├── google.ts │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ │ ├── unsplash │ │ │ └── index.ts │ │ │ └── user-agent │ │ │ └── index.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── view │ │ ├── connect-stripe.html │ │ └── social-login.html ├── shared-types-enums │ ├── .eslintrc │ ├── .prettierrc │ ├── package.json │ ├── src │ │ ├── audience.ts │ │ ├── constants │ │ │ └── form.ts │ │ ├── enums │ │ │ ├── form.ts │ │ │ ├── keyboard.ts │ │ │ ├── social-login.ts │ │ │ └── submission.ts │ │ ├── form.ts │ │ ├── index.ts │ │ ├── submission.ts │ │ └── unsplash.ts │ ├── tsconfig.json │ └── tsup.config.js ├── utils │ ├── .eslintrc │ ├── .prettierrc │ ├── package.json │ ├── src │ │ ├── bytes.ts │ │ ├── clone.ts │ │ ├── color.ts │ │ ├── conv.ts │ │ ├── date.ts │ │ ├── helper.ts │ │ ├── index.ts │ │ ├── mime-db.json │ │ ├── mime.ts │ │ ├── nanoid.ts │ │ ├── object.ts │ │ ├── random.ts │ │ ├── second.ts │ │ ├── slugify.ts │ │ ├── type.ts │ │ └── uuid.ts │ ├── test │ │ ├── __snapshots__ │ │ │ └── mime.test.ts.snap │ │ ├── bytes.test.ts │ │ ├── clone.test.ts │ │ ├── color.test.ts │ │ ├── conv.test.ts │ │ ├── date.test.ts │ │ ├── helper.test.ts │ │ ├── hs.test.ts │ │ ├── mime.test.ts │ │ ├── nanoid.test.ts │ │ ├── object.test.ts │ │ ├── parse.test.ts │ │ ├── qs.test.ts │ │ ├── random.test.ts │ │ ├── second.test.ts │ │ ├── slugify.test.ts │ │ ├── type.test.ts │ │ └── uuid.test.ts │ ├── tsconfig.json │ └── tsup.config.js └── webapp │ ├── .env.example │ ├── .eslintrc │ ├── .prettierrc │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── public │ └── static │ │ ├── apple-touch-icon.png │ │ ├── email.png │ │ ├── facebookpixel.png │ │ ├── favicon.ico │ │ ├── favicon.svg │ │ ├── googleanalytics.png │ │ ├── icon_120.png │ │ ├── icon_128.png │ │ ├── icon_180.png │ │ ├── icon_192.png │ │ ├── icon_512.png │ │ ├── icon_maskable_128.png │ │ ├── icon_maskable_192.png │ │ ├── icon_maskable_512.png │ │ ├── initial.css │ │ ├── manifest.webmanifest │ │ └── webhook.png │ ├── src │ ├── components │ │ ├── Async.tsx │ │ ├── CopyButton.tsx │ │ ├── DateRangePicker.tsx │ │ ├── DragUploader.tsx │ │ ├── Heading.tsx │ │ ├── MobilePhoneCode.tsx │ │ ├── Pagination.tsx │ │ ├── PhotoPickerField.tsx │ │ ├── RedirectUriLink.tsx │ │ ├── SubHeading.tsx │ │ ├── SwitchField.tsx │ │ ├── TagGroup.tsx │ │ ├── TimeInput.tsx │ │ ├── Uploader.tsx │ │ ├── icons │ │ │ ├── AddImageIcon.tsx │ │ │ ├── AppleIcon.tsx │ │ │ ├── BoldIcon.tsx │ │ │ ├── BranchIcon.tsx │ │ │ ├── CollapseIcon.tsx │ │ │ ├── ConcentricCirclesIcon.tsx │ │ │ ├── CrownIcon.tsx │ │ │ ├── DateRangeIcon.tsx │ │ │ ├── DateTimeIcon.tsx │ │ │ ├── EdgeArrow.tsx │ │ │ ├── EmotionIcon.tsx │ │ │ ├── ExpandIcon.tsx │ │ │ ├── FullpageIcon.tsx │ │ │ ├── GoogleIcon.tsx │ │ │ ├── ImageIcon.tsx │ │ │ ├── ItalicIcon.tsx │ │ │ ├── LayoutCoverIcon.tsx │ │ │ ├── LayoutFloatLeftIcon.tsx │ │ │ ├── LayoutFloatRightIcon.tsx │ │ │ ├── LayoutInlineIcon.tsx │ │ │ ├── LayoutSplitLeftIcon.tsx │ │ │ ├── LayoutSplitRightIcon.tsx │ │ │ ├── LikeIcon.tsx │ │ │ ├── LinkIcon.tsx │ │ │ ├── LogoIcon.tsx │ │ │ ├── LongTextIcon.tsx │ │ │ ├── ModalIcon.tsx │ │ │ ├── NumberVariableIcon.tsx │ │ │ ├── PopupIcon.tsx │ │ │ ├── RestoreIcon.tsx │ │ │ ├── RoundLogoIcon.tsx │ │ │ ├── ShortTextIcon.tsx │ │ │ ├── SignatureIcon.tsx │ │ │ ├── StandardIcon.tsx │ │ │ ├── StarIcon.tsx │ │ │ ├── StatementIcon.tsx │ │ │ ├── StringVariableIcon.tsx │ │ │ ├── TableIcon.tsx │ │ │ ├── ThankYouIcon.tsx │ │ │ ├── ThumbsUpIcon.tsx │ │ │ ├── UnderlineIcon.tsx │ │ │ ├── UnlinkIcon.tsx │ │ │ ├── WebsiteIcon.tsx │ │ │ ├── WelcomeIcon.tsx │ │ │ ├── WorkspaceIcon.tsx │ │ │ ├── YesOrNoIcon.tsx │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── layouts │ │ │ ├── AuthGuard.tsx │ │ │ ├── AuthLayout.tsx │ │ │ ├── CommonLayout.tsx │ │ │ ├── FormGuardLayout.tsx │ │ │ ├── FormLayout.tsx │ │ │ ├── PublicLayout.tsx │ │ │ ├── WorkspaceGuard.tsx │ │ │ ├── WorkspaceLayout.tsx │ │ │ ├── index.scss │ │ │ └── index.ts │ │ ├── numberRange │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ ├── photoPicker │ │ │ ├── Unsplash.tsx │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ ├── sidebar │ │ │ ├── Navigation.tsx │ │ │ ├── UserAccount.tsx │ │ │ ├── WorkspaceSwitcher.tsx │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── store.ts │ │ └── ui │ │ │ ├── avatar │ │ │ ├── Avatar.tsx │ │ │ ├── Group.tsx │ │ │ ├── index.ts │ │ │ └── style.scss │ │ │ ├── badge │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ │ ├── button │ │ │ ├── Button.tsx │ │ │ ├── Group.tsx │ │ │ ├── Link.tsx │ │ │ ├── index.ts │ │ │ └── style.scss │ │ │ ├── checkbox │ │ │ ├── Checkbox.tsx │ │ │ ├── Group.tsx │ │ │ ├── index.ts │ │ │ └── style.scss │ │ │ ├── color-picker │ │ │ ├── AlphaInput.tsx │ │ │ ├── HexColorInput.tsx │ │ │ ├── helper.ts │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ │ ├── date-picker │ │ │ ├── Calendar.tsx │ │ │ ├── MonthPicker.tsx │ │ │ ├── TimePicker.tsx │ │ │ ├── YearPicker.tsx │ │ │ ├── common.ts │ │ │ ├── index.tsx │ │ │ ├── store.ts │ │ │ ├── style.scss │ │ │ └── utils.ts │ │ │ ├── dropdown │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ │ ├── empty-states │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ │ ├── form │ │ │ ├── CustomForm.tsx │ │ │ ├── FormItem.tsx │ │ │ ├── SwitchItem.tsx │ │ │ ├── index.ts │ │ │ └── style.scss │ │ │ ├── heading │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ │ ├── hook.ts │ │ │ ├── icons │ │ │ ├── DefaultAvatarIcon.tsx │ │ │ ├── EyeCloseIcon.tsx │ │ │ └── index.ts │ │ │ ├── image │ │ │ └── index.tsx │ │ │ ├── index.ts │ │ │ ├── input │ │ │ ├── Input.tsx │ │ │ ├── Password.tsx │ │ │ ├── Search.tsx │ │ │ ├── Textarea.tsx │ │ │ ├── index.ts │ │ │ └── style.scss │ │ │ ├── loader │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ │ ├── menu │ │ │ ├── Divider.tsx │ │ │ ├── Item.tsx │ │ │ ├── Label.tsx │ │ │ ├── Menus.tsx │ │ │ ├── context.ts │ │ │ ├── index.ts │ │ │ └── style.scss │ │ │ ├── modal │ │ │ ├── Confirm.tsx │ │ │ ├── Modal.tsx │ │ │ ├── index.ts │ │ │ └── style.scss │ │ │ ├── navbar │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ │ ├── notification │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ │ ├── popup │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ │ ├── portal │ │ │ └── index.tsx │ │ │ ├── progress │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ │ ├── radio │ │ │ ├── Group.tsx │ │ │ ├── Radio.tsx │ │ │ ├── index.ts │ │ │ └── style.scss │ │ │ ├── rate │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ │ ├── select │ │ │ ├── Card.tsx │ │ │ ├── Cards.tsx │ │ │ ├── CardsContext.ts │ │ │ ├── Custom.tsx │ │ │ ├── Multiple.tsx │ │ │ ├── Native.tsx │ │ │ ├── index.tsx │ │ │ ├── style.scss │ │ │ └── utils.ts │ │ │ ├── slider │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ │ ├── spin │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ │ ├── stepper │ │ │ ├── StepperItem.tsx │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ │ ├── style.scss │ │ │ ├── switch │ │ │ ├── Group.tsx │ │ │ ├── Switch.tsx │ │ │ ├── index.ts │ │ │ └── style.scss │ │ │ ├── table │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ │ ├── tabs │ │ │ ├── Pane.tsx │ │ │ ├── Tabs.tsx │ │ │ ├── context.ts │ │ │ ├── index.ts │ │ │ └── style.scss │ │ │ ├── tooltip │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ │ ├── typing.ts │ │ │ └── utils.ts │ ├── consts │ │ ├── date.ts │ │ ├── environments.ts │ │ ├── formBuilder.ts │ │ ├── formSettings.ts │ │ ├── graphql.ts │ │ └── index.ts │ ├── locales │ │ ├── cs.ts │ │ ├── en.ts │ │ ├── es.ts │ │ ├── index.ts │ │ ├── pl.ts │ │ ├── ptBr.ts │ │ ├── tr.ts │ │ ├── zhCn.ts │ │ └── zhTw.ts │ ├── main.tsx │ ├── models │ │ ├── compose.ts │ │ ├── form.ts │ │ ├── index.ts │ │ ├── integration.ts │ │ ├── project.ts │ │ ├── template.ts │ │ ├── user.ts │ │ └── workspace.ts │ ├── pages │ │ ├── auth │ │ │ ├── ForgotPassword.tsx │ │ │ ├── Login.tsx │ │ │ ├── OauthAuthorization.tsx │ │ │ ├── ResetPassword.tsx │ │ │ ├── SignUp.tsx │ │ │ └── views │ │ │ │ └── ThirdPartyLogin.tsx │ │ ├── form │ │ │ ├── Analytics │ │ │ │ ├── index.tsx │ │ │ │ └── views │ │ │ │ │ ├── Report │ │ │ │ │ ├── AnswerList.tsx │ │ │ │ │ ├── FieldList.tsx │ │ │ │ │ ├── ReportItem.tsx │ │ │ │ │ └── index.tsx │ │ │ │ │ └── Summary.tsx │ │ │ ├── Create │ │ │ │ ├── consts │ │ │ │ │ ├── country.ts │ │ │ │ │ ├── date.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── logic.ts │ │ │ │ │ ├── payment.ts │ │ │ │ │ └── rating.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── store │ │ │ │ │ ├── actions.ts │ │ │ │ │ ├── context.ts │ │ │ │ │ ├── hook.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── style.scss │ │ │ │ ├── utils │ │ │ │ │ ├── field.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logic.ts │ │ │ │ │ └── queue.ts │ │ │ │ └── views │ │ │ │ │ ├── Compose │ │ │ │ │ ├── Blocks │ │ │ │ │ │ ├── Address.tsx │ │ │ │ │ │ ├── Block.tsx │ │ │ │ │ │ ├── Country.tsx │ │ │ │ │ │ ├── Date.tsx │ │ │ │ │ │ ├── DateRange.tsx │ │ │ │ │ │ ├── Email.tsx │ │ │ │ │ │ ├── FileUpload.tsx │ │ │ │ │ │ ├── FullName.tsx │ │ │ │ │ │ ├── InputTable.tsx │ │ │ │ │ │ ├── LegalTerms.tsx │ │ │ │ │ │ ├── LongText.tsx │ │ │ │ │ │ ├── MultipleChoice.tsx │ │ │ │ │ │ ├── Number.tsx │ │ │ │ │ │ ├── OpinionScale.tsx │ │ │ │ │ │ ├── Payment.tsx │ │ │ │ │ │ ├── PhoneNumber.tsx │ │ │ │ │ │ ├── PictureChoice.tsx │ │ │ │ │ │ ├── Rating.tsx │ │ │ │ │ │ ├── ShortText.tsx │ │ │ │ │ │ ├── Signature.tsx │ │ │ │ │ │ ├── Statement.tsx │ │ │ │ │ │ ├── ThankYou.tsx │ │ │ │ │ │ ├── Website.tsx │ │ │ │ │ │ ├── Welcome.tsx │ │ │ │ │ │ ├── YesNo.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── FakeRadio.tsx │ │ │ │ │ ├── FakeSelect.tsx │ │ │ │ │ ├── FakeSubmit.tsx │ │ │ │ │ ├── FlagIcon.tsx │ │ │ │ │ ├── Layout.tsx │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── FieldConfig.ts │ │ │ │ │ ├── FieldIcon.tsx │ │ │ │ │ ├── LeftSidebar │ │ │ │ │ ├── CreateHiddenField.tsx │ │ │ │ │ ├── EditHiddenField.tsx │ │ │ │ │ ├── FieldCard.tsx │ │ │ │ │ ├── FieldKindIcon.tsx │ │ │ │ │ ├── FieldList.tsx │ │ │ │ │ ├── HiddenFieldCard.tsx │ │ │ │ │ ├── HiddenFields.tsx │ │ │ │ │ ├── InsertFieldDropdown.tsx │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── LogicBulkEditPanel │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── LogicFlow │ │ │ │ │ ├── ConnectionLine.tsx │ │ │ │ │ ├── CustomNode.tsx │ │ │ │ │ ├── Flow.tsx │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── LogicPanel │ │ │ │ │ ├── Action │ │ │ │ │ │ ├── FieldSelect.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Condition │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── PayloadForm.tsx │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── RichText │ │ │ │ │ ├── FloatingToolbar.tsx │ │ │ │ │ ├── MentionMenu.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── utils.ts │ │ │ │ │ ├── RightSidebar │ │ │ │ │ ├── Design │ │ │ │ │ │ ├── Customize │ │ │ │ │ │ │ ├── BackgroundBrightness.tsx │ │ │ │ │ │ │ ├── BackgroundImage.tsx │ │ │ │ │ │ │ ├── ColorPickerField.tsx │ │ │ │ │ │ │ ├── CustomCssHelpModal.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── Theme │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── style.scss │ │ │ │ │ ├── Logic │ │ │ │ │ │ ├── KindSelect.tsx │ │ │ │ │ │ ├── Rules.tsx │ │ │ │ │ │ ├── Variables.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Question │ │ │ │ │ │ ├── CoverImage.tsx │ │ │ │ │ │ ├── Layout.tsx │ │ │ │ │ │ ├── Settings │ │ │ │ │ │ │ ├── Basic.tsx │ │ │ │ │ │ │ ├── ConnectStripeModal.tsx │ │ │ │ │ │ │ ├── Date.tsx │ │ │ │ │ │ │ ├── MultipleChoice.tsx │ │ │ │ │ │ │ ├── OpinionScale.tsx │ │ │ │ │ │ │ ├── Payment.tsx │ │ │ │ │ │ │ ├── PhoneNumber.tsx │ │ │ │ │ │ │ ├── Rating.tsx │ │ │ │ │ │ │ ├── Statement.tsx │ │ │ │ │ │ │ ├── ThankYou.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── TypeSelect.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.tsx │ │ │ │ │ └── VariablePanel │ │ │ │ │ └── index.tsx │ │ │ ├── FormSettings │ │ │ │ ├── Basic.tsx │ │ │ │ ├── ExpirationDate.tsx │ │ │ │ ├── FormStatus.tsx │ │ │ │ ├── Protection.tsx │ │ │ │ ├── Sidebar.tsx │ │ │ │ ├── SubmissionArchive.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── style.scss │ │ │ ├── Integration │ │ │ │ ├── index.tsx │ │ │ │ └── views │ │ │ │ │ ├── AppItem.tsx │ │ │ │ │ └── Settings │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── views │ │ │ │ │ ├── CommonSettings.tsx │ │ │ │ │ ├── SettingsWrapper.tsx │ │ │ │ │ └── Summary.tsx │ │ │ ├── Render │ │ │ │ ├── CustomCode.tsx │ │ │ │ ├── PasswordCheck.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── utils │ │ │ │ │ ├── captcha.ts │ │ │ │ │ ├── payment.ts │ │ │ │ │ └── uploader.ts │ │ │ ├── Submissions │ │ │ │ ├── SelectedPanel.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── style.scss │ │ │ │ └── views │ │ │ │ │ ├── CategorySelect.tsx │ │ │ │ │ ├── ExportLink.tsx │ │ │ │ │ └── Sheet │ │ │ │ │ ├── Cell.tsx │ │ │ │ │ ├── Columns.tsx │ │ │ │ │ ├── DataGrid.tsx │ │ │ │ │ ├── FilterRow.tsx │ │ │ │ │ ├── GroupCell.tsx │ │ │ │ │ ├── GroupRow.tsx │ │ │ │ │ ├── HeaderCell.tsx │ │ │ │ │ ├── HeaderRow.tsx │ │ │ │ │ ├── Row.tsx │ │ │ │ │ ├── SheetHeaderCell.tsx │ │ │ │ │ ├── SheetKindIcon.tsx │ │ │ │ │ ├── SheetRowModal.tsx │ │ │ │ │ ├── SummaryCell.tsx │ │ │ │ │ ├── SummaryRow.tsx │ │ │ │ │ ├── formatters │ │ │ │ │ ├── SelectCellFormatter.tsx │ │ │ │ │ ├── ToggleGroupFormatter.tsx │ │ │ │ │ ├── ValueFormatter.tsx │ │ │ │ │ └── index.ts │ │ │ │ │ ├── headerCells │ │ │ │ │ └── SortableHeaderCell.tsx │ │ │ │ │ ├── hooks │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useFocusRef.ts │ │ │ │ │ ├── useGridDimensions.ts │ │ │ │ │ ├── useLatestFunc.ts │ │ │ │ │ ├── useViewportColumns.ts │ │ │ │ │ └── useViewportRows.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── sheetCells │ │ │ │ │ ├── AddressCell.tsx │ │ │ │ │ ├── DateRangeCell.tsx │ │ │ │ │ ├── DropPickerCell.tsx │ │ │ │ │ ├── FileUploadCell.tsx │ │ │ │ │ ├── FullNameCell.tsx │ │ │ │ │ ├── HiddenFieldCell.tsx │ │ │ │ │ ├── InputTableCell.tsx │ │ │ │ │ ├── MultipleChoiceCell.tsx │ │ │ │ │ ├── OpinionScaleCell.tsx │ │ │ │ │ ├── PaymentCell.tsx │ │ │ │ │ ├── PictureChoiceCell.tsx │ │ │ │ │ ├── SignatureCell.tsx │ │ │ │ │ ├── SubmitDateCell.tsx │ │ │ │ │ ├── TextCell.tsx │ │ │ │ │ ├── UrlCell.tsx │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── keyboardUtils.ts │ │ │ │ │ └── selectedCellUtils.ts │ │ │ └── views │ │ │ │ ├── CreateWorkspaceModal.tsx │ │ │ │ ├── FormComponents │ │ │ │ ├── Renderer.tsx │ │ │ │ ├── blocks │ │ │ │ │ ├── Address.tsx │ │ │ │ │ ├── Block.tsx │ │ │ │ │ ├── ClosedMessage.tsx │ │ │ │ │ ├── Country.tsx │ │ │ │ │ ├── Date.tsx │ │ │ │ │ ├── DateRange.tsx │ │ │ │ │ ├── Email.tsx │ │ │ │ │ ├── EmptyState.tsx │ │ │ │ │ ├── FileUpload.tsx │ │ │ │ │ ├── Form.tsx │ │ │ │ │ ├── FullName.tsx │ │ │ │ │ ├── InputTable.tsx │ │ │ │ │ ├── LegalTerms.tsx │ │ │ │ │ ├── LongText.tsx │ │ │ │ │ ├── MultipleChoice.tsx │ │ │ │ │ ├── Number.tsx │ │ │ │ │ ├── OpinionScale.tsx │ │ │ │ │ ├── Payment.tsx │ │ │ │ │ ├── PhoneNumber.tsx │ │ │ │ │ ├── PictureChoice.tsx │ │ │ │ │ ├── Rating.tsx │ │ │ │ │ ├── ShortText.tsx │ │ │ │ │ ├── Signature.tsx │ │ │ │ │ ├── Statement.tsx │ │ │ │ │ ├── ThankYou.tsx │ │ │ │ │ ├── Website.tsx │ │ │ │ │ ├── Welcome.tsx │ │ │ │ │ ├── YesNo.tsx │ │ │ │ │ └── hook.ts │ │ │ │ ├── components │ │ │ │ │ ├── ChoiceRadio.tsx │ │ │ │ │ ├── ChoiceRadioGroup.tsx │ │ │ │ │ ├── Countdown.tsx │ │ │ │ │ ├── CountrySelect.tsx │ │ │ │ │ ├── DateInput.tsx │ │ │ │ │ ├── DateRangeInput.tsx │ │ │ │ │ ├── FileUploader.tsx │ │ │ │ │ ├── FlagIcon.tsx │ │ │ │ │ ├── FormField.tsx │ │ │ │ │ ├── Icons │ │ │ │ │ │ ├── CollapseIcon.tsx │ │ │ │ │ │ ├── CrownIcon.tsx │ │ │ │ │ │ ├── EmotionIcon.tsx │ │ │ │ │ │ ├── LikeIcon.tsx │ │ │ │ │ │ ├── LinkIcon.tsx │ │ │ │ │ │ ├── LogoIcon.tsx │ │ │ │ │ │ ├── StarIcon.tsx │ │ │ │ │ │ ├── ThumbsUpIcon.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Input.tsx │ │ │ │ │ ├── Layout.tsx │ │ │ │ │ ├── PhoneNumberInput.tsx │ │ │ │ │ ├── Radio.tsx │ │ │ │ │ ├── RadioGroup.tsx │ │ │ │ │ ├── SelectHelper.tsx │ │ │ │ │ ├── SignaturePad.tsx │ │ │ │ │ ├── Slide.tsx │ │ │ │ │ ├── Submit.tsx │ │ │ │ │ ├── TableInput.tsx │ │ │ │ │ ├── TemporaryError.tsx │ │ │ │ │ ├── Textarea.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── consts │ │ │ │ │ ├── country.ts │ │ │ │ │ ├── date.ts │ │ │ │ │ ├── fileUpload.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── motion.ts │ │ │ │ │ ├── payment.ts │ │ │ │ │ └── rating.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── locales │ │ │ │ │ ├── cs.ts │ │ │ │ │ ├── de.ts │ │ │ │ │ ├── en.ts │ │ │ │ │ ├── es.ts │ │ │ │ │ ├── fr.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── pl.ts │ │ │ │ │ ├── pt-br.ts │ │ │ │ │ ├── tr.ts │ │ │ │ │ ├── zh-cn.ts │ │ │ │ │ └── zh-tw.ts │ │ │ │ ├── store.ts │ │ │ │ ├── theme.ts │ │ │ │ ├── typings.ts │ │ │ │ ├── utils │ │ │ │ │ ├── browser-language.ts │ │ │ │ │ ├── form.ts │ │ │ │ │ ├── hook.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── lru.ts │ │ │ │ │ ├── message.ts │ │ │ │ │ ├── script.ts │ │ │ │ │ └── timeout.ts │ │ │ │ └── views │ │ │ │ │ ├── Blocks.tsx │ │ │ │ │ ├── Branding.tsx │ │ │ │ │ ├── Footer.tsx │ │ │ │ │ ├── Header.tsx │ │ │ │ │ ├── Progress.tsx │ │ │ │ │ └── Sidebar.tsx │ │ │ │ ├── FormEmbedModal │ │ │ │ ├── EmbedPreview.tsx │ │ │ │ ├── FullpageSettings.tsx │ │ │ │ ├── ModalSettings.tsx │ │ │ │ ├── PopupSettings.tsx │ │ │ │ ├── StandardSettings.tsx │ │ │ │ ├── WidthInput.tsx │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ │ ├── FormNavbar │ │ │ │ ├── FormActions.tsx │ │ │ │ ├── Navigation.tsx │ │ │ │ ├── UserAccount.tsx │ │ │ │ └── index.tsx │ │ │ │ ├── FormPreviewModal │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ │ └── FormShareModal.tsx │ │ ├── home │ │ │ └── Home.tsx │ │ ├── project │ │ │ ├── Project │ │ │ │ ├── index.scss │ │ │ │ ├── index.tsx │ │ │ │ └── views │ │ │ │ │ ├── CreateForm.tsx │ │ │ │ │ └── RenameForm.tsx │ │ │ ├── Trash.tsx │ │ │ └── views │ │ │ │ ├── DeleteProject.tsx │ │ │ │ ├── ProjectLayout.tsx │ │ │ │ ├── ProjectMembers.tsx │ │ │ │ ├── RenameProject.tsx │ │ │ │ ├── Skeleton.tsx │ │ │ │ └── index.scss │ │ ├── user │ │ │ ├── UserSettings │ │ │ │ ├── Avatar.tsx │ │ │ │ ├── DeleteAccount.tsx │ │ │ │ ├── EmailAddress.tsx │ │ │ │ ├── Password.tsx │ │ │ │ ├── UserName.tsx │ │ │ │ └── index.tsx │ │ │ └── VerifyEmail.tsx │ │ └── workspace │ │ │ ├── CreateWorkspace.tsx │ │ │ ├── JoinWorkspace.tsx │ │ │ ├── Members │ │ │ ├── DeleteMember.tsx │ │ │ ├── InviteMember.tsx │ │ │ ├── LeaveWorkspace.tsx │ │ │ ├── TransferWorkspace.tsx │ │ │ └── index.tsx │ │ │ ├── Workspace │ │ │ ├── AssignMember.tsx │ │ │ ├── CreateProject.tsx │ │ │ └── index.tsx │ │ │ └── WorkspaceSettings │ │ │ ├── Branding.tsx │ │ │ ├── DeleteWorkspace.tsx │ │ │ └── index.tsx │ ├── router │ │ ├── config.ts │ │ └── index.tsx │ ├── service │ │ ├── app.service.ts │ │ ├── auth.service.ts │ │ ├── form.service.ts │ │ ├── index.ts │ │ ├── integration.service.ts │ │ ├── payment.service.ts │ │ ├── project.service.ts │ │ ├── submission.service.ts │ │ ├── template.service.ts │ │ ├── unsplash.service.ts │ │ ├── user.service.ts │ │ └── workspace.service.ts │ ├── store │ │ ├── app.store.ts │ │ ├── form.store.ts │ │ ├── index.ts │ │ ├── integration.store.ts │ │ ├── mobxStorage.ts │ │ ├── user.store.ts │ │ └── workspace.store.ts │ ├── styles │ │ ├── base.scss │ │ ├── components.scss │ │ └── index.scss │ ├── typings.d.ts │ └── utils │ │ ├── auth.ts │ │ ├── helper.ts │ │ ├── hook.ts │ │ ├── index.ts │ │ └── request.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.dockerignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | /.idea 3 | /.awcache 4 | /.cache 5 | /.vscode 6 | 7 | # tests 8 | **/coverage 9 | **/coverage-e2e 10 | **/logs 11 | .nyc_output 12 | 13 | **/node_modules 14 | .git 15 | .github 16 | .DS_Store 17 | 18 | # production 19 | docker.env 20 | **/.env.local 21 | **/.env.*.local 22 | **/.env.example 23 | docker-compose.yml 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: heyform 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Feature request 4 | url: https://github.com/orgs/heyform/discussions/new?category=ideas 5 | about: Request a feature to be added to the project 6 | - name: Self hosting questions 7 | url: https://github.com/orgs/heyform/discussions/new?category=self-hosting 8 | about: Ask questions and discuss running HeyForm with community members 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | package-lock.json 4 | 5 | # IDE 6 | /.idea 7 | /.awcache 8 | /.cache 9 | /.vscode 10 | 11 | # misc 12 | npm-debug.log 13 | yarn-error.log 14 | .DS_Store 15 | *.swp 16 | *.tar 17 | *.tar.gz 18 | *.zip 19 | *.p8 20 | 21 | # tests 22 | coverage 23 | coverage-e2e 24 | logs 25 | .nyc_output 26 | 27 | # environment 28 | .env 29 | .env.* 30 | docker.env 31 | !.env.example 32 | 33 | # production 34 | dist 35 | lib 36 | packages/server/static 37 | *.tsbuildinfo 38 | 39 | -------------------------------------------------------------------------------- /assets/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heyform/heyform/c8856912b787038c3b0f21c56640f081fe636e32/assets/images/screenshot.png -------------------------------------------------------------------------------- /packages/answer-utils/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "semi": false, 6 | "tabWidth": 2, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "avoid" 11 | } 12 | -------------------------------------------------------------------------------- /packages/answer-utils/src/answer-to-api-object.ts: -------------------------------------------------------------------------------- 1 | import { parsePlainAnswer } from './answer-to-plain' 2 | import { Answer } from '@heyform-inc/shared-types-enums' 3 | 4 | export function answersToApiObject(answers: Answer[]): Record<string, any> { 5 | const result: Record<string, any> = {} 6 | 7 | answers.forEach(answer => { 8 | const key = `(ID: ${answer.id}) ${answer.title}` 9 | result[key] = parsePlainAnswer(answer) 10 | }) 11 | 12 | return result 13 | } 14 | -------------------------------------------------------------------------------- /packages/answer-utils/src/consts.ts: -------------------------------------------------------------------------------- 1 | export const CURRENCY_SYMBOLS: Record<string, string> = { 2 | EUR: '\u20ac', 3 | GBP: '\xa3', 4 | USD: '#39;, 5 | AUD: 'A#39;, 6 | CAD: 'CA#39;, 7 | CHF: 'CHF ', 8 | NOK: 'NOK ', 9 | SEK: 'SEK ', 10 | DKK: 'DKK ', 11 | MXN: 'MX#39;, 12 | NZD: 'NZ#39;, 13 | BRL: 'R#39; 14 | } 15 | -------------------------------------------------------------------------------- /packages/answer-utils/src/flatten-fields.ts: -------------------------------------------------------------------------------- 1 | import { FieldKindEnum, FormField } from '@heyform-inc/shared-types-enums' 2 | import { helper } from '@heyform-inc/utils' 3 | 4 | export function flattenFields(fields?: FormField[], withGroup = false): FormField[] { 5 | if (helper.isEmpty(fields)) { 6 | return [] 7 | } 8 | 9 | return fields!.reduce((prev: FormField[], curr) => { 10 | if (curr.kind === FieldKindEnum.GROUP) { 11 | if (withGroup) { 12 | const group = { 13 | ...{}, 14 | ...curr, 15 | properties: { 16 | ...curr.properties, 17 | fields: [] 18 | } 19 | } 20 | 21 | return [...prev, group, ...(curr.properties?.fields || [])] 22 | } 23 | 24 | return [...prev, ...(curr.properties?.fields || [])] 25 | } 26 | return [...prev, curr] 27 | }, []) 28 | } 29 | -------------------------------------------------------------------------------- /packages/answer-utils/src/hidden-fields-to-html.ts: -------------------------------------------------------------------------------- 1 | import { HiddenFieldAnswer } from '@heyform-inc/shared-types-enums' 2 | 3 | export function hiddenFieldsToHtml(hiddenFields: HiddenFieldAnswer[]): string { 4 | if (!hiddenFields.length) return '' 5 | 6 | const html = hiddenFields 7 | .map(hiddenField => { 8 | return ` 9 | <li> 10 | <h3>${hiddenField.name}</h3> 11 | <p>${hiddenField.value}</p> 12 | </li> 13 | ` 14 | }) 15 | .join('') 16 | 17 | return `<ol>${html}</ol>` 18 | } 19 | -------------------------------------------------------------------------------- /packages/answer-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './answer-to-html' 2 | export * from './answer-to-api-object' 3 | export * from './answer-to-plain' 4 | export * from './answer-to-json' 5 | export * from './hidden-fields-to-html' 6 | export * from './field-values-to-answer' 7 | export * from './fields-to-validate-rules' 8 | export * from './validate' 9 | export * from './html-utils' 10 | export * from './flatten-fields' 11 | export * from './validate-condition' 12 | export * from './validate-payload' 13 | export * from './apply-logic-to-fields' 14 | export * from './consts' 15 | export * from './helper' 16 | -------------------------------------------------------------------------------- /packages/answer-utils/src/validate-payload.ts: -------------------------------------------------------------------------------- 1 | import { ActionEnum, ComparisonEnum, LogicPayload } from '@heyform-inc/shared-types-enums' 2 | import { helper } from '@heyform-inc/utils' 3 | 4 | const OTHER_COMPARISONS = [ComparisonEnum.IS_EMPTY, ComparisonEnum.IS_NOT_EMPTY] 5 | 6 | export function validatePayload(payload: LogicPayload): boolean { 7 | if ( 8 | !payload.action.kind || 9 | (!OTHER_COMPARISONS.includes(payload.condition.comparison) && 10 | helper.isEmpty((payload.condition as any).expected)) 11 | ) { 12 | return false 13 | } 14 | 15 | if (payload.action.kind === ActionEnum.NAVIGATE) { 16 | return helper.isValid(payload.action.fieldId) 17 | } 18 | 19 | return ( 20 | helper.isValid(payload.action.variable) && 21 | helper.isValid(payload.action.operator) && 22 | helper.isValid(payload.action.value) 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /packages/answer-utils/test/__snapshots__/address.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`correct value should be verified 1`] = ` 4 | [ 5 | { 6 | "id": "ADDRESS", 7 | "kind": "address", 8 | "properties": {}, 9 | "title": "address.test", 10 | "value": { 11 | "address1": "address1", 12 | "city": "city", 13 | "country": "country", 14 | "state": "state", 15 | "zip": "zip", 16 | }, 17 | }, 18 | ] 19 | `; 20 | 21 | exports[`undefined value should be verified if not required 1`] = ` 22 | [ 23 | { 24 | "id": "ADDRESS", 25 | "kind": "address", 26 | "properties": {}, 27 | "title": "address.test", 28 | "value": "", 29 | }, 30 | ] 31 | `; 32 | -------------------------------------------------------------------------------- /packages/answer-utils/test/__snapshots__/email.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`correct value should be verified 1`] = ` 4 | [ 5 | { 6 | "id": "EMAIL", 7 | "kind": "email", 8 | "properties": {}, 9 | "title": "number.test", 10 | "value": "email@example.com", 11 | }, 12 | ] 13 | `; 14 | 15 | exports[`undefined value should be verified if not required 1`] = ` 16 | [ 17 | { 18 | "id": "EMAIL", 19 | "kind": "email", 20 | "properties": {}, 21 | "title": "number.test", 22 | "value": "", 23 | }, 24 | ] 25 | `; 26 | -------------------------------------------------------------------------------- /packages/answer-utils/test/__snapshots__/number.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`correct value should be verified 1`] = ` 4 | [ 5 | { 6 | "id": "NUMBER", 7 | "kind": "number", 8 | "properties": {}, 9 | "title": "number.test", 10 | "value": 5, 11 | }, 12 | ] 13 | `; 14 | 15 | exports[`should be verified if value greater then 5 1`] = ` 16 | [ 17 | { 18 | "id": "NUMBER", 19 | "kind": "number", 20 | "properties": {}, 21 | "title": "number.test", 22 | "value": 7, 23 | }, 24 | ] 25 | `; 26 | 27 | exports[`undefined value should be verified if not required 1`] = ` 28 | [ 29 | { 30 | "id": "NUMBER", 31 | "kind": "number", 32 | "properties": {}, 33 | "title": "number.test", 34 | "value": "", 35 | }, 36 | ] 37 | `; 38 | -------------------------------------------------------------------------------- /packages/answer-utils/test/__snapshots__/phone-number.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`correct value should be verified 1`] = ` 4 | [ 5 | { 6 | "id": "PHONE_NUMBER", 7 | "kind": "phone_number", 8 | "properties": {}, 9 | "title": "phone-number.test", 10 | "value": "+12015550123", 11 | }, 12 | ] 13 | `; 14 | 15 | exports[`undefined value should be verified if not required 1`] = ` 16 | [ 17 | { 18 | "id": "PHONE_NUMBER", 19 | "kind": "phone_number", 20 | "properties": {}, 21 | "title": "phone-number.test", 22 | "value": "", 23 | }, 24 | ] 25 | `; 26 | -------------------------------------------------------------------------------- /packages/answer-utils/test/__snapshots__/url.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`correct value should be verified 1`] = ` 4 | [ 5 | { 6 | "id": "URL", 7 | "kind": "url", 8 | "properties": {}, 9 | "title": "url.test", 10 | "value": "https://www.google.com", 11 | }, 12 | ] 13 | `; 14 | 15 | exports[`undefined value should be verified if not required 1`] = ` 16 | [ 17 | { 18 | "id": "URL", 19 | "kind": "url", 20 | "properties": {}, 21 | "title": "url.test", 22 | "value": "", 23 | }, 24 | ] 25 | `; 26 | -------------------------------------------------------------------------------- /packages/answer-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "forceConsistentCasingInFileNames": true, 6 | "esModuleInterop": true, 7 | "noUnusedLocals": true, 8 | "noUnusedParameters": true, 9 | "strict": true, 10 | "removeComments": true, 11 | "allowSyntheticDefaultImports": true, 12 | "listEmittedFiles": true, 13 | "listFiles": true, 14 | "allowJs": false, 15 | "declaration": true, 16 | "sourceMap": true, 17 | "importHelpers": true, 18 | "resolveJsonModule": true, 19 | "skipLibCheck": true 20 | }, 21 | "exclude": [ 22 | "coverage", 23 | "lib", 24 | "node_modules" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /packages/answer-utils/tsup.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tsup').Options} */ 2 | module.exports = { 3 | target: 'esnext', 4 | dts: true, 5 | sourcemap: true, 6 | entry: ['src/index.ts'], 7 | format: ['cjs', 'esm'], 8 | splitting: false, 9 | treeshake: true, 10 | clean: true 11 | } 12 | -------------------------------------------------------------------------------- /packages/embed/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2020, 5 | "sourceType": "module", 6 | "project": "./tsconfig.json" 7 | }, 8 | "extends": [ 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:prettier/recommended", 12 | "prettier" 13 | ], 14 | "plugins": [ 15 | "@typescript-eslint" 16 | ], 17 | "env": { 18 | "browser": true, 19 | "es2021": true 20 | }, 21 | "rules": { 22 | "@typescript-eslint/no-unused-expressions": "off", 23 | "@typescript-eslint/no-explicit-any": "off", 24 | "prettier/prettier": "error" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/embed/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "semi": false, 6 | "tabWidth": 2, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "avoid", 11 | "plugins": ["@trivago/prettier-plugin-sort-imports"], 12 | "importOrder": [ 13 | "^@/", 14 | "^[../]", 15 | "^[./]" 16 | ], 17 | "importOrderSeparation": true, 18 | "importOrderSortSpecifiers": true, 19 | "importOrderGroupNamespaceSpecifiers": false 20 | } 21 | -------------------------------------------------------------------------------- /packages/embed/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: string 3 | export default content 4 | } 5 | -------------------------------------------------------------------------------- /packages/embed/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer') 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /packages/embed/src/assets/icon-close.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" 2 | stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 3 | <path stroke="none" d="M0 0h24v24H0z" fill="none"/> 4 | <path d="M18 6l-12 12"/> 5 | <path d="M6 6l12 12"/> 6 | </svg> -------------------------------------------------------------------------------- /packages/embed/src/assets/icon-loading.svg: -------------------------------------------------------------------------------- 1 | <svg width="22" height="5" viewBox="0 0 21 5" fill="none" xmlns="http://www.w3.org/2000/svg" class="heyform__loading"> 2 | <rect class="heyform__loading-dot" width="5" height="5" rx="2.5" fill="currentColor"/> 3 | <rect class="heyform__loading-dot" x="8" width="5" height="5" rx="2.5" fill="currentColor"/> 4 | <rect class="heyform__loading-dot" x="16" width="5" height="5" rx="2.5" fill="currentColor"/> 5 | </svg> -------------------------------------------------------------------------------- /packages/embed/src/assets/icon-message.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" 2 | stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 3 | <path stroke="none" d="M0 0h24v24H0z" fill="none"/> 4 | <path d="M12 11v.01"/> 5 | <path d="M8 11v.01"/> 6 | <path d="M16 11v.01"/> 7 | <path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3z"/> 8 | </svg> -------------------------------------------------------------------------------- /packages/embed/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common' 2 | export * from './dom' 3 | -------------------------------------------------------------------------------- /packages/embed/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | }, 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "target": "es5", 10 | "lib": ["es6", "dom"], 11 | "sourceMap": true, 12 | "allowJs": false, 13 | "skipLibCheck": true, 14 | "esModuleInterop": true, 15 | "allowSyntheticDefaultImports": true, 16 | "strict": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": true 22 | }, 23 | "include": ["src", "global.d.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/form-renderer/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "semi": false, 6 | "tabWidth": 2, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "bracketSameLine": false, 10 | "arrowParens": "avoid", 11 | "plugins": ["@trivago/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"], 12 | "importOrder": ["^@/", "^[../]", "^[./]"], 13 | "importOrderSeparation": true, 14 | "importOrderSortSpecifiers": true, 15 | "importOrderGroupNamespaceSpecifiers": false 16 | } 17 | -------------------------------------------------------------------------------- /packages/form-renderer/global-env.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | heyform: { 4 | device: { 5 | ios: boolean 6 | android: boolean 7 | mobile: boolean 8 | windowHeight: number 9 | screenHeight: number 10 | } 11 | } 12 | } 13 | } 14 | 15 | export {} 16 | -------------------------------------------------------------------------------- /packages/form-renderer/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: require("./tailwind.config.js"), 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/form-renderer/src/blocks/Statement.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import type { BlockProps } from './Block' 4 | import { Block } from './Block' 5 | import { Form } from './Form' 6 | 7 | export const Statement: FC<BlockProps> = ({ field, ...restProps }) => { 8 | return ( 9 | <Block className="heyform-statement heyform-empty-state" field={field} {...restProps}> 10 | <Form field={field} /> 11 | </Block> 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /packages/form-renderer/src/blocks/SuspendedMessage.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from '../utils' 2 | import { ThankYou } from './ThankYou' 3 | 4 | export const SuspendedMessage = () => { 5 | const { t } = useTranslation() 6 | 7 | const field: any = { 8 | title: t("This page doesn't exist"), 9 | description: t('If you have any questions, please contact us.') 10 | } 11 | 12 | return <ThankYou field={field} /> 13 | } 14 | -------------------------------------------------------------------------------- /packages/form-renderer/src/components/FlagIcon.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import type { FC } from 'react' 3 | 4 | import { IComponentProps } from '../typings' 5 | 6 | export interface FlagIconProps extends IComponentProps { 7 | countryCode?: string 8 | } 9 | 10 | export const FlagIcon: FC<FlagIconProps> = ({ className, countryCode = 'US' }) => { 11 | return <span className={clsx(`fi bg-black/10 fi-${countryCode?.toLowerCase()}`, className)} /> 12 | } 13 | -------------------------------------------------------------------------------- /packages/form-renderer/src/components/Icons/CollapseIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import { IComponentProps } from '../../typings' 4 | 5 | export const CollapseIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => ( 6 | <svg width="8" height="6" viewBox="0 0 8 6" xmlns="http://www.w3.org/2000/svg" {...props}> 7 | <path d="M4 6l4-6H0z" fillRule="evenodd" fill="currentColor"></path> 8 | </svg> 9 | ) 10 | -------------------------------------------------------------------------------- /packages/form-renderer/src/components/Icons/LikeIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import { IComponentProps } from '../../typings' 4 | 5 | export const LikeIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => { 6 | return ( 7 | <svg 8 | width="24" 9 | height="24" 10 | viewBox="0 0 24 24" 11 | fill="none" 12 | xmlns="http://www.w3.org/2000/svg" 13 | className="heyform-icon" 14 | {...props} 15 | > 16 | <path 17 | d="M7.5 4C4.46244 4 2 6.46245 2 9.5C2 15 8.5 20 12 21.1631C15.5 20 22 15 22 9.5C22 6.46245 19.5375 4 16.5 4C14.6399 4 12.9954 4.92345 12 6.3369C11.0046 4.92345 9.36015 4 7.5 4Z" 18 | className="heyform-icon-fill heyform-icon-stroke" 19 | strokeWidth="1" 20 | strokeLinecap="round" 21 | strokeLinejoin="round" 22 | /> 23 | </svg> 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /packages/form-renderer/src/components/Icons/StarIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import { IComponentProps } from '../../typings' 4 | 5 | export const StarIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => { 6 | return ( 7 | <svg 8 | width="24" 9 | height="24" 10 | viewBox="0 0 24 24" 11 | fill="none" 12 | xmlns="http://www.w3.org/2000/svg" 13 | className="heyform-icon" 14 | {...props} 15 | > 16 | <path 17 | d="M11.9993 2.5L8.9428 8.7388L2 9.74555L7.02945 14.6625L5.8272 21.5L11.9993 18.2096L18.1727 21.5L16.9793 14.6625L22 9.74555L15.0956 8.7388L11.9993 2.5Z" 18 | className="heyform-icon-fill heyform-icon-stroke" 19 | strokeWidth="1" 20 | strokeLinejoin="round" 21 | /> 22 | </svg> 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /packages/form-renderer/src/components/Icons/XIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import { IComponentProps } from '../../typings' 4 | 5 | export const XIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => { 6 | return ( 7 | <svg 8 | xmlns="http://www.w3.org/2000/svg" 9 | width="24" 10 | height="24" 11 | viewBox="0 0 24 24" 12 | fill="none" 13 | stroke="currentColor" 14 | strokeWidth="2" 15 | strokeLinecap="round" 16 | strokeLinejoin="round" 17 | {...props} 18 | > 19 | <path stroke="none" d="M0 0h24v24H0z" fill="none" /> 20 | <path d="M18 6l-12 12" /> 21 | <path d="M6 6l12 12" /> 22 | </svg> 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /packages/form-renderer/src/components/Icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CollapseIcon' 2 | export * from './CrownIcon' 3 | export * from './EmotionIcon' 4 | export * from './LikeIcon' 5 | export * from './LogoIcon' 6 | export * from './StarIcon' 7 | export * from './ThumbsUpIcon' 8 | export * from './XIcon' 9 | -------------------------------------------------------------------------------- /packages/form-renderer/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Button' 2 | export * from './ChoiceRadioGroup' 3 | export * from './CountrySelect' 4 | export * from './DateInput' 5 | export * from './DateRangeInput' 6 | export * from './FileUploader' 7 | export * from './FlagIcon' 8 | export * from './FormField' 9 | export * from './Icons' 10 | export * from './Input' 11 | export * from './Layout' 12 | export * from './Loader' 13 | export * from './PhoneNumberInput' 14 | export * from './Popup' 15 | export * from './Radio' 16 | export * from './RadioGroup' 17 | export * from './Rate' 18 | export * from './SelectHelper' 19 | export * from './SignaturePad' 20 | export * from './Slide' 21 | export * from './Submit' 22 | export * from './TableInput' 23 | export * from './TemporaryError' 24 | export * from './Textarea' 25 | export * from './Tooltip' 26 | -------------------------------------------------------------------------------- /packages/form-renderer/src/consts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './country' 2 | export * from './date' 3 | export * from './fileUpload' 4 | export * from './other' 5 | export * from './payment' 6 | export * from './rating' 7 | -------------------------------------------------------------------------------- /packages/form-renderer/src/consts/other.ts: -------------------------------------------------------------------------------- 1 | export const TRANSITION_UNMOUNTED_STATES = ['exited', 'unmounted'] 2 | export const CHAR_A_KEY_CODE = 65 3 | -------------------------------------------------------------------------------- /packages/form-renderer/src/consts/payment.ts: -------------------------------------------------------------------------------- 1 | import { AnyMap } from '../typings' 2 | 3 | export const CURRENCY_SYMBOLS: AnyMap = { 4 | EUR: '\u20ac', 5 | GBP: '\xa3', 6 | USD: '#39;, 7 | AUD: 'A#39;, 8 | CAD: 'CA#39;, 9 | CHF: 'CHF ', 10 | NOK: 'NOK ', 11 | SEK: 'SEK ', 12 | DKK: 'DKK ', 13 | MXN: 'MX#39;, 14 | NZD: 'NZ#39;, 15 | BRL: 'R#39; 16 | } 17 | -------------------------------------------------------------------------------- /packages/form-renderer/src/consts/rating.tsx: -------------------------------------------------------------------------------- 1 | import { CrownIcon, EmotionIcon, LikeIcon, StarIcon, ThumbsUpIcon } from '../components/Icons' 2 | import { AnyMap } from '../typings' 3 | 4 | export const RATING_SHAPE_ICONS: AnyMap = { 5 | heart: <LikeIcon />, 6 | thumb_up: <ThumbsUpIcon />, 7 | happy: <EmotionIcon />, 8 | crown: <CrownIcon />, 9 | star: <StarIcon /> 10 | } 11 | -------------------------------------------------------------------------------- /packages/form-renderer/src/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next' 2 | import { initReactI18next } from 'react-i18next' 3 | 4 | import { locales } from './locales' 5 | import { AnyMap } from './typings' 6 | 7 | export function initI18n(fallbackLng = 'en', customLocales?: AnyMap) { 8 | const resources = customLocales || locales 9 | 10 | i18n.use(initReactI18next).init({ 11 | lowerCaseLng: true, 12 | supportedLngs: Object.keys(resources), 13 | fallbackLng, 14 | resources, 15 | interpolation: { 16 | escapeValue: false 17 | }, 18 | react: { 19 | // https://react.i18next.com/latest/trans-component#trans-props 20 | transSupportBasicHtmlNodes: true, 21 | transKeepBasicHtmlNodesFor: ['br', 'strong', 'b', 'i', 'a'] 22 | } 23 | }) 24 | 25 | return i18n 26 | } 27 | -------------------------------------------------------------------------------- /packages/form-renderer/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components/Button' 2 | export * from './components/FlagIcon' 3 | export * from './components/Input' 4 | export { AutoResizeTextarea } from './components/Textarea' 5 | export * from './components/Rate' 6 | export * from './consts' 7 | export * from './blocks/SuspendedMessage' 8 | export * from './i18n' 9 | export * from './locales' 10 | export * from './Renderer' 11 | export * from './theme' 12 | export * from './utils' 13 | -------------------------------------------------------------------------------- /packages/form-renderer/src/typings.ts: -------------------------------------------------------------------------------- 1 | import { FieldKindEnum, FormField, FormModel } from '@heyform-inc/shared-types-enums' 2 | import type { HTMLAttributes } from 'react' 3 | 4 | export interface IFormField extends FormField { 5 | parent?: IFormField 6 | isTouched?: boolean 7 | } 8 | 9 | export interface IFormModel extends FormModel { 10 | fields: IFormField[] 11 | } 12 | 13 | export interface IPartialFormField { 14 | id: string 15 | index: number 16 | title?: string | any[] 17 | kind: FieldKindEnum 18 | required?: boolean 19 | children?: IPartialFormField[] 20 | } 21 | 22 | export type AnyMap<T = any> = Record<string, T> 23 | export type IComponentProps<E = HTMLElement> = HTMLAttributes<E> 24 | -------------------------------------------------------------------------------- /packages/form-renderer/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common' 2 | export * from './form' 3 | export * from './hook' 4 | export * from './lru' 5 | export * from './message' 6 | export * from './script' 7 | export { default as GlobalTimeout, Timeout } from './timeout' 8 | -------------------------------------------------------------------------------- /packages/form-renderer/src/utils/message.ts: -------------------------------------------------------------------------------- 1 | export function sendMessageToParent(eventName: string) { 2 | window.parent?.postMessage( 3 | { 4 | source: 'HEYFORM', 5 | eventName 6 | }, 7 | '*' 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /packages/form-renderer/src/utils/timeout.ts: -------------------------------------------------------------------------------- 1 | import { AnyMap } from '../typings' 2 | 3 | interface TimeoutProps { 4 | name: string 5 | duration: number 6 | callback: () => void 7 | } 8 | 9 | export class Timeout { 10 | private readonly caches: AnyMap<number> = {} 11 | 12 | add({ name, duration, callback }: TimeoutProps) { 13 | this.caches[name] = setTimeout(callback, duration) 14 | } 15 | 16 | remove(name: string) { 17 | const cache = this.caches[name] 18 | 19 | if (cache) { 20 | clearTimeout(cache) 21 | delete this.caches[name] 22 | } 23 | } 24 | 25 | clear() { 26 | Object.keys(this.caches).forEach(key => { 27 | this.remove(key) 28 | }) 29 | } 30 | } 31 | 32 | export default new Timeout() 33 | -------------------------------------------------------------------------------- /packages/form-renderer/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme') 2 | 3 | module.exports = { 4 | content: ['./index.html', './src/**/*.{js,jsx,ts,tsx}'], 5 | theme: { 6 | fontFamily: { 7 | sans: ['Inter', ...defaultTheme.fontFamily.sans], 8 | }, 9 | extend: { 10 | } 11 | }, 12 | variants: { 13 | extend: {} 14 | }, 15 | plugins: [require('tailwindcss-animate'), require('@tailwindcss/forms')] 16 | } 17 | -------------------------------------------------------------------------------- /packages/form-renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "lib": ["dom", "dom.iterable", "esnext"], 7 | "allowJs": false, 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src", "global-env.d.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/form-renderer/tsup.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tsup').Options} */ 2 | module.exports = { 3 | target: 'esnext', 4 | dts: true, 5 | sourcemap: true, 6 | entry: ['src/index.ts'], 7 | format: ['cjs', 'esm'], 8 | splitting: false, 9 | treeshake: true, 10 | clean: true, 11 | esbuildOptions(options) { 12 | options.charset = 'utf8' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/server/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "plugin:@typescript-eslint/eslint-recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "prettier/@typescript-eslint", 7 | "plugin:prettier/recommended" 8 | ], 9 | "rules": { 10 | "@typescript-eslint/explicit-function-return-type": "off", 11 | "@typescript-eslint/ban-ts-ignore": "off", 12 | "@typescript-eslint/camelcase": "off", 13 | "@typescript-eslint/ban-ts-comment": "off", 14 | "@typescript-eslint/ban-types": "off", 15 | "@typescript-eslint/no-var-requires": "off", 16 | "prettier/prettier": "error" 17 | }, 18 | "env": { 19 | "node": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/server/.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true 2 | -------------------------------------------------------------------------------- /packages/server/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "semi": false, 6 | "tabWidth": 2, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "avoid", 11 | "importOrder": ["^@heyform-inc/", "^@(decorator|graphql|guard|dto|interceptor|middleware|config|environments|controller|model|resolver|service|schedule|utils)", "^[.+/]"], 12 | "importOrderSeparation": true, 13 | "importOrderSortSpecifiers": true, 14 | "importOrderGroupNamespaceSpecifiers": false, 15 | "importOrderParserPlugins": [ 16 | "typescript", 17 | "decorators-legacy" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/server/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /packages/server/resources/email-templates/account_deletion_alert.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Your HeyForm account has been deleted 3 | --- 4 | 5 | <p> 6 | Your HeyForm account has been deleted. We've processed the request to delete your account. 7 | </p> 8 | 9 | <p> 10 | All the data, including but not limited to the account, workspaces, projects, forms, and submissions, has been 11 | permanently deleted. 12 | </p> 13 | -------------------------------------------------------------------------------- /packages/server/resources/email-templates/account_deletion_request.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Verify your HeyForm account deletion request 3 | --- 4 | 5 | <p>We're sorry to say goodbye. You have requested to delete your HeyForm account. The verification code to proceed with the 6 | delete request is</p> 7 | 8 | <p style="font-weight: bold"> 9 | <code>{code}</code> 10 | </p> 11 | -------------------------------------------------------------------------------- /packages/server/resources/email-templates/email_verification_request.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Verify your HeyForm email address 3 | --- 4 | 5 | <p>To continue setting up your HeyForm account, we need to confirm your email address. Please enter this code in your 6 | browser.</p> 7 | 8 | <p style="font-weight: bold;"> 9 | <code>{code}</code> 10 | </p> -------------------------------------------------------------------------------- /packages/server/resources/email-templates/password_change_alert.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Your HeyForm password has been updated 3 | --- 4 | 5 | <p>Your HeyForm password has been changed.</p> 6 | <p> 7 | If you did not make these changes or you believe an unauthorized person has accessed your account, you should visit the dashboard immediately to reset your password. 8 | </p> -------------------------------------------------------------------------------- /packages/server/resources/email-templates/project_deletion_alert.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Your HeyForm project has been deleted 3 | --- 4 | 5 | <p> 6 | This email is to confirm that the <span style="font-weight: bold">{projectName}</span> project has been deleted from 7 | <span style="font-weight: bold">{teamName}</span> workspace by <span style="font-weight: bold">{userName}</span> 8 | </p> 9 | 10 | <p>All your forms and submissions associated with the <span style="font-weight: bold">{projectName}</span> project have 11 | been deleted from the system. 12 | </p> -------------------------------------------------------------------------------- /packages/server/resources/email-templates/project_deletion_request.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Verify your project deletion request 3 | --- 4 | 5 | <p>You have requested the deletion of the <span style="font-weight: bold;">{projectName}</span> project from <span style="font-weight: bold;">{teamName}</span> workspace. The verification code to proceed with the delete request is</p> 6 | <p> 7 | <span style="font-weight: bold;">{code}</span> 8 | </p> 9 | -------------------------------------------------------------------------------- /packages/server/resources/email-templates/schedule_account_deletion_alert.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: You have scheduled your HeyForm account for deletion 3 | --- 4 | 5 | <p>We've received the request to delete your HeyForm account. Our system will permanently delete all of your 6 | data, including but not limited to account, workspaces, projects, forms and submissions in 48 hours.</p> 7 | <p> 8 | The deletion was requested by <strong>{fullName} ({email})</strong>. 9 | </p> 10 | <p> 11 | Having a second thought? Please login to your HeyForm account and select the option to <strong>Cancel scheduled 12 | deletion</strong>. You must do this within 48 hours of the request to prevent permanent data loss. 13 | </p> 14 | <p> 15 | NB: Your decision is final. HeyForm will not be able to recover your data after deletion. 16 | </p> -------------------------------------------------------------------------------- /packages/server/resources/email-templates/submission_notification.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: A new submission was received for {formName} 3 | --- 4 | 5 | <p>You have just received a new form submission. Here it goes:</p> 6 | 7 | <div> 8 | {submission} 9 | 10 | <p> 11 | <a href="{link}">View All Submissions</a> 12 | </p> 13 | </div> 14 | -------------------------------------------------------------------------------- /packages/server/resources/email-templates/team_deletion_alert.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: You HeyForm workspace has been deleted 3 | --- 4 | 5 | <p> 6 | This email is to confirm that the '{teamName}' workspace has been deleted by '{userName}'. Your workspace's projects, forms and submissions have been deleted from the system. 7 | </p> 8 | -------------------------------------------------------------------------------- /packages/server/resources/email-templates/team_deletion_request.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Verify your HeyForm workspace deletion request 3 | --- 4 | 5 | <p>You have requested the deletion of the {teamName} HeyForm workspace. The verification code to proceed with 6 | the delete request is</p> 7 | <p> 8 | <span style="font-weight: bold;">{code}</span> 9 | </p> -------------------------------------------------------------------------------- /packages/server/resources/email-templates/team_invitation.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: {userName} invited you to '{teamName}' workspace 3 | --- 4 | 5 | <p>{userName} has invited you to collaborate on the {teamName} workspace.</p> 6 | <p> 7 | <a href="{link}">View Invitation</a> 8 | </p> 9 | <p>This invitation will expire in 7 days.</p> 10 | <p> 11 | Note: This invitation was intended for {email}. If you were not 12 | expecting this invitation, you can ignore this email. 13 | </p> -------------------------------------------------------------------------------- /packages/server/src/common/decorator/auth.decorator.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards, applyDecorators } from '@nestjs/common' 2 | 3 | import { AuthGuard, BrowserIdGuard } from '@guard' 4 | 5 | export function Auth(): any { 6 | return applyDecorators(UseGuards(BrowserIdGuard, AuthGuard)) 7 | } 8 | -------------------------------------------------------------------------------- /packages/server/src/common/decorator/data-mask-options.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common' 2 | import { CustomDecorator } from '@nestjs/common/decorators/core/set-metadata.decorator' 3 | import { ClassTransformOptions } from 'class-transformer' 4 | 5 | import { TypeFunc } from '@interceptor' 6 | 7 | export const DATA_MASK_OPTIONS = 'DATA_MASK_OPTIONS' 8 | 9 | export const DataMaskOptions = ( 10 | typeFunc: TypeFunc, 11 | options?: ClassTransformOptions 12 | ): CustomDecorator<any> => { 13 | return SetMetadata(DATA_MASK_OPTIONS, { 14 | ...(options || {}), 15 | typeFunc 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /packages/server/src/common/decorator/form.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, createParamDecorator } from '@nestjs/common' 2 | import { GqlExecutionContext } from '@nestjs/graphql' 3 | 4 | import { helper } from '@heyform-inc/utils' 5 | 6 | import { UserModel } from '@model' 7 | 8 | /** 9 | * Get req.form attached to AuthGuard (guard/permission.guard.ts) 10 | */ 11 | export const Form = createParamDecorator( 12 | (_: any, context: ExecutionContext): UserModel => { 13 | const ctx = GqlExecutionContext.create(context) 14 | let { req } = ctx.getContext() 15 | 16 | if (helper.isEmpty(req)) { 17 | req = context.switchToHttp().getRequest() 18 | } 19 | 20 | return req.form 21 | } 22 | ) 23 | -------------------------------------------------------------------------------- /packages/server/src/common/decorator/graphql.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, createParamDecorator } from '@nestjs/common' 2 | import { GqlExecutionContext } from '@nestjs/graphql' 3 | 4 | export const GraphqlRequest = createParamDecorator((_: any, context: ExecutionContext) => { 5 | const ctx = GqlExecutionContext.create(context) 6 | const { req } = ctx.getContext() 7 | return req 8 | }) 9 | 10 | export const GraphqlResponse = createParamDecorator((_: any, context: ExecutionContext) => { 11 | const ctx = GqlExecutionContext.create(context) 12 | const { req } = ctx.getContext() 13 | return req.res 14 | }) 15 | -------------------------------------------------------------------------------- /packages/server/src/common/decorator/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.decorator' 2 | export * from './graphql.decorator' 3 | export * from './team.decorator' 4 | export * from './form.decorator' 5 | export * from './user.decorator' 6 | export * from './data-mask-options.decorator' 7 | export * from './project.decorator' 8 | export * from './permission.decorator' 9 | -------------------------------------------------------------------------------- /packages/server/src/common/decorator/permission.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata, UseGuards, applyDecorators } from '@nestjs/common' 2 | 3 | import { PermissionGuard, PermissionScopeEnum } from '@guard' 4 | 5 | export function TeamGuard(): any { 6 | return applyDecorators(SetMetadata('scope', PermissionScopeEnum.team), UseGuards(PermissionGuard)) 7 | } 8 | 9 | export function ProjectGuard(): any { 10 | return applyDecorators( 11 | SetMetadata('scope', PermissionScopeEnum.project), 12 | UseGuards(PermissionGuard) 13 | ) 14 | } 15 | 16 | export function FormGuard(): any { 17 | return applyDecorators(SetMetadata('scope', PermissionScopeEnum.form), UseGuards(PermissionGuard)) 18 | } 19 | -------------------------------------------------------------------------------- /packages/server/src/common/decorator/project.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, createParamDecorator } from '@nestjs/common' 2 | import { GqlExecutionContext } from '@nestjs/graphql' 3 | 4 | import { helper } from '@heyform-inc/utils' 5 | 6 | import { UserModel } from '@model' 7 | 8 | /** 9 | * Get req.project attached to AuthGuard (guard/permission.guard.ts) 10 | */ 11 | export const Project = createParamDecorator( 12 | (_: any, context: ExecutionContext): UserModel => { 13 | const ctx = GqlExecutionContext.create(context) 14 | let { req } = ctx.getContext() 15 | 16 | if (helper.isEmpty(req)) { 17 | req = context.switchToHttp().getRequest() 18 | } 19 | 20 | return req.project 21 | } 22 | ) 23 | -------------------------------------------------------------------------------- /packages/server/src/common/decorator/team.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, createParamDecorator } from '@nestjs/common' 2 | import { GqlExecutionContext } from '@nestjs/graphql' 3 | 4 | import { helper } from '@heyform-inc/utils' 5 | 6 | import { UserModel } from '@model' 7 | 8 | /** 9 | * Get req.team attached to AuthGuard (guard/permission.guard.ts) 10 | */ 11 | export const Team = createParamDecorator( 12 | (_: any, context: ExecutionContext): UserModel => { 13 | const ctx = GqlExecutionContext.create(context) 14 | let { req } = ctx.getContext() 15 | 16 | if (helper.isEmpty(req)) { 17 | req = context.switchToHttp().getRequest() 18 | } 19 | 20 | return req.team 21 | } 22 | ) 23 | -------------------------------------------------------------------------------- /packages/server/src/common/decorator/user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, createParamDecorator } from '@nestjs/common' 2 | import { GqlExecutionContext } from '@nestjs/graphql' 3 | 4 | import { helper } from '@heyform-inc/utils' 5 | 6 | import { UserModel } from '@model' 7 | 8 | /** 9 | * Get req.user attached to AuthGuard (guard/auth.guard.ts) 10 | */ 11 | export const User = createParamDecorator( 12 | (_: any, context: ExecutionContext): UserModel => { 13 | const ctx = GqlExecutionContext.create(context) 14 | let { req } = ctx.getContext() 15 | 16 | if (helper.isEmpty(req)) { 17 | req = context.switchToHttp().getRequest() 18 | } 19 | 20 | return req.user 21 | } 22 | ) 23 | -------------------------------------------------------------------------------- /packages/server/src/common/dto/index.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer' 2 | import { IsInt, IsOptional, IsString, IsUrl } from 'class-validator' 3 | import { APP_HOMEPAGE_URL } from '@environments' 4 | 5 | export class ImageResizingDto { 6 | @IsUrl({ 7 | require_protocol: true, 8 | host_whitelist: [ 9 | '127.0.0.1', 10 | 'localhost', 11 | 'secure.gravatar.com', 12 | 'googleusercontent.com', 13 | 'images.unsplash.com', 14 | 'unsplash.com', 15 | new URL(APP_HOMEPAGE_URL).hostname 16 | ] 17 | }) 18 | url: string 19 | 20 | @Transform(parseInt) 21 | @IsInt() 22 | @IsOptional() 23 | w?: number 24 | 25 | @Transform(parseInt) 26 | @IsInt() 27 | @IsOptional() 28 | h?: number 29 | } 30 | 31 | export class ExportSubmissionsDto { 32 | @IsString() 33 | formId: string 34 | } 35 | -------------------------------------------------------------------------------- /packages/server/src/common/filter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './all-exceptions.filter' 2 | -------------------------------------------------------------------------------- /packages/server/src/common/graphql/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.graphql' 2 | export * from './endpoint.graphql' 3 | export * from './form.graphql' 4 | export * from './integration.graphql' 5 | export * from './submission.graphql' 6 | export * from './team.graphql' 7 | export * from './payment.graphql' 8 | export * from './user.graphql' 9 | export * from './app.graphql' 10 | export * from './template.graphql' 11 | export * from './project.graphql' 12 | export * from './unsplash.graphql' 13 | -------------------------------------------------------------------------------- /packages/server/src/common/graphql/label.graphql.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heyform/heyform/c8856912b787038c3b0f21c56640f081fe636e32/packages/server/src/common/graphql/label.graphql.ts -------------------------------------------------------------------------------- /packages/server/src/common/graphql/payment.graphql.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType, ObjectType } from '@nestjs/graphql' 2 | 3 | import { FormDetailInput } from './form.graphql' 4 | 5 | @InputType() 6 | export class ConnectStripeInput extends FormDetailInput { 7 | @Field() 8 | state: string 9 | 10 | @Field() 11 | code: string 12 | } 13 | 14 | @ObjectType() 15 | export class ConnectStripeType { 16 | @Field() 17 | accountId: string 18 | 19 | @Field() 20 | email: string 21 | } 22 | -------------------------------------------------------------------------------- /packages/server/src/common/graphql/unsplash.graphql.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType, ObjectType } from '@nestjs/graphql' 2 | 3 | @InputType() 4 | export class UnsplashSearchInput { 5 | @Field({ nullable: true }) 6 | keyword?: string 7 | 8 | @Field({ nullable: true }) 9 | page?: number 10 | } 11 | 12 | @ObjectType() 13 | export class UnsplashImageType { 14 | @Field() 15 | id: string 16 | 17 | @Field() 18 | url: string 19 | 20 | @Field() 21 | thumbUrl: string 22 | 23 | @Field({ nullable: true }) 24 | downloadUrl?: string 25 | 26 | @Field() 27 | author: string 28 | 29 | @Field() 30 | authorUrl: string 31 | } 32 | 33 | @InputType() 34 | export class UnsplashTrackDownloadInput { 35 | @Field() 36 | downloadUrl: string 37 | } 38 | -------------------------------------------------------------------------------- /packages/server/src/common/guard/endpoint-anonymous-id.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common' 2 | import { GqlExecutionContext } from '@nestjs/graphql' 3 | 4 | import { helper } from '@heyform-inc/utils' 5 | 6 | @Injectable() 7 | export class EndpointAnonymousIdGuard implements CanActivate { 8 | async canActivate(context: ExecutionContext): Promise<boolean> { 9 | const ctx = GqlExecutionContext.create(context) 10 | const { req } = ctx.getContext() 11 | const anonymousId = req.get('x-anonymous-id') 12 | 13 | if (helper.isEmpty(anonymousId)) { 14 | throw new ForbiddenException('Forbidden request error') 15 | } 16 | 17 | return true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/server/src/common/guard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.guard' 2 | export * from './browser-id.guard' 3 | export * from './endpoint-anonymous-id.guard' 4 | export * from './role.guard' 5 | export * from './permission.guard' 6 | -------------------------------------------------------------------------------- /packages/server/src/common/interceptor/index.ts: -------------------------------------------------------------------------------- 1 | export * from './data-mask.interceptor' 2 | -------------------------------------------------------------------------------- /packages/server/src/common/middleware/form-body.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestMiddleware } from '@nestjs/common' 2 | import * as bodyParser from 'body-parser' 3 | import { Request, Response } from 'express' 4 | 5 | @Injectable() 6 | export class FormBodyMiddleware implements NestMiddleware { 7 | use(req: Request, res: Response, next: () => any) { 8 | bodyParser.urlencoded({ 9 | limit: '1mb', 10 | extended: true 11 | })(req, res, next) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/server/src/common/middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from './form-body.middleware' 2 | export * from './json-body.middleware' 3 | export * from './raw-body.middleware' 4 | -------------------------------------------------------------------------------- /packages/server/src/common/middleware/json-body.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestMiddleware } from '@nestjs/common' 2 | import * as bodyParser from 'body-parser' 3 | import { Request, Response } from 'express' 4 | 5 | @Injectable() 6 | export class JsonBodyMiddleware implements NestMiddleware { 7 | use(req: Request, res: Response, next: () => any) { 8 | bodyParser.json({ 9 | limit: '1mb' 10 | })(req, res, next) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/server/src/common/middleware/raw-body.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestMiddleware } from '@nestjs/common' 2 | import * as bodyParser from 'body-parser' 3 | import { Request, Response } from 'express' 4 | 5 | @Injectable() 6 | export class RawBodyMiddleware implements NestMiddleware { 7 | use(req: Request, res: Response, next: () => any) { 8 | bodyParser.raw({ type: '*/*' })(req, res, next) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/server/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bull' 2 | export * from './graphql' 3 | export * from './mongo' 4 | export * from './redis' 5 | export * from './cookie' 6 | export * from './smtp' 7 | export * from './upload' 8 | -------------------------------------------------------------------------------- /packages/server/src/config/redis/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RedisModuleOptions, 3 | RedisModuleOptionsFactory 4 | } from '@svtslv/nestjs-ioredis/dist/redis.interfaces' 5 | 6 | import { REDIS_DB, REDIS_HOST, REDIS_PASSWORD, REDIS_PORT } from '@environments' 7 | 8 | export class RedisService implements RedisModuleOptionsFactory { 9 | createRedisModuleOptions(): Promise<RedisModuleOptions> | RedisModuleOptions { 10 | return { 11 | config: { 12 | host: REDIS_HOST, 13 | port: REDIS_PORT, 14 | password: REDIS_PASSWORD, 15 | db: REDIS_DB 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/server/src/config/smtp/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SMTP_HOST, 3 | SMTP_PASSWORD, 4 | SMTP_PORT, 5 | SMTP_USER, 6 | SMTP_SECURE, 7 | SMTP_IGNORE_CERT, 8 | SMTP_SERVERNAME 9 | } from '@environments' 10 | import { SmtpOptions } from '@utils' 11 | 12 | export const SmtpOptionsFactory = (): SmtpOptions => ({ 13 | host: SMTP_HOST, 14 | port: SMTP_PORT, 15 | user: SMTP_USER, 16 | password: SMTP_PASSWORD, 17 | secure: SMTP_SECURE, 18 | servername: SMTP_SERVERNAME, 19 | ignoreCert: SMTP_IGNORE_CERT, 20 | pool: true, 21 | logger: false 22 | }) 23 | -------------------------------------------------------------------------------- /packages/server/src/controller/connect-stripe.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Query, Res } from '@nestjs/common' 2 | import { Response } from 'express' 3 | 4 | @Controller() 5 | export class ConnectStripeController { 6 | @Get('/connect/stripe/callback') 7 | async index(@Query() query: Record<string, string>, @Res() res: Response) { 8 | return res.render('connect-stripe', { 9 | rendererData: query 10 | }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/server/src/controller/form.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Res } from '@nestjs/common' 2 | import { Response } from 'express' 3 | 4 | @Controller() 5 | export class FormController { 6 | @Get('/form/*') 7 | index(@Res() res: Response) { 8 | return res.render('index', { 9 | rendererData: {} 10 | }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/server/src/controller/health.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common' 2 | 3 | @Controller() 4 | export class HealthController { 5 | @Get('/health') 6 | index() { 7 | return 'OK' 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/server/src/model/form-analytic.model.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' 2 | import { Document } from 'mongoose' 3 | 4 | @Schema({ 5 | timestamps: true 6 | }) 7 | export class FormAnalyticModel extends Document { 8 | @Prop({ required: true, index: true }) 9 | formId: string 10 | 11 | @Prop({ required: true }) 12 | totalVisits: number 13 | } 14 | 15 | export const FormAnalyticSchema = SchemaFactory.createForClass(FormAnalyticModel) 16 | -------------------------------------------------------------------------------- /packages/server/src/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.model' 2 | export * from './form.model' 3 | export * from './form-analytic.model' 4 | export * from './form-report.model' 5 | export * from './integration.model' 6 | export * from './integration-record.model' 7 | export * from './project.model' 8 | export * from './project-group.model' 9 | export * from './project-member.model' 10 | export * from './submission.model' 11 | export * from './submission-ip-limit.model' 12 | export * from './team.model' 13 | export * from './team-activity.model' 14 | export * from './team-invitation.model' 15 | export * from './team-member.model' 16 | export * from './template.model' 17 | export * from './user.model' 18 | export * from './user-social-account.model' 19 | -------------------------------------------------------------------------------- /packages/server/src/model/integration-record.model.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' 2 | 3 | import { IntegrationModel } from './integration.model' 4 | 5 | @Schema({ 6 | timestamps: true 7 | }) 8 | export class IntegrationRecordModel extends IntegrationModel { 9 | @Prop({ required: true }) 10 | integrationId: string 11 | 12 | @Prop({ type: Map }) 13 | response?: Record<string, any> 14 | } 15 | 16 | export const IntegrationRecordSchema = SchemaFactory.createForClass(IntegrationRecordModel) 17 | -------------------------------------------------------------------------------- /packages/server/src/model/project-group.model.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' 2 | import { Document } from 'mongoose' 3 | 4 | @Schema({ 5 | timestamps: true 6 | }) 7 | export class ProjectGroupModel extends Document { 8 | @Prop({ required: true, index: true }) 9 | projectId: string 10 | 11 | @Prop({ required: true, index: true }) 12 | groupId: string 13 | } 14 | 15 | export const ProjectGroupSchema = SchemaFactory.createForClass(ProjectGroupModel) 16 | 17 | ProjectGroupSchema.index({ projectId: 1, groupId: 1 }, { unique: true }) 18 | -------------------------------------------------------------------------------- /packages/server/src/model/project-member.model.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' 2 | import { Document } from 'mongoose' 3 | 4 | @Schema() 5 | export class ProjectMemberModel extends Document { 6 | @Prop({ required: true, index: true }) 7 | projectId: string 8 | 9 | @Prop({ required: true, index: true }) 10 | memberId: string 11 | } 12 | 13 | export const ProjectMemberSchema = SchemaFactory.createForClass(ProjectMemberModel) 14 | 15 | ProjectMemberSchema.index({ projectId: 1, memberId: 1 }, { unique: true }) 16 | -------------------------------------------------------------------------------- /packages/server/src/model/submission-ip-limit.model.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' 2 | import { Document } from 'mongoose' 3 | 4 | @Schema({ 5 | timestamps: true 6 | }) 7 | export class SubmissionIpLimitModel extends Document { 8 | @Prop({ required: true }) 9 | formId: string 10 | 11 | @Prop({ required: true }) 12 | ip: string 13 | 14 | @Prop({ required: true }) 15 | count: number 16 | 17 | @Prop({ required: true, index: true }) 18 | expiredAt: number 19 | } 20 | 21 | export const SubmissionIpLimitSchema = SchemaFactory.createForClass(SubmissionIpLimitModel) 22 | 23 | SubmissionIpLimitSchema.index({ formId: 1, ip: 1 }, { unique: true }) 24 | -------------------------------------------------------------------------------- /packages/server/src/model/team-invitation.model.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' 2 | import { Document } from 'mongoose' 3 | 4 | @Schema({ 5 | timestamps: true 6 | }) 7 | export class TeamInvitationModel extends Document { 8 | @Prop({ required: true }) 9 | teamId: string 10 | 11 | @Prop({ required: true }) 12 | email: string 13 | 14 | @Prop({ required: true }) 15 | expireAt: number 16 | } 17 | 18 | export const TeamInvitationSchema = SchemaFactory.createForClass(TeamInvitationModel) 19 | 20 | // Unique constraint on name and lang 21 | TeamInvitationSchema.index({ teamId: 1, email: 1 }, { unique: true }) 22 | -------------------------------------------------------------------------------- /packages/server/src/model/team-member.model.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' 2 | import { Document } from 'mongoose' 3 | 4 | export enum TeamRoleEnum { 5 | OWNER, 6 | ADMIN, 7 | COLLABORATOR, 8 | MEMBER 9 | } 10 | 11 | @Schema() 12 | export class TeamMemberModel extends Document { 13 | @Prop({ required: true, index: true }) 14 | teamId: string 15 | 16 | @Prop({ required: true, index: true }) 17 | memberId: string 18 | 19 | @Prop({ type: Number, required: true, enum: Object.values(TeamRoleEnum) }) 20 | role: TeamRoleEnum 21 | 22 | @Prop() 23 | lastSeenAt?: number 24 | } 25 | 26 | export const TeamMemberSchema = SchemaFactory.createForClass(TeamMemberModel) 27 | 28 | TeamMemberSchema.index({ teamId: 1, memberId: 1 }, { unique: true }) 29 | -------------------------------------------------------------------------------- /packages/server/src/model/user-social-account.model.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' 2 | import { Document } from 'mongoose' 3 | 4 | import { SocialLoginTypeEnum } from '@heyform-inc/shared-types-enums' 5 | 6 | @Schema({ 7 | timestamps: true 8 | }) 9 | export class UserSocialAccountModel extends Document { 10 | @Prop({ 11 | type: String, 12 | required: true 13 | }) 14 | kind: SocialLoginTypeEnum 15 | 16 | @Prop({ required: true }) 17 | userId: string 18 | 19 | @Prop({ required: true }) 20 | openId: string 21 | } 22 | 23 | export const UserSocialAccountSchema = SchemaFactory.createForClass(UserSocialAccountModel) 24 | 25 | // Unique constraint 26 | UserSocialAccountSchema.index({ kind: 1, openId: 1 }, { unique: true }) 27 | UserSocialAccountSchema.index({ kind: 1, userId: 1 }, { unique: true }) 28 | -------------------------------------------------------------------------------- /packages/server/src/queue/form-report.queue.ts: -------------------------------------------------------------------------------- 1 | import { Process, Processor } from '@nestjs/bull' 2 | import { Job } from 'bull' 3 | 4 | import { FormReportService } from '@service' 5 | 6 | import { BaseQueue } from './base.queue' 7 | 8 | interface FormReportQueueJob { 9 | formId: string 10 | } 11 | 12 | @Processor('FormReportQueue') 13 | export class FormReportQueue extends BaseQueue { 14 | constructor(private readonly formReportService: FormReportService) { 15 | super() 16 | } 17 | 18 | @Process() 19 | async generateReport(job: Job<FormReportQueueJob>): Promise<any> { 20 | const { formId } = job.data 21 | await this.formReportService.generate(formId) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/server/src/queue/index.ts: -------------------------------------------------------------------------------- 1 | import { BullModule } from '@nestjs/bull' 2 | 3 | import { BullOptionsFactory } from '@config' 4 | 5 | import { FormReportQueue } from './form-report.queue' 6 | import { MailQueue } from './mail.queue' 7 | import { IntegrationEmailQueue } from './integration-email.queue' 8 | import { IntegrationWebhookQueue } from './integration-webhook.queue' 9 | import { TranslateFormQueue } from './translate-form.queue' 10 | 11 | export const QueueProviders = { 12 | FormReportQueue, 13 | MailQueue, 14 | IntegrationEmailQueue, 15 | IntegrationWebhookQueue, 16 | TranslateFormQueue 17 | } 18 | 19 | export const QueueModules = Object.keys(QueueProviders).map(queueName => { 20 | return BullModule.registerQueueAsync({ 21 | name: queueName, 22 | useFactory: BullOptionsFactory 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /packages/server/src/queue/mail.queue.ts: -------------------------------------------------------------------------------- 1 | import { Process, Processor } from '@nestjs/bull' 2 | import { Job } from 'bull' 3 | 4 | import { SmtpOptionsFactory } from '@config' 5 | import { SmtpMessage, SmtpOptions, smtpSendMail } from '@utils' 6 | 7 | import { BaseQueue } from './base.queue' 8 | 9 | export interface MailQueueJob { 10 | data: SmtpMessage 11 | } 12 | 13 | @Processor('MailQueue') 14 | export class MailQueue extends BaseQueue { 15 | private readonly options!: SmtpOptions 16 | 17 | constructor() { 18 | super() 19 | this.options = SmtpOptionsFactory() 20 | } 21 | 22 | @Process() 23 | async sendMail(job: Job<MailQueueJob>) { 24 | return smtpSendMail(this.options, job.data.data) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/server/src/resolver/app/apps.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Query, Resolver } from '@nestjs/graphql' 2 | 3 | import { AppType } from '@graphql' 4 | import { AppModel } from '@model' 5 | import { AppService } from '@service' 6 | 7 | @Resolver() 8 | export class AppsResolver { 9 | constructor(private readonly appService: AppService) {} 10 | 11 | @Query(returns => [AppType]) 12 | async apps(): Promise<AppModel[]> { 13 | return this.appService.findAll() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/server/src/resolver/form/create-form-field.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql' 2 | 3 | import { FormField } from '@heyform-inc/shared-types-enums' 4 | 5 | import { Auth, FormGuard } from '@decorator' 6 | import { CreateFormFieldInput } from '@graphql' 7 | import { FormService } from '@service' 8 | 9 | @Resolver() 10 | @Auth() 11 | export class CreateFormFieldResolver { 12 | constructor(private readonly formService: FormService) {} 13 | 14 | @Mutation(returns => Boolean) 15 | @FormGuard() 16 | async createFormField(@Args('input') input: CreateFormFieldInput): Promise<boolean> { 17 | return this.formService.createField(input.formId, input.field as FormField) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/server/src/resolver/form/create-form-hidden-field.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql' 2 | import { Auth, FormGuard } from '@decorator' 3 | import { FormService } from '@service' 4 | import { CreateHiddenFieldInput } from '@graphql' 5 | 6 | @Resolver() 7 | @Auth() 8 | export class CreateFormHiddenFieldResolver { 9 | constructor(private readonly formService: FormService) {} 10 | 11 | @Mutation(returns => Boolean) 12 | @FormGuard() 13 | async createFormHiddenField(@Args('input') input: CreateHiddenFieldInput): Promise<boolean> { 14 | await this.formService.update(input.formId, { 15 | $push: { 16 | hiddenFields: { 17 | id: input.fieldId, 18 | name: input.fieldName 19 | } 20 | } 21 | }) 22 | 23 | return true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/server/src/resolver/form/delete-form-field.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql' 2 | 3 | import { Auth, FormGuard } from '@decorator' 4 | import { DeleteFormFieldInput } from '@graphql' 5 | import { FormService } from '@service' 6 | 7 | @Resolver() 8 | @Auth() 9 | export class DeleteFormFieldResolver { 10 | constructor(private readonly formService: FormService) {} 11 | 12 | @Mutation(returns => Boolean) 13 | @FormGuard() 14 | async deleteFormField(@Args('input') input: DeleteFormFieldInput): Promise<boolean> { 15 | return this.formService.deleteField(input.formId, input.fieldId) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/server/src/resolver/form/delete-form-hidden-field.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql' 2 | import { Auth, FormGuard } from '@decorator' 3 | import { FormService } from '@service' 4 | import { DeleteHiddenFieldInput } from '@graphql' 5 | 6 | @Resolver() 7 | @Auth() 8 | export class DeleteFormHiddenFieldResolver { 9 | constructor(private readonly formService: FormService) {} 10 | 11 | @Mutation(returns => Boolean) 12 | @FormGuard() 13 | async deleteFormHiddenField(@Args('input') input: DeleteHiddenFieldInput): Promise<boolean> { 14 | await this.formService.update(input.formId, { 15 | $pull: { 16 | hiddenFields: { 17 | id: input.fieldId 18 | } 19 | } 20 | }) 21 | 22 | return true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/server/src/resolver/form/delete-form.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql' 2 | 3 | import { Auth, FormGuard } from '@decorator' 4 | import { FormDetailInput } from '@graphql' 5 | import { FormService, SubmissionService } from '@service' 6 | 7 | @Resolver() 8 | @Auth() 9 | export class DeleteFormResolver { 10 | constructor( 11 | private readonly formService: FormService, 12 | private readonly submissionService: SubmissionService 13 | ) {} 14 | 15 | /** 16 | * Delete form 17 | * 18 | * @param input 19 | */ 20 | @Mutation(returns => Boolean) 21 | @FormGuard() 22 | async deleteForm(@Args('input') input: FormDetailInput): Promise<boolean> { 23 | await this.formService.delete(input.formId) 24 | await this.submissionService.deleteAll(input.formId) 25 | return true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/server/src/resolver/form/form-integrations.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Query, Resolver } from '@nestjs/graphql' 2 | 3 | import { Auth, FormGuard } from '@decorator' 4 | import { FormDetailInput, FormIntegrationType } from '@graphql' 5 | import { IntegrationModel } from '@model' 6 | import { IntegrationService } from '@service' 7 | 8 | @Resolver() 9 | @Auth() 10 | export class FormIntegrationsResolver { 11 | constructor(private readonly integrationService: IntegrationService) {} 12 | 13 | @Query(returns => [FormIntegrationType]) 14 | @FormGuard() 15 | async formIntegrations(@Args('input') input: FormDetailInput): Promise<IntegrationModel[]> { 16 | return this.integrationService.findAllInForm(input.formId) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/server/src/resolver/form/restore-form.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql' 2 | 3 | import { FormStatusEnum } from '@heyform-inc/shared-types-enums' 4 | 5 | import { Auth, Form, FormGuard } from '@decorator' 6 | import { FormDetailInput } from '@graphql' 7 | import { FormModel } from '@model' 8 | import { FormService } from '@service' 9 | 10 | @Resolver() 11 | @Auth() 12 | export class RestoreFormResolver { 13 | constructor(private readonly formService: FormService) {} 14 | 15 | @Mutation(returns => Boolean) 16 | @FormGuard() 17 | async restoreForm( 18 | @Form() form: FormModel, 19 | @Args('input') input: FormDetailInput 20 | ): Promise<boolean> { 21 | return this.formService.update(input.formId, { 22 | retentionAt: -1, 23 | status: FormStatusEnum.NORMAL 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/server/src/resolver/form/update-form-field.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql' 2 | 3 | import { Auth, FormGuard } from '@decorator' 4 | import { UpdateFormFieldInput } from '@graphql' 5 | import { FormService } from '@service' 6 | 7 | @Resolver() 8 | @Auth() 9 | export class UpdateFormFieldResolver { 10 | constructor(private readonly formService: FormService) {} 11 | 12 | @Mutation(returns => Boolean) 13 | @FormGuard() 14 | async updateFormField(@Args('input') input: UpdateFormFieldInput): Promise<boolean> { 15 | return this.formService.updateField(input) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/server/src/resolver/form/update-form-hidden-field.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql' 2 | import { Auth, FormGuard } from '@decorator' 3 | import { FormService } from '@service' 4 | import { CreateHiddenFieldInput } from '@graphql' 5 | 6 | @Resolver() 7 | @Auth() 8 | export class UpdateFormHiddenFieldResolver { 9 | constructor(private readonly formService: FormService) {} 10 | 11 | @Mutation(returns => Boolean) 12 | @FormGuard() 13 | async updateFormHiddenField(@Args('input') input: CreateHiddenFieldInput): Promise<boolean> { 14 | await this.formService.update( 15 | input.formId, 16 | { 17 | $set: { 18 | 'hiddenFields.$.name': input.fieldName 19 | } 20 | }, 21 | { 22 | 'hiddenFields.id': input.fieldId 23 | } 24 | ) 25 | 26 | return true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/server/src/resolver/form/update-form-logics.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql' 2 | 3 | import { Auth, FormGuard } from '@decorator' 4 | import { UpdateFormLogicsInput } from '@graphql' 5 | import { FormService } from '@service' 6 | 7 | @Resolver() 8 | @Auth() 9 | export class UpdateFormLogicsResolver { 10 | constructor(private readonly formService: FormService) {} 11 | 12 | /** 13 | * Update form logics 14 | */ 15 | @Mutation(returns => Boolean) 16 | @FormGuard() 17 | async updateFormLogics(@Args('input') input: UpdateFormLogicsInput): Promise<boolean> { 18 | return this.formService.update(input.formId, { 19 | logics: input.logics 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/server/src/resolver/form/update-form-schemas.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql' 2 | 3 | import { timestamp } from '@heyform-inc/utils' 4 | 5 | import { Auth, FormGuard } from '@decorator' 6 | import { UpdateFormSchemasInput } from '@graphql' 7 | import { FormService } from '@service' 8 | 9 | @Resolver() 10 | @Auth() 11 | export class UpdateFormSchemasResolver { 12 | constructor(private readonly formService: FormService) {} 13 | 14 | @Mutation(returns => Boolean) 15 | @FormGuard() 16 | async updateFormSchemas(@Args('input') input: UpdateFormSchemasInput): Promise<boolean> { 17 | return this.formService.update(input.formId, { 18 | fields: input.fields, 19 | fieldUpdateAt: timestamp() 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/server/src/resolver/form/update-form-variables.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql' 2 | 3 | import { Auth, FormGuard } from '@decorator' 4 | import { UpdateFormVariablesInput } from '@graphql' 5 | import { FormService } from '@service' 6 | 7 | @Resolver() 8 | @Auth() 9 | export class UpdateFormVariablesResolver { 10 | constructor(private readonly formService: FormService) {} 11 | 12 | /** 13 | * Update form variables 14 | */ 15 | @Mutation(returns => Boolean) 16 | @FormGuard() 17 | async updateFormVariables(@Args('input') input: UpdateFormVariablesInput): Promise<boolean> { 18 | return this.formService.update(input.formId, { 19 | variables: input.variables 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/server/src/resolver/integration/delete-integration-settings.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql' 2 | 3 | import { Auth, FormGuard } from '@decorator' 4 | import { ThirdPartyInput } from '@graphql' 5 | import { IntegrationService } from '@service' 6 | 7 | @Resolver() 8 | @Auth() 9 | export class DeleteIntegrationSettingsResolver { 10 | constructor(private readonly integrationService: IntegrationService) {} 11 | 12 | @Mutation(returns => Boolean) 13 | @FormGuard() 14 | async deleteIntegrationSettings( 15 | @Args('input') 16 | input: ThirdPartyInput 17 | ): Promise<boolean> { 18 | return this.integrationService.delete(input.formId, input.appId) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/server/src/resolver/payment/revoke-stripe-account.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql' 2 | 3 | import { Auth, FormGuard } from '@decorator' 4 | import { FormDetailInput } from '@graphql' 5 | import { FormService } from '@service' 6 | 7 | @Resolver() 8 | @Auth() 9 | export class RevokeStripeAccountResolver { 10 | constructor(private readonly formService: FormService) {} 11 | 12 | @Mutation(returns => Boolean) 13 | @FormGuard() 14 | async revokeStripeAccount(@Args('input') input: FormDetailInput): Promise<boolean> { 15 | return this.formService.update(input.formId, { 16 | stripeAccount: undefined 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/server/src/resolver/project/leave-project.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql' 2 | 3 | import { Auth, ProjectGuard, User } from '@decorator' 4 | import { ProjectDetailInput } from '@graphql' 5 | import { UserModel } from '@model' 6 | import { ProjectService } from '@service' 7 | 8 | @Resolver() 9 | @Auth() 10 | export class LeaveProjectResolver { 11 | constructor(private readonly projectService: ProjectService) {} 12 | 13 | @ProjectGuard() 14 | @Mutation(returns => Boolean) 15 | async leaveProject( 16 | @User() user: UserModel, 17 | @Args('input') input: ProjectDetailInput 18 | ): Promise<boolean> { 19 | return this.projectService.deleteMember(input.projectId, user.id) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/server/src/resolver/project/projects.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Query, Resolver } from '@nestjs/graphql' 2 | 3 | import { Auth, TeamGuard, User } from '@decorator' 4 | import { ProjectType, TeamDetailInput } from '@graphql' 5 | import { ProjectModel, UserModel } from '@model' 6 | import { ProjectService } from '@service' 7 | 8 | @Resolver() 9 | @Auth() 10 | export class ProjectsResolver { 11 | constructor(private readonly projectService: ProjectService) {} 12 | 13 | @TeamGuard() 14 | @Query(returns => [ProjectType]) 15 | async projects( 16 | @User() user: UserModel, 17 | @Args('input') input: TeamDetailInput 18 | ): Promise<ProjectModel[]> { 19 | const projectIds = await this.projectService.findProjectsByMemberId(user.id) 20 | 21 | return this.projectService.findByIds(projectIds, { 22 | teamId: input.teamId 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/server/src/resolver/project/rename-project.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql' 2 | 3 | import { Auth, ProjectGuard, Team } from '@decorator' 4 | import { RenameProjectInput } from '@graphql' 5 | import { TeamModel } from '@model' 6 | import { ProjectService } from '@service' 7 | 8 | @Resolver() 9 | @Auth() 10 | export class RenameProjectResolver { 11 | constructor(private readonly projectService: ProjectService) {} 12 | 13 | @ProjectGuard() 14 | @Mutation(returns => Boolean) 15 | async renameProject( 16 | @Team() team: TeamModel, 17 | @Args('input') input: RenameProjectInput 18 | ): Promise<boolean> { 19 | return this.projectService.update(input.projectId, { 20 | name: input.name 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/server/src/resolver/submission/delete-submission.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql' 2 | 3 | import { Auth, FormGuard } from '@decorator' 4 | import { DeleteSubmissionInput } from '@graphql' 5 | import { SubmissionService } from '@service' 6 | 7 | @Resolver() 8 | @Auth() 9 | export class DeleteSubmissionResolver { 10 | constructor(private readonly submissionService: SubmissionService) {} 11 | 12 | /** 13 | * Delete submissions 14 | * 15 | * @param input 16 | */ 17 | @Mutation(returns => Boolean) 18 | @FormGuard() 19 | async deleteSubmissions(@Args('input') input: DeleteSubmissionInput): Promise<boolean> { 20 | return this.submissionService.deleteByIds(input.formId, input.submissionIds) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/server/src/resolver/template/template-detail.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Query, Resolver } from '@nestjs/graphql' 2 | 3 | import { helper } from '@heyform-inc/utils' 4 | 5 | import { TemplateDetailInput, TemplateDetailType } from '@graphql' 6 | import { TemplateModel } from '@model' 7 | import { TemplateService } from '@service' 8 | 9 | @Resolver() 10 | export class TemplateDetailResolver { 11 | constructor(private readonly templateService: TemplateService) {} 12 | 13 | @Query(returns => TemplateDetailType) 14 | async templateDetail(@Args('input') input: TemplateDetailInput): Promise<TemplateModel> { 15 | if (helper.isValid(input.templateSlug)) { 16 | return this.templateService.findBySlug(input.templateSlug) 17 | } else { 18 | return this.templateService.findById(input.templateId) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/server/src/resolver/template/templates.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Query, Resolver } from '@nestjs/graphql' 2 | 3 | import { TemplateType, TemplatesInput } from '@graphql' 4 | import { TemplateModel } from '@model' 5 | import { TemplateService } from '@service' 6 | 7 | @Resolver() 8 | export class TemplatesResolver { 9 | constructor(private readonly templateService: TemplateService) {} 10 | 11 | @Query(returns => [TemplateType]) 12 | async templates(@Args('input') input: TemplatesInput): Promise<TemplateModel[]> { 13 | return this.templateService.findAll(input.keyword, input.limit) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/server/src/resolver/unsplash/unsplash-track-download.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql' 2 | 3 | import { helper } from '@heyform-inc/utils' 4 | 5 | import { Auth } from '@decorator' 6 | import { UNSPLASH_CLIENT_ID } from '@environments' 7 | import { UnsplashTrackDownloadInput } from '@graphql' 8 | import { Unsplash } from '@utils' 9 | 10 | @Resolver() 11 | @Auth() 12 | export class UnsplashTrackDownloadResolver { 13 | @Mutation(returns => Boolean) 14 | async unsplashTrackDownload(@Args('input') input: UnsplashTrackDownloadInput): Promise<boolean> { 15 | const unsplash = Unsplash.init({ 16 | clientId: UNSPLASH_CLIENT_ID 17 | }) 18 | return helper.isValid(await unsplash.trackDownload(input.downloadUrl)) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/server/src/resolver/user/cancel-user-deletion.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Mutation, Resolver } from '@nestjs/graphql' 2 | 3 | import { Auth, User } from '@decorator' 4 | import { UserModel } from '@model' 5 | import { RedisService, UserService } from '@service' 6 | 7 | @Resolver() 8 | @Auth() 9 | export class CancelUserDeletionResolver { 10 | constructor( 11 | private readonly userService: UserService, 12 | private readonly redisService: RedisService 13 | ) {} 14 | 15 | @Mutation(returns => Boolean) 16 | async cancelUserDeletion(@User() user: UserModel): Promise<boolean> { 17 | const key = `UserDeletion:${user.id}` 18 | 19 | const result = await this.userService.update(user.id, { 20 | isDeletionScheduled: false, 21 | deletionScheduledAt: 0 22 | }) 23 | 24 | await this.redisService.del(key) 25 | 26 | return result 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/server/src/resolver/user/user-deletion-code.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Query, Resolver } from '@nestjs/graphql' 2 | 3 | import { Auth, User } from '@decorator' 4 | import { UserModel } from '@model' 5 | import { AuthService, MailService } from '@service' 6 | 7 | @Resolver() 8 | @Auth() 9 | export class UserDeletionCodeResolver { 10 | constructor( 11 | private readonly mailService: MailService, 12 | private readonly authService: AuthService 13 | ) {} 14 | 15 | @Query(returns => Boolean) 16 | async userDeletionCode(@User() user: UserModel): Promise<boolean> { 17 | const key = `user_deletion:${user.id}` 18 | const code = await this.authService.getVerificationCode(key) 19 | 20 | await this.mailService.accountDeletionRequest(user.email, code) 21 | 22 | return true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/server/src/schedule/delete-form-in-trash.schedule.ts: -------------------------------------------------------------------------------- 1 | import { Process, Processor } from '@nestjs/bull' 2 | 3 | import { FormService } from '@service' 4 | 5 | import { BaseQueue } from '../queue/base.queue' 6 | 7 | @Processor('DeleteFormInTrashSchedule') 8 | export class DeleteFormInTrashSchedule extends BaseQueue { 9 | constructor(private readonly formService: FormService) { 10 | super() 11 | } 12 | 13 | @Process() 14 | async deleteFormInTrash(): Promise<any> { 15 | const forms = await this.formService.findAllInTrash() 16 | 17 | if (forms.length > 0) { 18 | await this.formService.delete(forms.map(row => row.id)) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/server/src/schedule/index.ts: -------------------------------------------------------------------------------- 1 | import { BullModule } from '@nestjs/bull' 2 | 3 | import { BullOptionsFactory } from '@config' 4 | 5 | import { DeleteFormInTrashSchedule } from './delete-form-in-trash.schedule' 6 | import { DeleteUserAccountSchedule } from './delete-user-account.schedule' 7 | import { ResetInviteCodeSchedule } from './reset-invite-code.schedule' 8 | 9 | export const ScheduleProviders = { 10 | DeleteFormInTrashSchedule, 11 | ResetInviteCodeSchedule, 12 | DeleteUserAccountSchedule 13 | } 14 | 15 | export const ScheduleModules = Object.keys(ScheduleProviders).map(scheduleName => { 16 | return BullModule.registerQueueAsync({ 17 | name: scheduleName, 18 | useFactory: BullOptionsFactory 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /packages/server/src/service/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.service' 2 | export * from './endpoint.service' 3 | export * from './form.service' 4 | export * from './form-analytic.service' 5 | export * from './form-report.service' 6 | export * from './mail.service' 7 | export * from './payment.service' 8 | export * from './redis.service' 9 | export * from './schedule.service' 10 | export * from './social-login.service' 11 | export * from './submission.service' 12 | export * from './submission-ip-limit.service' 13 | export * from './team.service' 14 | export * from './user.service' 15 | export * from './integration.service' 16 | export * from './export-file.service' 17 | export * from './app.service' 18 | export * from './template.service' 19 | export * from './project.service' 20 | -------------------------------------------------------------------------------- /packages/server/src/utils/anti-bot/akismet.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { AkismetClient } from 'akismet-api' 3 | 4 | interface VerifySpamOptions { 5 | key: string 6 | url: string 7 | ip?: string 8 | userAgent?: string 9 | } 10 | 11 | export async function verifySpam(content: string, options: VerifySpamOptions): Promise<boolean> { 12 | const client = new AkismetClient({ 13 | key: options.key, 14 | blog: options.url 15 | }) 16 | 17 | return await client.checkSpam({ 18 | type: 'contact-form', 19 | ip: options.ip, 20 | useragent: options.userAgent, 21 | content 22 | }) 23 | } 24 | 25 | export default { 26 | verifySpam 27 | } 28 | -------------------------------------------------------------------------------- /packages/server/src/utils/anti-bot/index.ts: -------------------------------------------------------------------------------- 1 | export { default as akismet } from './akismet' 2 | export { default as recaptcha } from './recaptcha' 3 | -------------------------------------------------------------------------------- /packages/server/src/utils/anti-bot/recaptcha.ts: -------------------------------------------------------------------------------- 1 | import { request } from '../social-login/utils' 2 | 3 | interface RecaptchaOptions { 4 | secret: string 5 | } 6 | 7 | export async function verifyRecaptcha(token: string, options: RecaptchaOptions): Promise<any> { 8 | const result = await request({ 9 | method: 'POST', 10 | url: 'https://www.google.com/recaptcha/api/siteverify', 11 | data: { 12 | secret: options.secret, 13 | response: token 14 | } 15 | }) 16 | return result.data?.success 17 | } 18 | 19 | export default { 20 | verifyRecaptcha 21 | } 22 | -------------------------------------------------------------------------------- /packages/server/src/utils/decorators/device-id.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, createParamDecorator } from '@nestjs/common' 2 | import { GqlExecutionContext } from '@nestjs/graphql' 3 | import 'reflect-metadata' 4 | 5 | export const GqlBrowserId = createParamDecorator((_: any, context: ExecutionContext): string => { 6 | const ctx = GqlExecutionContext.create(context) 7 | const { req } = ctx.getContext() 8 | return req.get('x-browser-Id') 9 | }) 10 | 11 | export const HttpBrowserId = createParamDecorator((_: any, ctx: ExecutionContext): string => { 12 | const req = ctx.switchToHttp().getRequest() 13 | return req.get('x-browser-Id') 14 | }) 15 | -------------------------------------------------------------------------------- /packages/server/src/utils/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client' 2 | export * from './device-id' 3 | export * from './ip' 4 | export * from './lang' 5 | export * from './user-agent' 6 | export * from './raw-body' 7 | -------------------------------------------------------------------------------- /packages/server/src/utils/decorators/raw-body.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, createParamDecorator } from '@nestjs/common' 2 | import * as rawBody from 'raw-body' 3 | 4 | import { helper } from '@heyform-inc/utils' 5 | 6 | export const HttpRawBody = createParamDecorator( 7 | async (_: any, ctx: ExecutionContext): Promise<string> => { 8 | const req = ctx.switchToHttp().getRequest() 9 | let payload: string 10 | 11 | if (req.readable) { 12 | const raw = await rawBody(req) 13 | payload = raw.toString('utf8').trim() 14 | } else if (helper.isPlainObject(req.body) || helper.isArray(req.body)) { 15 | payload = JSON.stringify(req.body, null, 2) 16 | } else { 17 | payload = req.body.toString() 18 | } 19 | 20 | return payload 21 | } 22 | ) 23 | -------------------------------------------------------------------------------- /packages/server/src/utils/decorators/user-agent.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, createParamDecorator } from '@nestjs/common' 2 | import { GqlExecutionContext } from '@nestjs/graphql' 3 | import 'reflect-metadata' 4 | 5 | import { UserAgent as UserAgentInterface, parseUserAgent } from '../user-agent' 6 | 7 | export const GqlUserAgent = createParamDecorator( 8 | (_: any, context: ExecutionContext): UserAgentInterface => { 9 | const ctx = GqlExecutionContext.create(context) 10 | const { req } = ctx.getContext() 11 | return parseUserAgent(req.get('user-agent')) 12 | } 13 | ) 14 | 15 | export const HttpUserAgent = createParamDecorator( 16 | (_: any, ctx: ExecutionContext): UserAgentInterface => { 17 | const req = ctx.switchToHttp().getRequest() 18 | return parseUserAgent(req.get('user-agent')) 19 | } 20 | ) 21 | -------------------------------------------------------------------------------- /packages/server/src/utils/directives/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lower' 2 | -------------------------------------------------------------------------------- /packages/server/src/utils/directives/lower.ts: -------------------------------------------------------------------------------- 1 | import { SchemaDirectiveVisitor } from 'apollo-server' 2 | import { GraphQLField, defaultFieldResolver } from 'graphql' 3 | 4 | export class LowerCaseDirective extends SchemaDirectiveVisitor { 5 | visitFieldDefinition(field: GraphQLField<any, any>) { 6 | const { resolve = defaultFieldResolver } = field 7 | field.resolve = async function (...args) { 8 | const result = await resolve.apply(this, args) 9 | if (typeof result === 'string') { 10 | return result.toLowerCase() 11 | } 12 | return result 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/server/src/utils/disposable-email.ts: -------------------------------------------------------------------------------- 1 | import * as blocklist from 'disposable-email-blocklist' 2 | 3 | export function isDisposableEmail(email: string): boolean { 4 | const [_, domain] = email.toLowerCase().split('@') 5 | return ((blocklist as unknown) as string[]).includes(domain) 6 | } 7 | -------------------------------------------------------------------------------- /packages/server/src/utils/gravatar/index.ts: -------------------------------------------------------------------------------- 1 | import { md5 } from '../crypto' 2 | 3 | export function gravatar(email: string, size = 120): string { 4 | return `https://secure.gravatar.com/avatar/${md5(email)}?s=${size}&d=mm&r=g` 5 | } 6 | -------------------------------------------------------------------------------- /packages/server/src/utils/handlebars.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import * as hbs from 'hbs' 3 | 4 | const h = hbs as any 5 | 6 | h.registerHelper('json', v1 => JSON.stringify(v1)) 7 | h.registerHelper('eq', (v1, v2) => v1 === v2) 8 | h.registerHelper('ne', (v1, v2) => v1 !== v2) 9 | 10 | export default h 11 | -------------------------------------------------------------------------------- /packages/server/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logger' 2 | export * from './mongo' 3 | export * from './anti-bot' 4 | export * from './crypto' 5 | export * from './decorators' 6 | export * from './directives' 7 | export * from './gravatar' 8 | export * from './lower-case-scalar' 9 | export * from './social-login' 10 | export * from './unsplash' 11 | export * from './smtp' 12 | export * from './user-agent' 13 | export * from './map-to-object' 14 | export * from './read-dir-sync' 15 | export * from './promise-timeout' 16 | export { default as hbs } from './handlebars' 17 | export * from './request-parser' 18 | export * from './random-number' 19 | export * from './disposable-email' 20 | -------------------------------------------------------------------------------- /packages/server/src/utils/logger/index.ts: -------------------------------------------------------------------------------- 1 | import { Logger as L } from '@nestjs/common' 2 | 3 | export class Logger extends L { 4 | static info(message: any, context?: string): void { 5 | L.log(message, context) 6 | } 7 | 8 | static trace(message: any, trace?: string, context?: string): void { 9 | L.error(message, trace, context) 10 | } 11 | 12 | static fatal(message: any, trace?: string, context?: string): void { 13 | L.error(message, trace, context) 14 | } 15 | 16 | info(message: any, context?: string): void { 17 | this.log(message, context) 18 | } 19 | 20 | trace(message: any, trace?: string, context?: string): void { 21 | this.error(message, trace, context) 22 | } 23 | 24 | fatal(message: any, trace?: string, context?: string): void { 25 | this.error(message, trace, context) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/server/src/utils/lower-case-scalar.ts: -------------------------------------------------------------------------------- 1 | import { CustomScalar, Scalar } from '@nestjs/graphql' 2 | import { Kind, ValueNode } from 'graphql' 3 | 4 | export class LowerCase extends String {} 5 | 6 | @Scalar('LowerCase', () => LowerCase) 7 | export class LowerCaseScalar implements CustomScalar<string, LowerCase> { 8 | description = 'Lower string custom scalar type' 9 | 10 | parseValue(value: string): LowerCase { 11 | return value.toLowerCase() 12 | } 13 | 14 | serialize(value: LowerCase): string { 15 | return value.toString() 16 | } 17 | 18 | parseLiteral(ast: ValueNode): LowerCase | null { 19 | if (ast.kind === Kind.STRING) { 20 | return ast.value.toLowerCase() 21 | } 22 | return null 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/server/src/utils/map-to-object.ts: -------------------------------------------------------------------------------- 1 | import { helper } from '@heyform-inc/utils' 2 | 3 | export function mapToObject<T = any>(mapLike: any): T { 4 | if (helper.isEmpty(mapLike)) { 5 | return {} as T 6 | } 7 | 8 | // @ts-ignore 9 | return helper.isMap(mapLike) ? Object.fromEntries(mapLike) : mapLike 10 | } 11 | -------------------------------------------------------------------------------- /packages/server/src/utils/mongo.ts: -------------------------------------------------------------------------------- 1 | import { helper } from '@heyform-inc/utils' 2 | 3 | export function getUpdateQuery( 4 | obj: Record<string, any>, 5 | prefix: string, 6 | deep = true 7 | ): Record<string, any> { 8 | let query: Record<string, any> = {} 9 | 10 | Object.keys(obj).forEach(key => { 11 | const value = obj[key] 12 | 13 | if (deep && helper.isPlainObject(value)) { 14 | query = { 15 | ...query, 16 | ...getUpdateQuery(value, `${prefix}.${key}`) 17 | } 18 | } else if (!helper.isNil(value)) { 19 | query[`${prefix}.${key}`] = value 20 | } 21 | }) 22 | 23 | return query 24 | } 25 | -------------------------------------------------------------------------------- /packages/server/src/utils/random-number.ts: -------------------------------------------------------------------------------- 1 | export function randomNumber(min: number, max: number): number { 2 | return Math.floor(Math.random() * (max - min + 1)) + min 3 | } 4 | -------------------------------------------------------------------------------- /packages/server/src/utils/read-dir-sync.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync } from 'fs' 2 | import { resolve } from 'path' 3 | 4 | export function readDirSync(dir: string, ext: string) { 5 | return readdirSync(dir) 6 | .filter(filePath => filePath.endsWith(ext)) 7 | .map(filePath => resolve(dir, filePath)) 8 | } 9 | -------------------------------------------------------------------------------- /packages/server/src/utils/request-parser.ts: -------------------------------------------------------------------------------- 1 | import { helper } from '@heyform-inc/utils' 2 | 3 | export function requestParser(req: any, keys: string[]): any { 4 | const sources = ['body', 'query', 'params'] 5 | let value: any 6 | 7 | for (const source of sources) { 8 | for (const key of keys) { 9 | const searchValue = req[source][key] 10 | 11 | if (helper.isValid(searchValue)) { 12 | value = searchValue 13 | break 14 | } 15 | } 16 | } 17 | 18 | return value 19 | } 20 | -------------------------------------------------------------------------------- /packages/server/src/utils/social-login/utils.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import { qs } from '@heyform-inc/utils' 4 | 5 | export const request = axios.create({ 6 | withCredentials: true, 7 | timeout: 6e4 8 | }).request 9 | 10 | export function generateUrl(prefixUri: string, query: Record<string, any>): string { 11 | const queryString = qs.stringify(query) 12 | return `${prefixUri}?${queryString}` 13 | } 14 | 15 | export const defaultLocales = ['en', 'zh-cn'] 16 | 17 | export function formatLocale(lang?: string, whiteList?: string[]): string { 18 | const customWhiteList = whiteList || defaultLocales 19 | const defaultLocale = customWhiteList[0] 20 | 21 | if (!lang) { 22 | return defaultLocale 23 | } 24 | 25 | const lower = lang.toLowerCase() 26 | return customWhiteList.includes(lower) ? lower : defaultLocale 27 | } 28 | -------------------------------------------------------------------------------- /packages/server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "view", 5 | "node_modules", 6 | "mongoose", 7 | "dist" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/server/view/connect-stripe.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <meta charset="utf-8"/> 5 | <meta content="width=device-width, initial-scale=1" name="viewport" /> 6 | <meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible"> 7 | <meta name="robots" content="noindex"> 8 | <title>Connect to Stripe - HeyForm</title> 9 | </head> 10 | <body> 11 | <script> 12 | var origin = window.location.origin; 13 | 14 | if (window.opener && window.opener.origin === origin) { 15 | var data = { 16 | source: 'heyform-connect-stripe', 17 | payload: {{{json rendererData}}} 18 | }; 19 | window.opener.postMessage(data, origin); 20 | } 21 | </script> 22 | </body> 23 | </html> 24 | -------------------------------------------------------------------------------- /packages/server/view/social-login.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <meta charset="utf-8"/> 5 | <meta name="robots" content="noindex"> 6 | <meta http-equiv="refresh" content="0; url='{{redirectUri}}'"> 7 | </head> 8 | <body> 9 | Redirecting to <a href="{{redirectUri}}">HeyForm</a> 10 | </body> 11 | </html> 12 | -------------------------------------------------------------------------------- /packages/shared-types-enums/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "semi": false, 6 | "tabWidth": 2, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "avoid" 11 | } 12 | -------------------------------------------------------------------------------- /packages/shared-types-enums/src/audience.ts: -------------------------------------------------------------------------------- 1 | export interface GroupModel { 2 | id: string 3 | teamId?: string 4 | name: string 5 | contactCount?: number 6 | } 7 | 8 | export interface ContactModel { 9 | id: string 10 | teamId: string 11 | fullName: string 12 | email: string 13 | jobTitle?: string 14 | avatar?: string 15 | phoneNumber?: string 16 | note?: string 17 | groups?: GroupModel[] 18 | } 19 | -------------------------------------------------------------------------------- /packages/shared-types-enums/src/enums/keyboard.ts: -------------------------------------------------------------------------------- 1 | export enum KeyCodeEnum { 2 | BACKSPACE = 8, 3 | TAB = 9, 4 | ENTER = 13, 5 | ESC = 27, 6 | SPACE = 32, 7 | LEFT = 37, 8 | UP = 38, 9 | RIGHT = 39, 10 | DOWN = 40, 11 | DELETE = 46, 12 | VOID = 229 13 | } 14 | -------------------------------------------------------------------------------- /packages/shared-types-enums/src/enums/social-login.ts: -------------------------------------------------------------------------------- 1 | export enum SocialLoginTypeEnum { 2 | APPLE = 'apple', 3 | GOOGLE = 'google', 4 | WECHAT = 'wechat', 5 | GOOGLE_ONE_TAP = 'google-one-tap' 6 | } 7 | -------------------------------------------------------------------------------- /packages/shared-types-enums/src/enums/submission.ts: -------------------------------------------------------------------------------- 1 | export enum SubmissionCategoryEnum { 2 | INBOX = 'inbox', 3 | SPAM = 'spam', 4 | STARRED = 'starred', 5 | ARCHIVE = 'archive' 6 | } 7 | 8 | export enum SubmissionStatusEnum { 9 | PUBLIC = 1, 10 | PRIVATE = 2, 11 | DELETED = 3 12 | } 13 | -------------------------------------------------------------------------------- /packages/shared-types-enums/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './enums/form' 2 | export * from './enums/keyboard' 3 | export * from './enums/submission' 4 | export * from './enums/social-login' 5 | export * from './constants/form' 6 | export * from './audience' 7 | export * from './form' 8 | export * from './submission' 9 | export * from './unsplash' 10 | -------------------------------------------------------------------------------- /packages/shared-types-enums/src/unsplash.ts: -------------------------------------------------------------------------------- 1 | export interface UnsplashImage { 2 | id: string 3 | url: string 4 | thumbUrl: string 5 | downloadUrl: string 6 | author: string 7 | authorUrl: string 8 | } 9 | -------------------------------------------------------------------------------- /packages/shared-types-enums/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "forceConsistentCasingInFileNames": true, 5 | "esModuleInterop": true, 6 | "noUnusedLocals": true, 7 | "noUnusedParameters": true, 8 | "strict": true, 9 | "removeComments": true, 10 | "allowSyntheticDefaultImports": true, 11 | "listEmittedFiles": true, 12 | "listFiles": true, 13 | "allowJs": false, 14 | "declaration": true, 15 | "sourceMap": true, 16 | "importHelpers": true, 17 | "resolveJsonModule": true, 18 | "moduleResolution": "node", 19 | "skipLibCheck": true 20 | }, 21 | "exclude": [ 22 | "coverage", 23 | "lib", 24 | "node_modules" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /packages/shared-types-enums/tsup.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tsup').Options} */ 2 | module.exports = { 3 | target: 'esnext', 4 | dts: true, 5 | sourcemap: true, 6 | entry: ['src/index.ts'], 7 | format: ['cjs', 'esm'], 8 | splitting: false, 9 | treeshake: true, 10 | clean: true 11 | } 12 | -------------------------------------------------------------------------------- /packages/utils/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "semi": false, 6 | "tabWidth": 2, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "avoid" 11 | } 12 | -------------------------------------------------------------------------------- /packages/utils/src/clone.ts: -------------------------------------------------------------------------------- 1 | import _clone from 'clone' 2 | 3 | export function clone<T>(obj: T): T { 4 | return _clone(obj) 5 | } 6 | -------------------------------------------------------------------------------- /packages/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'deep-object-diff' 2 | export { default as qs } from 'qs' 3 | export * from './bytes' 4 | export * from './clone' 5 | export * from './color' 6 | export * from './conv' 7 | export * from './date' 8 | export { default as helper } from './helper' 9 | export * from './second' 10 | export * from './mime' 11 | export * from './nanoid' 12 | export * from './object' 13 | export * from './random' 14 | export * from './slugify' 15 | export * from './type' 16 | export * from './uuid' 17 | -------------------------------------------------------------------------------- /packages/utils/src/nanoid.ts: -------------------------------------------------------------------------------- 1 | import { customAlphabet } from 'nanoid' 2 | 3 | const NANOID_ALPHABET = 4 | 'ModuleSymbhasOwnPr0123456789ABCDEFGHNRVfgctiUvzKqYTJkLxpZXIjQW' 5 | 6 | export function nanoid(len = 21): string { 7 | return nanoidCustomAlphabet(NANOID_ALPHABET, len) 8 | } 9 | 10 | export function nanoidCustomAlphabet(alphabet: string, len = 21): string { 11 | const generate = customAlphabet(alphabet, len) 12 | return generate() 13 | } 14 | -------------------------------------------------------------------------------- /packages/utils/src/second.ts: -------------------------------------------------------------------------------- 1 | import _ms from 'ms' 2 | 3 | export function hs(arg: string): number | undefined { 4 | const value = ms(arg) 5 | 6 | if (value) { 7 | return Math.round(value / 1_000) 8 | } 9 | } 10 | 11 | export function ms(arg: string): number | undefined { 12 | return _ms(arg) 13 | } 14 | 15 | export const toSecond = hs 16 | export const toMillisecond = ms 17 | -------------------------------------------------------------------------------- /packages/utils/src/slugify.ts: -------------------------------------------------------------------------------- 1 | import slug from 'slugify' 2 | 3 | export interface SlugifyOptions { 4 | replacement?: string 5 | remove?: RegExp 6 | lower?: boolean 7 | strict?: boolean 8 | locale?: string 9 | trim?: boolean 10 | } 11 | 12 | export function slugify(text: string, options?: SlugifyOptions): string { 13 | return slug(text, { 14 | replacement: '_', 15 | lower: true, 16 | ...options 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /packages/utils/src/type.ts: -------------------------------------------------------------------------------- 1 | const toString = Object.prototype.toString 2 | 3 | export function type(obj: any): string { 4 | if (obj === null) { 5 | return 'null' 6 | } 7 | 8 | let type: string = typeof obj 9 | 10 | if (type !== 'object') { 11 | return type 12 | } 13 | 14 | type = toString.call(obj).slice(8, -1) 15 | 16 | const typeLower = type.toLowerCase() 17 | 18 | if (typeLower !== 'object') { 19 | if ( 20 | typeLower === 'number' || 21 | typeLower === 'boolean' || 22 | typeLower === 'string' 23 | ) { 24 | return type 25 | } 26 | return typeLower 27 | } 28 | 29 | return typeLower 30 | } 31 | -------------------------------------------------------------------------------- /packages/utils/src/uuid.ts: -------------------------------------------------------------------------------- 1 | export { v4 as uuidv4, v5 as uuidv5 } from 'uuid' 2 | -------------------------------------------------------------------------------- /packages/utils/test/clone.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | import { clone } from '../src' 3 | 4 | const obj = { 5 | a: { 6 | w: 'hello' 7 | }, 8 | b: [ 9 | { 10 | x: 2, 11 | y: true 12 | } 13 | ] 14 | } 15 | 16 | test('clone nested object', () => { 17 | expect(clone(obj)).toStrictEqual(obj) 18 | }) 19 | 20 | test('clone nested object', () => { 21 | const copyObj = clone(obj) 22 | copyObj.a.w = 'world' 23 | 24 | expect(obj.a.w).toBe('hello') 25 | expect(copyObj.a.w).toBe('world') 26 | expect(copyObj.b).toStrictEqual([ 27 | { 28 | x: 2, 29 | y: true 30 | } 31 | ]) 32 | }) 33 | -------------------------------------------------------------------------------- /packages/utils/test/nanoid.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | import { nanoid, nanoidCustomAlphabet } from '../src' 3 | 4 | test('nanoid length', () => { 5 | expect(nanoid().length).toBe(21) 6 | }) 7 | 8 | test('nanoid custom alphabet', () => { 9 | expect(nanoidCustomAlphabet('a', 6)).toBe('aaaaaa') 10 | expect(nanoidCustomAlphabet('b')).toBe('bbbbbbbbbbbbbbbbbbbbb') 11 | }) 12 | -------------------------------------------------------------------------------- /packages/utils/test/qs.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | import { qs } from '../src' 3 | 4 | const obj = { 5 | a: [1, 2, 3], 6 | b: 'hello', 7 | c: true, 8 | d: undefined 9 | } 10 | 11 | const str = 'a%5B0%5D=1&a%5B1%5D=2&a%5B2%5D=3&b=hello&c=true' 12 | 13 | test('stringify object', () => { 14 | expect(qs.stringify(obj)).toBe(str) 15 | }) 16 | 17 | test('parse string', () => { 18 | expect(qs.parse(str)).toStrictEqual({ 19 | a: ['1', '2', '3'], 20 | b: 'hello', 21 | c: 'true' 22 | }) 23 | }) 24 | 25 | test('parse array', () => { 26 | expect(qs.parse([] as any)).toStrictEqual({}) 27 | }) 28 | 29 | test('parse empty string', () => { 30 | expect(qs.parse('')).toStrictEqual({}) 31 | }) 32 | -------------------------------------------------------------------------------- /packages/utils/test/slugify.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | import { slugify } from '../src' 3 | 4 | test('slugify', () => { 5 | expect(slugify('/user/sign/in')).toBe('usersignin') 6 | expect(slugify('Hello World')).toBe('hello_world') 7 | expect(slugify('Hello World', { replacement: '-' })).toBe('hello-world') 8 | }) 9 | -------------------------------------------------------------------------------- /packages/utils/test/uuid.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | import { uuidv4, helper } from '../src' 3 | 4 | test('uuidv4', () => { 5 | expect(helper.isUUID(uuidv4())).toBe(true) 6 | }) 7 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "forceConsistentCasingInFileNames": true, 5 | "esModuleInterop": true, 6 | "noUnusedLocals": true, 7 | "noUnusedParameters": true, 8 | "strict": true, 9 | "removeComments": true, 10 | "allowSyntheticDefaultImports": true, 11 | "listEmittedFiles": true, 12 | "listFiles": true, 13 | "allowJs": false, 14 | "declaration": true, 15 | "sourceMap": true, 16 | "importHelpers": true, 17 | "resolveJsonModule": true, 18 | "moduleResolution": "node", 19 | "skipLibCheck": true 20 | }, 21 | "exclude": [ 22 | "coverage", 23 | "lib", 24 | "node_modules" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /packages/utils/tsup.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tsup').Options} */ 2 | module.exports = { 3 | target: 'esnext', 4 | dts: true, 5 | sourcemap: true, 6 | entry: ['src/index.ts'], 7 | format: ['cjs', 'esm'], 8 | splitting: false, 9 | treeshake: true, 10 | clean: true 11 | } 12 | -------------------------------------------------------------------------------- /packages/webapp/.env.example: -------------------------------------------------------------------------------- 1 | VITE_HOMEPAGE_URL=http://127.0.0.1:3000 2 | VITE_COOKIE_DOMAIN=127.0.0.1 3 | 4 | VITE_STRIPE_PUBLISHABLE_KEY= 5 | VITE_GEETEST_CAPTCHA_ID= 6 | VITE_GOOGLE_RECAPTCHA_KEY= 7 | 8 | VITE_DISABLE_LOGIN_WITH_GOOGLE=false 9 | VITE_DISABLE_LOGIN_WITH_APPLE=false 10 | 11 | VITE_VERIFY_USER_EMAIL=true 12 | VITE_APP_DISABLE_REGISTRATION=false 13 | -------------------------------------------------------------------------------- /packages/webapp/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "semi": false, 6 | "tabWidth": 2, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "avoid", 11 | "plugins": ["@trivago/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"], 12 | "importOrder": [ 13 | "^@/", 14 | "^[../]", 15 | "^[./]" 16 | ], 17 | "importOrderSeparation": true, 18 | "importOrderSortSpecifiers": true, 19 | "importOrderGroupNamespaceSpecifiers": false 20 | } 21 | -------------------------------------------------------------------------------- /packages/webapp/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: require("./tailwind.config.js"), 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/webapp/public/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heyform/heyform/c8856912b787038c3b0f21c56640f081fe636e32/packages/webapp/public/static/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/webapp/public/static/email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heyform/heyform/c8856912b787038c3b0f21c56640f081fe636e32/packages/webapp/public/static/email.png -------------------------------------------------------------------------------- /packages/webapp/public/static/facebookpixel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heyform/heyform/c8856912b787038c3b0f21c56640f081fe636e32/packages/webapp/public/static/facebookpixel.png -------------------------------------------------------------------------------- /packages/webapp/public/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heyform/heyform/c8856912b787038c3b0f21c56640f081fe636e32/packages/webapp/public/static/favicon.ico -------------------------------------------------------------------------------- /packages/webapp/public/static/googleanalytics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heyform/heyform/c8856912b787038c3b0f21c56640f081fe636e32/packages/webapp/public/static/googleanalytics.png -------------------------------------------------------------------------------- /packages/webapp/public/static/icon_120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heyform/heyform/c8856912b787038c3b0f21c56640f081fe636e32/packages/webapp/public/static/icon_120.png -------------------------------------------------------------------------------- /packages/webapp/public/static/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heyform/heyform/c8856912b787038c3b0f21c56640f081fe636e32/packages/webapp/public/static/icon_128.png -------------------------------------------------------------------------------- /packages/webapp/public/static/icon_180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heyform/heyform/c8856912b787038c3b0f21c56640f081fe636e32/packages/webapp/public/static/icon_180.png -------------------------------------------------------------------------------- /packages/webapp/public/static/icon_192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heyform/heyform/c8856912b787038c3b0f21c56640f081fe636e32/packages/webapp/public/static/icon_192.png -------------------------------------------------------------------------------- /packages/webapp/public/static/icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heyform/heyform/c8856912b787038c3b0f21c56640f081fe636e32/packages/webapp/public/static/icon_512.png -------------------------------------------------------------------------------- /packages/webapp/public/static/icon_maskable_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heyform/heyform/c8856912b787038c3b0f21c56640f081fe636e32/packages/webapp/public/static/icon_maskable_128.png -------------------------------------------------------------------------------- /packages/webapp/public/static/icon_maskable_192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heyform/heyform/c8856912b787038c3b0f21c56640f081fe636e32/packages/webapp/public/static/icon_maskable_192.png -------------------------------------------------------------------------------- /packages/webapp/public/static/icon_maskable_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heyform/heyform/c8856912b787038c3b0f21c56640f081fe636e32/packages/webapp/public/static/icon_maskable_512.png -------------------------------------------------------------------------------- /packages/webapp/public/static/webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heyform/heyform/c8856912b787038c3b0f21c56640f081fe636e32/packages/webapp/public/static/webhook.png -------------------------------------------------------------------------------- /packages/webapp/src/components/RedirectUriLink.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | import { Link } from 'react-router-dom' 3 | 4 | import { useQueryURL } from '@/utils' 5 | 6 | interface RedirectUriLinkProps extends IComponentProps { 7 | href: string 8 | } 9 | 10 | export const RedirectUriLink: FC<RedirectUriLinkProps> = ({ href, children, ...restProps }) => { 11 | const to = useQueryURL(href) 12 | 13 | return ( 14 | <Link to={to} {...restProps}> 15 | {children} 16 | </Link> 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/webapp/src/components/SubHeading.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react' 2 | 3 | interface SubHeadingProps extends IComponentProps { 4 | id?: string 5 | description?: ReactNode 6 | action?: ReactNode 7 | } 8 | 9 | export const SubHeading: FC<SubHeadingProps> = ({ 10 | children, 11 | description, 12 | action, 13 | ...restProps 14 | }) => { 15 | return ( 16 | <div className="mb-5 mt-11 flex items-center justify-between" {...restProps}> 17 | <div> 18 | <div className="subheading-title mb-3 text-base font-medium leading-relaxed text-gray-700"> 19 | {children} 20 | </div> 21 | {description && ( 22 | <div className="subheading-description mt-1 text-gray-500">{description}</div> 23 | )} 24 | </div> 25 | {action && action} 26 | </div> 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/BoldIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const BoldIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => ( 4 | <svg 5 | width="24" 6 | height="24" 7 | viewBox="0 0 24 24" 8 | fill="none" 9 | xmlns="http://www.w3.org/2000/svg" 10 | {...props} 11 | > 12 | <path 13 | d="M6 12V3H11.85C14.3275 3 16.336 5.01472 16.336 7.5C16.336 9.98526 14.3275 12 11.85 12H6ZM6 12H13.664C16.1416 12 18.15 14.0147 18.15 16.5C18.15 18.9853 16.1416 21 13.664 21H6V12Z" 14 | stroke="currentColor" 15 | strokeWidth="2" 16 | strokeLinecap="round" 17 | strokeLinejoin="round" 18 | /> 19 | </svg> 20 | ) 21 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/CollapseIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const CollapseIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => ( 4 | <svg width="8" height="6" viewBox="0 0 8 6" xmlns="http://www.w3.org/2000/svg" {...props}> 5 | <path d="M4 6l4-6H0z" fillRule="evenodd" fill="currentColor"></path> 6 | </svg> 7 | ) 8 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/ConcentricCirclesIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const ConcentricCirclesIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => ( 4 | <svg 5 | width="24" 6 | height="24" 7 | viewBox="0 0 24 24" 8 | fill="none" 9 | xmlns="http://www.w3.org/2000/svg" 10 | {...props} 11 | > 12 | <path 13 | d="M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z" 14 | stroke="currentColor" 15 | strokeWidth="2" 16 | /> 17 | <path 18 | d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z" 19 | stroke="currentColor" 20 | strokeWidth="2" 21 | strokeLinecap="round" 22 | strokeLinejoin="round" 23 | /> 24 | </svg> 25 | ) 26 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/DateRangeIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const DateRangeIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => { 4 | return ( 5 | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" {...props}> 6 | <g fill="none" fillRule="evenodd"> 7 | <path 8 | stroke="currentColor" 9 | strokeLinecap="round" 10 | strokeLinejoin="round" 11 | strokeWidth="2" 12 | d="M5,4 L5,0 M13,4 L13,0 M4,8 L14,8 M4,13 L10,13 M2,18 L16,18 C17.1046,18 18,17.1046 18,16 L18,4 C18,2.89543 17.1046,2 16,2 L2,2 C0.89543,2 0,2.89543 0,4 L0,16 C0,17.1046 0.89543,18 2,18 Z" 13 | transform="translate(3 3)" 14 | /> 15 | <rect width="24" height="24" /> 16 | </g> 17 | </svg> 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/FullpageIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const FullpageIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => ( 4 | <svg viewBox="0 0 219 200" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> 5 | <rect x="0.47699" y="12.9384" width="218.238" height="187.062" fill="#BFDBFE" /> 6 | <path 7 | d="M4.47699 0.5H214.715C216.648 0.5 218.215 2.06701 218.215 4V12.4384H0.97699V4C0.97699 2.06701 2.54399 0.5 4.47699 0.5Z" 8 | fill="#F4F4F4" 9 | stroke="#F2F2F2" 10 | /> 11 | <rect x="6.08881" y="4.20886" width="4.36477" height="4.36477" rx="2.18238" fill="#D8D8D8" /> 12 | <rect x="15.1302" y="4.20886" width="4.36477" height="4.36477" rx="2.18238" fill="#D8D8D8" /> 13 | <rect x="24.1714" y="4.20886" width="4.36477" height="4.36477" rx="2.18238" fill="#D8D8D8" /> 14 | </svg> 15 | ) 16 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/ItalicIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const ItalicIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => ( 4 | <svg 5 | width="24" 6 | height="24" 7 | viewBox="0 0 24 24" 8 | fill="none" 9 | xmlns="http://www.w3.org/2000/svg" 10 | {...props} 11 | > 12 | <path 13 | d="M10 3H18M6 21H14M14.5 2.9762L9.5 21" 14 | stroke="currentColor" 15 | strokeWidth="2" 16 | strokeLinecap="round" 17 | strokeLinejoin="round" 18 | /> 19 | </svg> 20 | ) 21 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/LayoutCoverIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const LayoutCoverIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => { 4 | return ( 5 | <svg 6 | width="64" 7 | height="40" 8 | viewBox="0 0 64 40" 9 | fill="none" 10 | xmlns="http://www.w3.org/2000/svg" 11 | {...props} 12 | > 13 | <path 14 | fillRule="evenodd" 15 | clipRule="evenodd" 16 | d="M3 9C3 5.68629 5.68629 3 9 3H55C58.3137 3 61 5.68629 61 9V31C61 34.3137 58.3137 37 55 37H9C5.68629 37 3 34.3137 3 31V9ZM24 22H36V24H24V22ZM40 16H24V18H40V16Z" 17 | fill="currentColor" 18 | /> 19 | </svg> 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/LayoutFloatLeftIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const LayoutFloatLeftIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => { 4 | return ( 5 | <svg 6 | width="64" 7 | height="40" 8 | viewBox="0 0 64 40" 9 | fill="none" 10 | xmlns="http://www.w3.org/2000/svg" 11 | {...props} 12 | > 13 | <path d="M36 22H48V24H36V22Z" fill="currentColor" /> 14 | <path d="M12 14H28V26H12V14Z" fill="currentColor" /> 15 | <path d="M36 16H52V18H36V16Z" fill="currentColor" /> 16 | </svg> 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/LayoutFloatRightIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const LayoutFloatRightIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => { 4 | return ( 5 | <svg 6 | width="64" 7 | height="40" 8 | viewBox="0 0 64 40" 9 | fill="none" 10 | xmlns="http://www.w3.org/2000/svg" 11 | {...props} 12 | > 13 | <path d="M12 22H24V24H12V22Z" fill="currentColor" /> 14 | <path d="M36 14H52V26H36V14Z" fill="currentColor" /> 15 | <path d="M12 16H28V18H12V16Z" fill="currentColor" /> 16 | </svg> 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/LayoutInlineIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const LayoutInlineIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => { 4 | return ( 5 | <svg 6 | width="64" 7 | height="40" 8 | viewBox="0 0 64 40" 9 | fill="none" 10 | xmlns="http://www.w3.org/2000/svg" 11 | {...props} 12 | > 13 | <path d="M20 9H32V11H20V9Z" fill="currentColor" /> 14 | <path d="M20 14H44V26H20V14Z" fill="currentColor" /> 15 | <path d="M20 29H36V31H20V29Z" fill="currentColor" /> 16 | </svg> 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/LayoutSplitLeftIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const LayoutSplitLeftIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => { 4 | return ( 5 | <svg 6 | width="64" 7 | height="40" 8 | viewBox="0 0 64 40" 9 | fill="none" 10 | xmlns="http://www.w3.org/2000/svg" 11 | {...props} 12 | > 13 | <path d="M38 22H50V24H38V22Z" fill="currentColor" /> 14 | <path 15 | d="M3 9C3 5.68629 5.68629 3 9 3H30V37H9C5.68629 37 3 34.3137 3 31V9Z" 16 | fill="currentColor" 17 | /> 18 | <path d="M38 16H54V18H38V16Z" fill="currentColor" /> 19 | </svg> 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/LayoutSplitRightIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const LayoutSplitRightIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => { 4 | return ( 5 | <svg 6 | width="64" 7 | height="40" 8 | viewBox="0 0 64 40" 9 | fill="none" 10 | xmlns="http://www.w3.org/2000/svg" 11 | {...props} 12 | > 13 | <path d="M9 22H21V24H9V22Z" fill="currentColor" /> 14 | <path 15 | d="M34 3H55C58.3137 3 61 5.68629 61 9V31C61 34.3137 58.3137 37 55 37H34V3Z" 16 | fill="currentColor" 17 | /> 18 | <path d="M9 16H25V18H9V16Z" fill="currentColor" /> 19 | </svg> 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/LikeIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const LikeIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => { 4 | return ( 5 | <svg 6 | width="24" 7 | height="24" 8 | viewBox="0 0 24 24" 9 | fill="none" 10 | xmlns="http://www.w3.org/2000/svg" 11 | className="heyform-icon" 12 | {...props} 13 | > 14 | <path 15 | d="M7.5 4C4.46244 4 2 6.46245 2 9.5C2 15 8.5 20 12 21.1631C15.5 20 22 15 22 9.5C22 6.46245 19.5375 4 16.5 4C14.6399 4 12.9954 4.92345 12 6.3369C11.0046 4.92345 9.36015 4 7.5 4Z" 16 | className="heyform-icon-fill heyform-icon-stroke" 17 | strokeWidth="1" 18 | strokeLinecap="round" 19 | strokeLinejoin="round" 20 | /> 21 | </svg> 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/LongTextIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const LongTextIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => ( 4 | <svg 5 | width="24" 6 | height="24" 7 | viewBox="0 0 24 24" 8 | fill="none" 9 | xmlns="http://www.w3.org/2000/svg" 10 | {...props} 11 | > 12 | <path 13 | d="M4 6H20M4 12H20M4 18H11" 14 | stroke="currentColor" 15 | strokeWidth="2" 16 | strokeLinecap="round" 17 | strokeLinejoin="round" 18 | /> 19 | </svg> 20 | ) 21 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/ShortTextIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const ShortTextIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => ( 4 | <svg 5 | width="24" 6 | height="24" 7 | viewBox="0 0 24 24" 8 | fill="none" 9 | xmlns="http://www.w3.org/2000/svg" 10 | {...props} 11 | > 12 | <path 13 | d="M4 8H20M4 16H11" 14 | stroke="currentColor" 15 | strokeWidth="2" 16 | strokeLinecap="round" 17 | strokeLinejoin="round" 18 | /> 19 | </svg> 20 | ) 21 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/SignatureIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const SignatureIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => ( 4 | <svg 5 | width="24" 6 | height="24" 7 | viewBox="0 0 24 24" 8 | fill="none" 9 | xmlns="http://www.w3.org/2000/svg" 10 | {...props} 11 | > 12 | <path 13 | d="M8.4 12H5.25C4.00736 12 3 13.0074 3 14.25C3 15.4926 4.00736 16.5 5.25 16.5H18.75C19.9926 16.5 21 17.5074 21 18.75C21 19.9926 19.9926 21 18.75 21H9.3M12 12V9.75L18.75 3L21 5.25L14.25 12H12Z" 14 | stroke="currentColor" 15 | strokeWidth="2" 16 | strokeLinecap="round" 17 | strokeLinejoin="round" 18 | /> 19 | </svg> 20 | ) 21 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/StarIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const StarIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => { 4 | return ( 5 | <svg 6 | width="24" 7 | height="24" 8 | viewBox="0 0 24 24" 9 | fill="none" 10 | xmlns="http://www.w3.org/2000/svg" 11 | className="heyform-icon" 12 | {...props} 13 | > 14 | <path 15 | d="M11.9993 2.5L8.9428 8.7388L2 9.74555L7.02945 14.6625L5.8272 21.5L11.9993 18.2096L18.1727 21.5L16.9793 14.6625L22 9.74555L15.0956 8.7388L11.9993 2.5Z" 16 | className="heyform-icon-fill heyform-icon-stroke" 17 | strokeWidth="1" 18 | strokeLinejoin="round" 19 | /> 20 | </svg> 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/ThankYouIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const ThankYouIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => ( 4 | <svg 5 | width="24" 6 | height="24" 7 | viewBox="0 0 24 24" 8 | fill="none" 9 | xmlns="http://www.w3.org/2000/svg" 10 | {...props} 11 | > 12 | <path 13 | d="M11 5L11 19M11 5C11 3.89543 11.8954 3 13 3H19C20.1046 3 21 3.89543 21 5V19C21 20.1046 20.1046 21 19 21H13C11.8954 21 11 20.1046 11 19M11 5H9C7.89543 5 7 5.89543 7 7M11 19H9C7.89543 19 7 18.1046 7 17M7 17H5C3.89543 17 3 16.1046 3 15L3 9C3 7.89543 3.89543 7 5 7H7M7 17L7 7" 14 | stroke="currentColor" 15 | strokeWidth="2" 16 | strokeLinecap="round" 17 | strokeLinejoin="round" 18 | /> 19 | </svg> 20 | ) 21 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/WebsiteIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const WebsiteIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => ( 4 | <svg 5 | width="24" 6 | height="24" 7 | viewBox="0 0 24 24" 8 | fill="none" 9 | xmlns="http://www.w3.org/2000/svg" 10 | {...props} 11 | > 12 | <path 13 | d="M14.7 10.6H10.2C8.21176 10.6 6.6 12.2118 6.6 14.2C6.6 16.1882 8.21176 17.8 10.2 17.8H17.4C19.3882 17.8 21 16.1882 21 14.2C21 13.287 20.6601 12.4534 20.1 11.8188M3.9 12.9812C3.33987 12.3466 3 11.513 3 10.6C3 8.61176 4.61177 7 6.6 7H13.8C15.7882 7 17.4 8.61176 17.4 10.6C17.4 12.5882 15.7882 14.2 13.8 14.2H9.3" 14 | stroke="currentColor" 15 | strokeWidth="2" 16 | strokeLinecap="round" 17 | strokeLinejoin="round" 18 | /> 19 | </svg> 20 | ) 21 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/WelcomeIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const WelcomeIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => ( 4 | <svg 5 | width="24" 6 | height="24" 7 | viewBox="0 0 24 24" 8 | fill="none" 9 | xmlns="http://www.w3.org/2000/svg" 10 | {...props} 11 | > 12 | <path 13 | d="M13 19V5M13 19C13 20.1046 12.1046 21 11 21H5C3.89543 21 3 20.1046 3 19V5C3 3.89543 3.89543 3 5 3L11 3C12.1046 3 13 3.89543 13 5M13 19H15C16.1046 19 17 18.1046 17 17M13 5H15C16.1046 5 17 5.89543 17 7M17 7H19C20.1046 7 21 7.89543 21 9V15C21 16.1046 20.1046 17 19 17H17M17 7V17" 14 | stroke="currentColor" 15 | strokeWidth="2" 16 | strokeLinecap="round" 17 | strokeLinejoin="round" 18 | /> 19 | </svg> 20 | ) 21 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/WorkspaceIcon.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import type { FC } from 'react' 3 | 4 | export const WorkspaceIcon: FC<IComponentProps<HTMLOrSVGElement>> = ({ 5 | className, 6 | ...restProps 7 | }) => ( 8 | <svg 9 | className={clsx('avatar-placeholder', className)} 10 | viewBox="0 0 28 28" 11 | xmlns="http://www.w3.org/2000/svg" 12 | {...restProps} 13 | > 14 | <path 15 | d="M15 19.25a.75.75 0 001.086.67l3-1.5a.75.75 0 00.415-.67v-4.323a.75.75 0 00-1.086-.67l-3 1.5a.75.75 0 00-.415.67v4.323zm3.159-8.043a.75.75 0 000-1.341l-3.573-1.787a.75.75 0 00-.67 0l-3.574 1.787a.75.75 0 000 1.34l3.573 1.787a.749.749 0 00.67 0l3.574-1.786zm-8.074 1.55a.75.75 0 00-1.085.67v4.323a.75.75 0 00.415.67l3 1.5a.75.75 0 001.085-.67v-4.323a.75.75 0 00-.415-.67l-3-1.5z" 16 | fill="currentColor" 17 | /> 18 | </svg> 19 | ) 20 | -------------------------------------------------------------------------------- /packages/webapp/src/components/icons/YesOrNoIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const YesOrNoIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => ( 4 | <svg 5 | width="24" 6 | height="24" 7 | viewBox="0 0 24 24" 8 | fill="none" 9 | xmlns="http://www.w3.org/2000/svg" 10 | {...props} 11 | > 12 | <path 13 | d="M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3M12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3M12 21V3" 14 | stroke="currentColor" 15 | strokeWidth="2" 16 | strokeLinecap="round" 17 | strokeLinejoin="round" 18 | /> 19 | </svg> 20 | ) 21 | -------------------------------------------------------------------------------- /packages/webapp/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './icons' 2 | export * from './layouts' 3 | export * from './numberRange' 4 | export * from './photoPicker' 5 | export * from './Async' 6 | export * from './PhotoPickerField' 7 | export * from './SwitchField' 8 | export * from './Pagination' 9 | export * from './DragUploader' 10 | export * from './RedirectUriLink' 11 | export * from './MobilePhoneCode' 12 | export * from './CopyButton' 13 | export * from './Uploader' 14 | export * from './Heading' 15 | export * from './SubHeading' 16 | export * from './TagGroup' 17 | -------------------------------------------------------------------------------- /packages/webapp/src/components/layouts/AuthLayout.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import { AuthGuard, CommonLayout } from '@/components' 4 | 5 | export const AuthLayout: FC<IComponentProps> = ({ children }) => { 6 | return ( 7 | <AuthGuard> 8 | <CommonLayout>{children}</CommonLayout> 9 | </AuthGuard> 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /packages/webapp/src/components/layouts/CommonLayout.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const CommonLayout: FC<IComponentProps> = ({ children }) => { 4 | return ( 5 | <div className="flex min-h-screen flex-col justify-center bg-slate-50 py-12 sm:px-6 lg:px-8"> 6 | <div className="sm:mx-auto sm:w-full sm:max-w-md">{children}</div> 7 | </div> 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /packages/webapp/src/components/layouts/FormLayout.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | 3 | import { FormEmbedModal } from '@/pages/form/views/FormEmbedModal' 4 | import { FormNavbar } from '@/pages/form/views/FormNavbar' 5 | import { FormPreviewModal } from '@/pages/form/views/FormPreviewModal' 6 | import { FormShareModal } from '@/pages/form/views/FormShareModal' 7 | 8 | import { FormGuardLayout } from './FormGuardLayout' 9 | 10 | export const FormLayout: FC<IComponentProps> = ({ children }) => { 11 | return ( 12 | <FormGuardLayout> 13 | <div className="flex h-screen flex-col text-sm print:h-auto"> 14 | <FormNavbar /> 15 | <div className="content flex-1 bg-slate-50">{children}</div> 16 | </div> 17 | 18 | <FormPreviewModal /> 19 | <FormShareModal /> 20 | <FormEmbedModal /> 21 | </FormGuardLayout> 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /packages/webapp/src/components/layouts/PublicLayout.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const PublicLayout: FC<IComponentProps> = ({ children }) => { 4 | return <>{children}</> 5 | } 6 | -------------------------------------------------------------------------------- /packages/webapp/src/components/layouts/index.scss: -------------------------------------------------------------------------------- 1 | .workspace-container { 2 | @apply min-h-screen; 3 | 4 | @media (min-width: 768px) { 5 | padding-left: 16rem; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/webapp/src/components/layouts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CommonLayout' 2 | export * from './AuthGuard' 3 | export * from './AuthLayout' 4 | export * from './WorkspaceGuard' 5 | export * from './WorkspaceLayout' 6 | export * from './FormGuardLayout' 7 | export * from './FormLayout' 8 | export * from './PublicLayout' 9 | -------------------------------------------------------------------------------- /packages/webapp/src/components/numberRange/style.scss: -------------------------------------------------------------------------------- 1 | .number-range { 2 | @apply flex items-center w-full; 3 | 4 | .select { 5 | @apply w-auto; 6 | } 7 | 8 | &.number-range-unlimited { 9 | .select { 10 | @apply w-full; 11 | } 12 | } 13 | 14 | .select-button { 15 | @apply pr-7 cursor-pointer; 16 | } 17 | 18 | .input { 19 | @apply px-2; 20 | } 21 | } 22 | 23 | .number-range-popup { 24 | @apply w-56 #{!important}; 25 | } 26 | -------------------------------------------------------------------------------- /packages/webapp/src/components/photoPicker/style.scss: -------------------------------------------------------------------------------- 1 | .photo-picker { 2 | .modal-body { 3 | @apply p-0 overflow-hidden; 4 | } 5 | 6 | .modal-close-button { 7 | @apply top-3 right-5; 8 | } 9 | 10 | .tabs-wrapper { 11 | @apply flex flex-col; 12 | } 13 | 14 | .tabs-nav-list { 15 | @apply px-4; 16 | } 17 | 18 | .tabs-pane-group { 19 | @apply h-96; 20 | @extend .scrollbar; 21 | } 22 | 23 | .tabs-pane { 24 | @apply h-full p-4; 25 | } 26 | 27 | .drag-uploader { 28 | @apply h-full; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/avatar/index.ts: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import type { AvatarProps } from './Avatar' 4 | import Avatar from './Avatar' 5 | import type { AvatarGroupProps } from './Group' 6 | import Group from './Group' 7 | 8 | type ExportAvatarType = FC<AvatarProps> & { 9 | Group: FC<AvatarGroupProps> 10 | } 11 | 12 | const ExportAvatar = Avatar as ExportAvatarType 13 | ExportAvatar.Group = Group 14 | 15 | export default ExportAvatar 16 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/avatar/style.scss: -------------------------------------------------------------------------------- 1 | .avatar { 2 | @apply inline-flex items-center justify-center overflow-hidden w-8 h-8 bg-slate-200 bg-opacity-70; 3 | 4 | img { 5 | @apply h-full w-full object-cover; 6 | } 7 | 8 | .avatar-text { 9 | @apply text-lg font-medium leading-none text-slate-500; 10 | } 11 | 12 | svg { 13 | @apply text-opacity-70 text-slate-400; 14 | } 15 | } 16 | 17 | .avatar-rounded { 18 | @apply rounded-md; 19 | } 20 | 21 | .avatar-circular { 22 | @apply rounded-full; 23 | } 24 | 25 | .avatar-placeholder { 26 | @apply h-full w-full text-slate-300; 27 | } 28 | 29 | .avatar-group { 30 | @apply flex -space-x-2 overflow-hidden; 31 | 32 | .avatar { 33 | @apply ring-2 ring-white; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/button/Group.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import type { FC } from 'react' 3 | 4 | import { IComponentProps } from '../typing' 5 | 6 | export type ButtonGroupProps = IComponentProps 7 | 8 | const Group: FC<ButtonGroupProps> = ({ className, children, ...restProps }) => { 9 | return ( 10 | <span className={clsx('button-group', className)} {...restProps}> 11 | {children} 12 | </span> 13 | ) 14 | } 15 | 16 | export default Group 17 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/button/Link.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import type { FC } from 'react' 3 | 4 | import type { ButtonProps } from './Button' 5 | import Button from './Button' 6 | 7 | const Link: FC<ButtonProps> = ({ className, children, ...restProps }) => { 8 | return ( 9 | <Button className={clsx('button-link', className)} {...restProps}> 10 | {children} 11 | </Button> 12 | ) 13 | } 14 | 15 | export default Link 16 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import type { ButtonProps } from './Button' 4 | import Button from './Button' 5 | import type { ButtonGroupProps } from './Group' 6 | import Group from './Group' 7 | import Link from './Link' 8 | 9 | type ExportButtonType = FC<ButtonProps> & { 10 | Group: FC<ButtonGroupProps> 11 | Link: FC<ButtonProps> 12 | } 13 | 14 | const ExportButton = Button as ExportButtonType 15 | 16 | ExportButton.Group = Group 17 | ExportButton.Link = Link 18 | 19 | export default ExportButton 20 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/checkbox/index.ts: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import type { CheckboxProps } from './Checkbox' 4 | import Checkbox from './Checkbox' 5 | import type { CheckboxGroupProps } from './Group' 6 | import Group from './Group' 7 | 8 | type ExportCheckboxType = FC<CheckboxProps> & { 9 | Group: FC<CheckboxGroupProps> 10 | } 11 | 12 | const ExportCheckbox = Checkbox as ExportCheckboxType 13 | ExportCheckbox.Group = Group 14 | 15 | export default ExportCheckbox 16 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/checkbox/style.scss: -------------------------------------------------------------------------------- 1 | .checkbox-wrapper { 2 | @apply relative flex items-start; 3 | } 4 | 5 | .checkbox { 6 | @apply flex items-center h-5; 7 | } 8 | 9 | .checkbox-input { 10 | @apply focus:ring-blue-600 h-4 w-4 text-blue-700 border-gray-300; 11 | 12 | &[type=checkbox] { 13 | @apply rounded; 14 | } 15 | } 16 | 17 | .checkbox-input-checked { 18 | } 19 | 20 | .checkbox-description { 21 | @apply ml-3 sm:text-sm font-medium text-slate-700; 22 | } 23 | 24 | .checkbox-group { 25 | @apply space-y-4; 26 | } 27 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/color-picker/helper.ts: -------------------------------------------------------------------------------- 1 | import { helper } from '@heyform-inc/utils' 2 | 3 | export function rgbaToString(type: 'hex' | 'rgba', rgba: number[]) { 4 | if (helper.isValidArray(rgba)) { 5 | const [r, g, b, alpha] = rgba! 6 | 7 | if (type === 'hex') { 8 | let value = '#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('') 9 | 10 | if (alpha < 1) { 11 | value += Math.round(alpha * 0xff) 12 | .toString(16) 13 | .padStart(2, '0') 14 | } 15 | 16 | return value 17 | } else { 18 | return `rgba(${r}, ${g}, ${b}, ${alpha})` 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/date-picker/store.ts: -------------------------------------------------------------------------------- 1 | import { Dayjs } from 'dayjs' 2 | import { createContext } from 'react' 3 | 4 | import { DatePickerCommonOptions } from './common' 5 | 6 | interface IStore extends DatePickerCommonOptions { 7 | format?: string 8 | timeFormat?: string 9 | value?: Dayjs 10 | temp: Dayjs 11 | current: Dayjs 12 | isYearPickerOpen?: boolean 13 | isMonthPickerOpen?: boolean 14 | togglePicker: () => void 15 | toggleYearPicker: () => void 16 | toggleMonthPicker: () => void 17 | toPrevious: () => void 18 | toNext: () => void 19 | updateYear: (year: number) => void 20 | updateMonth: (month: number) => void 21 | updateValue: (date: Dayjs) => void 22 | setIsOpen: (open: boolean) => void 23 | } 24 | 25 | export const DatePickerStore = createContext<IStore>({} as IStore) 26 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/dropdown/style.scss: -------------------------------------------------------------------------------- 1 | .dropdown { 2 | @apply relative inline-block text-left; 3 | 4 | .menus { 5 | @apply absolute z-10 transition ease-in-out duration-100; 6 | } 7 | 8 | &.dropdown-top-left { 9 | .menus { 10 | @apply origin-top-left left-0; 11 | } 12 | } 13 | 14 | &.dropdown-top-right { 15 | .menus { 16 | @apply origin-top-right right-0; 17 | } 18 | } 19 | } 20 | 21 | .dropdown-trigger { 22 | @apply w-full; 23 | } 24 | 25 | .dropdown-popup-enter-active, 26 | .dropdown-popup-exit { 27 | @apply transform opacity-100 scale-100; 28 | } 29 | 30 | .dropdown-popup-enter, 31 | .dropdown-popup-exit-active { 32 | @apply transform opacity-0 scale-95; 33 | } 34 | 35 | .dropdown-popup-exit-done { 36 | @apply hidden; 37 | } 38 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/empty-states/style.scss: -------------------------------------------------------------------------------- 1 | .empty-states { 2 | @apply text-center; 3 | } 4 | 5 | .empty-states-icon { 6 | svg { 7 | @apply mx-auto h-12 w-12 text-slate-400; 8 | } 9 | } 10 | 11 | .empty-states-title { 12 | @apply mt-2 text-base font-medium text-slate-900; 13 | } 14 | 15 | .empty-states-description { 16 | @apply mt-1 w-full mx-auto text-sm text-slate-500; 17 | 18 | @media (min-width: 768px) { 19 | width: 32rem; 20 | } 21 | } 22 | 23 | .empty-states-action { 24 | @apply mt-6; 25 | } 26 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/form/index.ts: -------------------------------------------------------------------------------- 1 | import Form from 'rc-field-form' 2 | import type { FC } from 'react' 3 | 4 | import type { CustomFormProps } from './CustomForm' 5 | import CustomForm from './CustomForm' 6 | import type { FormItemProps } from './FormItem' 7 | import FormItem from './FormItem' 8 | import type { SwitchItemProps } from './SwitchItem' 9 | import SwitchItem from './SwitchItem' 10 | 11 | export { useForm } from 'rc-field-form' 12 | 13 | type ExportFormType = typeof Form & { 14 | Item: FC<FormItemProps> 15 | Switch: FC<SwitchItemProps> 16 | Custom: FC<CustomFormProps> 17 | } 18 | 19 | const ExportForm = Form as ExportFormType 20 | 21 | ExportForm.Item = FormItem 22 | ExportForm.Switch = SwitchItem 23 | ExportForm.Custom = CustomForm 24 | 25 | export default ExportForm 26 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/heading/style.scss: -------------------------------------------------------------------------------- 1 | .heading { 2 | @apply lg:flex lg:items-center lg:justify-between; 3 | } 4 | 5 | .heading-flex-auto { 6 | @apply flex-1 min-w-0; 7 | } 8 | 9 | .heading-left, 10 | .heading-icon { 11 | @apply flex items-center; 12 | } 13 | 14 | .heading-icon { 15 | @apply mr-5; 16 | } 17 | 18 | .heading-title { 19 | @apply text-3xl font-bold text-slate-900; 20 | } 21 | 22 | .heading-description { 23 | @apply text-sm font-medium text-slate-500 mt-1 flex flex-col sm:flex-row sm:flex-wrap sm:space-x-6; 24 | } 25 | 26 | .heading-actions { 27 | @apply mt-6 flex flex-col-reverse justify-items-stretch space-y-4 space-y-reverse sm:flex-row-reverse sm:justify-end sm:space-x-reverse sm:space-y-0 sm:space-x-3 md:mt-0 md:flex-row md:space-x-3; 28 | } 29 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/icons/DefaultAvatarIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import { IComponentProps } from '../typing' 4 | 5 | const DefaultAvatarIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => { 6 | return ( 7 | <svg fill="currentColor" viewBox="0 0 24 24" {...props}> 8 | <path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" /> 9 | </svg> 10 | ) 11 | } 12 | 13 | export default DefaultAvatarIcon 14 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/icons/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DefaultAvatarIcon } from './DefaultAvatarIcon' 2 | export { default as EyeCloseIcon } from './EyeCloseIcon' 3 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import type { InputProps } from './Input' 4 | import Input from './Input' 5 | import type { InputPasswordProps } from './Password' 6 | import Password from './Password' 7 | import type { InputSearchProps } from './Search' 8 | import Search from './Search' 9 | import type { TextareaProps } from './Textarea' 10 | import Textarea from './Textarea' 11 | 12 | type ExportInputType = FC<InputProps> & { 13 | Password: FC<InputPasswordProps> 14 | Textarea: FC<TextareaProps> 15 | Search: FC<InputSearchProps> 16 | } 17 | 18 | const ExportInput = Input as ExportInputType 19 | 20 | ExportInput.Password = Password 21 | ExportInput.Textarea = Textarea 22 | ExportInput.Search = Search 23 | 24 | export default ExportInput 25 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/loader/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import type { FC, SVGAttributes } from 'react' 3 | 4 | const Loader: FC<SVGAttributes<HTMLOrSVGElement>> = ({ className, ...restProps }) => { 5 | return ( 6 | <svg 7 | width="22" 8 | height="5" 9 | viewBox="0 0 21 5" 10 | fill="none" 11 | xmlns="http://www.w3.org/2000/svg" 12 | className={clsx('loader', className)} 13 | {...restProps} 14 | > 15 | <rect className="loader-span" width="5" height="5" rx="2.5" fill="currentColor" /> 16 | <rect className="loader-span" x="8" width="5" height="5" rx="2.5" fill="currentColor" /> 17 | <rect className="loader-span" x="16" width="5" height="5" rx="2.5" fill="currentColor" /> 18 | </svg> 19 | ) 20 | } 21 | 22 | export default Loader 23 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/loader/style.scss: -------------------------------------------------------------------------------- 1 | @keyframes loader-blink { 2 | 0% { 3 | opacity: 0.2; 4 | } 5 | 20% { 6 | opacity: 1; 7 | } 8 | 100% { 9 | opacity: 0.2; 10 | } 11 | } 12 | 13 | .loader-span { 14 | animation-name: loader-blink; 15 | animation-duration: 1.4s; 16 | animation-iteration-count: infinite; 17 | animation-fill-mode: both; 18 | 19 | &:nth-of-type(2) { 20 | animation-delay: 0.2s; 21 | } 22 | 23 | &:nth-of-type(3) { 24 | animation-delay: 0.4s; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/menu/Divider.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import type { FC } from 'react' 3 | 4 | import { IComponentProps } from '../typing' 5 | 6 | const MenuDivider: FC<IComponentProps<HTMLDivElement>> = ({ className, ...restProps }) => { 7 | return <div className={clsx('menu-divider', className)} role="none" {...restProps} /> 8 | } 9 | 10 | export default MenuDivider 11 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/menu/Label.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import type { FC } from 'react' 3 | import { MouseEvent, ReactNode } from 'react' 4 | 5 | import { IComponentProps } from '../typing' 6 | 7 | export interface MenuLabelProps extends Omit<IComponentProps, 'onClick'> { 8 | icon?: ReactNode 9 | label: ReactNode 10 | onClick?: (event?: MouseEvent<HTMLDivElement>) => void 11 | } 12 | 13 | const MenuLabel: FC<MenuLabelProps> = ({ className, icon, label, onClick, ...restProps }) => { 14 | return ( 15 | <div className={clsx('menu-label', className)} role="none" onClick={onClick} {...restProps}> 16 | {icon} 17 | {label} 18 | </div> 19 | ) 20 | } 21 | 22 | export default MenuLabel 23 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/menu/index.ts: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import { IComponentProps } from '../typing' 4 | import MenuDivider from './Divider' 5 | import type { MenuItemProps } from './Item' 6 | import MenuItem from './Item' 7 | import type { MenuLabelProps } from './Label' 8 | import MenuLabel from './Label' 9 | import type { MenusProps } from './Menus' 10 | import Menus from './Menus' 11 | 12 | type ExportMenusType = FC<MenusProps> & { 13 | Item: FC<MenuItemProps> 14 | Label: FC<MenuLabelProps> 15 | Divider: FC<IComponentProps<HTMLDivElement>> 16 | } 17 | 18 | const ExportMenus = Menus as unknown as ExportMenusType 19 | ExportMenus.Item = MenuItem 20 | ExportMenus.Label = MenuLabel 21 | ExportMenus.Divider = MenuDivider 22 | 23 | export default ExportMenus 24 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/modal/index.ts: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import type { ConfirmModalProps } from './Confirm' 4 | import Confirm from './Confirm' 5 | import type { ModalProps } from './Modal' 6 | import Modal from './Modal' 7 | 8 | type ExportModalType = FC<ModalProps> & { 9 | Confirm: FC<ConfirmModalProps> 10 | } 11 | 12 | const ExportModal = Modal as ExportModalType 13 | ExportModal.Confirm = Confirm 14 | 15 | export default ExportModal 16 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/navbar/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import type { FC } from 'react' 3 | 4 | import { IComponentProps } from '../typing' 5 | 6 | const Navbar: FC<IComponentProps> = ({ className, children, ...restProps }) => { 7 | return ( 8 | <div className={clsx('navbar', className)} {...restProps}> 9 | <nav aria-label="Tabs">{children}</nav> 10 | </div> 11 | ) 12 | } 13 | 14 | export default Navbar 15 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/navbar/style.scss: -------------------------------------------------------------------------------- 1 | .navbar { 2 | @apply border-b border-gray-200; 3 | 4 | nav { 5 | @apply mt-2 -mb-px flex space-x-8; 6 | } 7 | 8 | a { 9 | @apply border-transparent font-medium text-slate-500 hover:text-slate-700 hover:border-gray-200 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm; 10 | 11 | &.active { 12 | @apply border-blue-600 text-blue-700; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/portal/index.tsx: -------------------------------------------------------------------------------- 1 | import type { FC, ReactNode } from 'react' 2 | import { createPortal } from 'react-dom' 3 | 4 | export interface PortalProps { 5 | visible?: boolean 6 | container?: HTMLElement 7 | children: ReactNode 8 | } 9 | 10 | const Portal: FC<PortalProps> = ({ visible, container, children }) => { 11 | if (!visible || !children) { 12 | return null 13 | } 14 | 15 | return createPortal(children, container || document.body) 16 | } 17 | 18 | export default Portal 19 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/radio/index.ts: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import type { RadioGroupProps } from './Group' 4 | import Group from './Group' 5 | import type { RadioProps } from './Radio' 6 | import Radio from './Radio' 7 | 8 | type ExportRadioType = FC<RadioProps> & { 9 | Group: FC<RadioGroupProps> 10 | } 11 | 12 | const ExportRadio = Radio as ExportRadioType 13 | ExportRadio.Group = Group 14 | 15 | export default ExportRadio 16 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/radio/style.scss: -------------------------------------------------------------------------------- 1 | .radio-wrapper { 2 | @apply relative flex items-start; 3 | } 4 | 5 | .radio { 6 | @apply flex items-center h-5; 7 | } 8 | 9 | .radio-input { 10 | @apply h-4 w-4 text-blue-700 border-gray-300 focus:ring-blue-600; 11 | } 12 | 13 | .radio-input-checked { 14 | } 15 | 16 | .radio-description { 17 | @apply ml-3 sm:text-sm font-medium text-slate-700; 18 | } 19 | 20 | .radio-group { 21 | @apply space-y-4; 22 | } 23 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/rate/style.scss: -------------------------------------------------------------------------------- 1 | .rate { 2 | @apply flex items-center; 3 | } 4 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/slider/style.scss: -------------------------------------------------------------------------------- 1 | .slider { 2 | @apply relative w-full h-3 py-2.5; 3 | } 4 | 5 | .slider-progress { 6 | @apply h-0.5 bg-slate-100 rounded-sm; 7 | } 8 | 9 | .slider-progress-track { 10 | @apply relative h-full mt-0 bg-blue-700; 11 | 12 | &:after { 13 | @apply absolute top-1/2 -mt-1.5 -right-1.5 w-3 h-3 bg-blue-700 rounded-full; 14 | content: ''; 15 | } 16 | } 17 | 18 | .slider-input { 19 | @apply absolute inset-0 opacity-0 w-full h-full cursor-pointer appearance-none; 20 | } 21 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/spin/style.scss: -------------------------------------------------------------------------------- 1 | .spin { 2 | @apply w-5 h-5 animate-spin; 3 | } 4 | 5 | .spin-circle { 6 | @apply opacity-25; 7 | } 8 | 9 | .spin-path { 10 | @apply opacity-75; 11 | } 12 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/style.scss: -------------------------------------------------------------------------------- 1 | @import "avatar/style"; 2 | @import "badge/style"; 3 | @import "button/style"; 4 | @import "checkbox/style"; 5 | @import "color-picker/style"; 6 | @import "date-picker/style"; 7 | @import "dropdown/style"; 8 | @import "empty-states/style"; 9 | @import "form/style"; 10 | @import "heading/style"; 11 | @import "radio/style"; 12 | @import "rate/style"; 13 | @import "input/style"; 14 | @import "menu/style"; 15 | @import "modal/style"; 16 | @import "navbar/style"; 17 | @import "notification/style"; 18 | @import "popup/style"; 19 | @import "select/style"; 20 | @import "slider/style"; 21 | @import "spin/style"; 22 | @import "stepper/style"; 23 | @import "switch/style"; 24 | @import "table/style"; 25 | @import "tabs/style"; 26 | @import "tooltip/style"; 27 | @import "progress/style"; 28 | @import "loader/style"; 29 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/switch/index.ts: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import type { SwitchGroupProps } from './Group' 4 | import Group from './Group' 5 | import type { SwitchProps } from './Switch' 6 | import Switch from './Switch' 7 | 8 | type ExportCheckboxType = FC<SwitchProps> & { 9 | Group: FC<SwitchGroupProps> 10 | } 11 | 12 | const ExportSwitch = Switch as ExportCheckboxType 13 | ExportSwitch.Group = Group 14 | 15 | export default ExportSwitch 16 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/tabs/index.ts: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import type { TabsPaneProps } from './Pane' 4 | import Pane from './Pane' 5 | import type { TabsProps } from './Tabs' 6 | import Tabs from './Tabs' 7 | 8 | type ExportTabsType = FC<TabsProps> & { 9 | Pane: FC<TabsPaneProps> 10 | } 11 | 12 | const ExportTabs = Tabs as ExportTabsType 13 | ExportTabs.Pane = Pane 14 | 15 | export default ExportTabs 16 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/tooltip/style.scss: -------------------------------------------------------------------------------- 1 | .tooltip { 2 | @apply px-2.5 py-1.5 z-50 text-center text-xs text-white break-words whitespace-pre pointer-events-none rounded; 3 | text-decoration: none; 4 | text-shadow: none; 5 | text-transform: none; 6 | letter-spacing: normal; 7 | background: #1f1f1f; 8 | } 9 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/typing.ts: -------------------------------------------------------------------------------- 1 | import type { HTMLAttributes, ReactNode } from 'react' 2 | 3 | export type IComponentProps<E = HTMLElement> = HTMLAttributes<E> 4 | 5 | export type IMapType<V = any> = Record<string | number | symbol, V> 6 | 7 | export type IOptionType = IMapType<any> & { 8 | label: ReactNode 9 | value: any 10 | disabled?: boolean 11 | } 12 | 13 | export type IOptionGroupType = { 14 | group: ReactNode 15 | children: IOptionType[] 16 | } 17 | -------------------------------------------------------------------------------- /packages/webapp/src/consts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './date' 2 | export * from './environments' 3 | export * from './formBuilder' 4 | export * from './formSettings' 5 | export * from './graphql' 6 | -------------------------------------------------------------------------------- /packages/webapp/src/models/compose.ts: -------------------------------------------------------------------------------- 1 | import { FieldKindEnum } from '@heyform-inc/shared-types-enums' 2 | import type { ReactNode } from 'react' 3 | 4 | export interface FieldItemProps { 5 | kind: FieldKindEnum 6 | icon: ReactNode 7 | label: string 8 | description: string 9 | } 10 | 11 | export interface ComponentsOptions { 12 | name: string 13 | children: FieldItemProps[] 14 | } 15 | 16 | export enum ComposeTabKeyEnum { 17 | COMPONENT = 'component', 18 | THEME = 'theme', 19 | CUSTOMIZE = 'customize' 20 | } 21 | -------------------------------------------------------------------------------- /packages/webapp/src/models/form.ts: -------------------------------------------------------------------------------- 1 | import type { FormField as IFormField } from '@heyform-inc/shared-types-enums' 2 | import { Property } from '@heyform-inc/shared-types-enums' 3 | 4 | export enum IntegrationStatusEnum { 5 | ACTIVE = 1, 6 | DISABLED 7 | } 8 | 9 | export interface FormIntegration { 10 | formId: string 11 | thirdPartyId: string 12 | subKind: string 13 | uniqueName: string 14 | attributes?: Record<string, any> 15 | status: IntegrationStatusEnum 16 | } 17 | 18 | export interface FormAnalyticsSummary { 19 | totalVisits: number 20 | submissionCount: number 21 | completeRate: number 22 | averageDuration: string 23 | } 24 | 25 | export interface FormField extends IFormField { 26 | isCollapsed?: boolean 27 | parent?: IFormField 28 | properties?: Omit<Property, 'fields'> & { 29 | fields?: FormField[] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/webapp/src/models/index.ts: -------------------------------------------------------------------------------- 1 | export type { FormModel, SubmissionModel } from '@heyform-inc/shared-types-enums' 2 | export * from './project' 3 | export * from './user' 4 | export * from './workspace' 5 | export * from './form' 6 | export * from './template' 7 | export * from './integration' 8 | -------------------------------------------------------------------------------- /packages/webapp/src/models/integration.ts: -------------------------------------------------------------------------------- 1 | export interface AppModel { 2 | id: string 3 | uniqueId: string 4 | category: string 5 | name: string 6 | description?: string 7 | avatar?: string 8 | homepage?: string 9 | helpLinkUrl?: string 10 | attributes?: IMapType 11 | integration?: IntegrationModel 12 | } 13 | 14 | export interface IntegrationModel { 15 | appId: string 16 | attributes?: IMapType 17 | formId: string 18 | status: number 19 | } 20 | -------------------------------------------------------------------------------- /packages/webapp/src/models/project.ts: -------------------------------------------------------------------------------- 1 | export interface ProjectModel { 2 | id: string 3 | teamId: string 4 | name: string 5 | ownerId: string 6 | members: string[] 7 | formCount: number 8 | isOwner?: boolean 9 | } 10 | -------------------------------------------------------------------------------- /packages/webapp/src/models/template.ts: -------------------------------------------------------------------------------- 1 | import type { FormModel } from '@heyform-inc/shared-types-enums' 2 | 3 | export interface TemplateModal 4 | extends Pick<FormModel, 'id' | 'name' | 'interactiveMode' | 'kind' | 'fields' | 'themeSettings'> { 5 | category: string 6 | published: boolean 7 | } 8 | -------------------------------------------------------------------------------- /packages/webapp/src/models/user.ts: -------------------------------------------------------------------------------- 1 | export interface UserModel { 2 | id: string 3 | name: string 4 | email: string 5 | phoneNumber: string 6 | avatar: string 7 | note: string 8 | lastSeenAt?: number 9 | isSocialAccount?: boolean 10 | isEmailVerified?: boolean 11 | isDeletionScheduled?: boolean 12 | deletionScheduledAt?: number 13 | status: number 14 | createdAt: string 15 | updatedAt: string 16 | 17 | // Only for project members 18 | isAssigned?: boolean 19 | isOwner?: boolean 20 | isSelf?: boolean 21 | } 22 | -------------------------------------------------------------------------------- /packages/webapp/src/models/workspace.ts: -------------------------------------------------------------------------------- 1 | import { ProjectModel } from '@/models/project' 2 | import { UserModel } from '@/models/user' 3 | 4 | export interface WorkspaceModel { 5 | id: string 6 | name: string 7 | ownerId: string 8 | avatar?: string 9 | enableCustomDomain?: boolean 10 | inviteCode: string 11 | inviteCodeExpireAt?: number 12 | allowJoinByInviteLink: boolean 13 | storageQuota: number 14 | memberCount: number 15 | additionalSeats: number 16 | projects: ProjectModel[] 17 | members: UserModel[] 18 | isOwner?: boolean 19 | owner?: UserModel 20 | createdAt?: number 21 | } 22 | 23 | export interface WorkspaceMemberModel { 24 | id: string 25 | name: string 26 | email: string 27 | avatar: string 28 | isOwner: boolean 29 | lastSeenAt?: number 30 | } 31 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Analytics/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | 3 | import Report from './views/Report' 4 | import Summary from './views/Summary' 5 | 6 | const Analytics: FC = () => { 7 | return ( 8 | <div className="form-content-container"> 9 | <div className="container mx-auto max-w-5xl pt-14"> 10 | <div className="mx-4 md:mx-0"> 11 | <Summary /> 12 | <Report /> 13 | </div> 14 | </div> 15 | </div> 16 | ) 17 | } 18 | 19 | export default Analytics 20 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/consts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './country' 2 | export * from './date' 3 | export * from './layout' 4 | export * from './rating' 5 | export * from './logic' 6 | export * from './payment' 7 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/consts/logic.ts: -------------------------------------------------------------------------------- 1 | import { FieldKindEnum } from '@heyform-inc/shared-types-enums' 2 | 3 | import { NumberVariableIcon, StringVariableIcon } from '@/components' 4 | import { type FieldConfig } from '@/pages/form/Create/views/FieldConfig' 5 | 6 | export const VARIABLE_KIND_CONFIGS: FieldConfig[] = [ 7 | { 8 | kind: 'number' as FieldKindEnum, 9 | icon: NumberVariableIcon, 10 | label: 'formBuilder.variable.number', 11 | textColor: '#06a17e', 12 | backgroundColor: 'rgba(6,161,126,0.05)' 13 | }, 14 | { 15 | kind: 'string' as FieldKindEnum, 16 | icon: StringVariableIcon, 17 | label: 'formBuilder.variable.string', 18 | textColor: '#06a17e', 19 | backgroundColor: 'rgba(6,161,126,0.05)' 20 | } 21 | ] 22 | 23 | export const VARIABLE_INPUT_TYPES: any = { 24 | number: 'number', 25 | string: 'text' 26 | } 27 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/store/hook.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | 3 | import type { IContext } from './context' 4 | import { StoreContext } from './context' 5 | 6 | export function useStoreContext(): IContext { 7 | return useContext(StoreContext) 8 | } 9 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context' 2 | export * from './hook' 3 | export * from './actions' 4 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './field' 2 | export * from './logic' 3 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/views/Compose/Blocks/Country.tsx: -------------------------------------------------------------------------------- 1 | import { IconChevronRight } from '@tabler/icons-react' 2 | import type { FC } from 'react' 3 | import { useTranslation } from 'react-i18next' 4 | 5 | import { FakeSubmit } from '@/pages/form/Create/views/Compose/FakeSubmit' 6 | 7 | import { FakeSelect } from '../FakeSelect' 8 | import type { BlockProps } from './Block' 9 | import { Block } from './Block' 10 | 11 | export const Country: FC<BlockProps> = ({ field, locale, ...restProps }) => { 12 | const { t } = useTranslation() 13 | 14 | return ( 15 | <Block className="heyform-country" field={field} locale={locale} {...restProps}> 16 | <FakeSelect /> 17 | <FakeSubmit text={t('Next', { lng: locale })} icon={<IconChevronRight />} /> 18 | </Block> 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/views/Compose/Blocks/LegalTerms.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | import { useTranslation } from 'react-i18next' 3 | 4 | import { FakeRadio } from '../FakeRadio' 5 | import type { BlockProps } from './Block' 6 | import { Block } from './Block' 7 | 8 | export const LegalTerms: FC<BlockProps> = ({ field, locale, ...restProps }) => { 9 | const { t } = useTranslation() 10 | 11 | return ( 12 | <Block className="heyform-legal-terms" field={field} locale={locale} {...restProps}> 13 | <div className="heyform-radio-group w-56"> 14 | <FakeRadio hotkey="Y" label={t('I accept', { lng: locale })} /> 15 | <FakeRadio hotkey="N" label={t("I don't accept", { lng: locale })} /> 16 | </div> 17 | </Block> 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/views/Compose/Blocks/Rating.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import { Rate } from '@/components/ui' 4 | import { RATING_SHAPE_CONFIG } from '@/pages/form/Create/consts' 5 | 6 | import type { BlockProps } from './Block' 7 | import { Block } from './Block' 8 | 9 | export const Rating: FC<BlockProps> = ({ field, locale, ...restProps }) => { 10 | function characterRender(index: number) { 11 | return ( 12 | <> 13 | {RATING_SHAPE_CONFIG[field.properties?.shape || 'star']} 14 | <span className="heyform-rate-index">{index}</span> 15 | </> 16 | ) 17 | } 18 | 19 | return ( 20 | <Block className="heyform-rating" field={field} locale={locale} {...restProps}> 21 | <Rate count={field.properties?.total || 5} itemRender={characterRender} /> 22 | </Block> 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/views/Compose/Blocks/Statement.tsx: -------------------------------------------------------------------------------- 1 | import { IconChevronRight } from '@tabler/icons-react' 2 | import type { FC } from 'react' 3 | import { useTranslation } from 'react-i18next' 4 | 5 | import { FakeSubmit } from '@/pages/form/Create/views/Compose/FakeSubmit' 6 | 7 | import type { BlockProps } from './Block' 8 | import { Block } from './Block' 9 | 10 | export const Statement: FC<BlockProps> = ({ field, locale, ...restProps }) => { 11 | const { t } = useTranslation() 12 | 13 | return ( 14 | <Block className="heyform-statement" field={field} locale={locale} {...restProps}> 15 | <FakeSubmit 16 | text={field.properties?.buttonText || t('Next', { lng: locale })} 17 | icon={<IconChevronRight />} 18 | /> 19 | </Block> 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/views/Compose/Blocks/ThankYou.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | import { useTranslation } from 'react-i18next' 3 | 4 | import type { BlockProps } from './Block' 5 | import { Block } from './Block' 6 | 7 | export const ThankYou: FC<BlockProps> = ({ field, locale, className, children, ...restProps }) => { 8 | const { t } = useTranslation() 9 | 10 | return ( 11 | <Block 12 | className="heyform-thank-you heyform-empty-state" 13 | field={field} 14 | locale={locale} 15 | {...restProps} 16 | /> 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/views/Compose/Blocks/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | import { useTranslation } from 'react-i18next' 3 | 4 | import { FakeSubmit } from '@/pages/form/Create/views/Compose/FakeSubmit' 5 | 6 | import type { BlockProps } from './Block' 7 | import { Block } from './Block' 8 | 9 | export const Welcome: FC<BlockProps> = ({ field, locale, ...restProps }) => { 10 | const { t } = useTranslation() 11 | 12 | return ( 13 | <Block 14 | className="heyform-welcome heyform-empty-state" 15 | field={field} 16 | locale={locale} 17 | {...restProps} 18 | > 19 | <FakeSubmit text={field.properties?.buttonText || t('Next', { lng: locale })} /> 20 | </Block> 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/views/Compose/Blocks/YesNo.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | import { useTranslation } from 'react-i18next' 3 | 4 | import { FakeRadio } from '../FakeRadio' 5 | import type { BlockProps } from './Block' 6 | import { Block } from './Block' 7 | 8 | export const YesNo: FC<BlockProps> = ({ field, locale, ...restProps }) => { 9 | const { t } = useTranslation() 10 | 11 | return ( 12 | <Block className="heyform-yes-no" field={field} locale={locale} {...restProps}> 13 | <div className="heyform-radio-group w-40"> 14 | <FakeRadio hotkey="Y" label={t('Yes', { lng: locale })} /> 15 | <FakeRadio hotkey="N" label={t('No', { lng: locale })} /> 16 | </div> 17 | </Block> 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/views/Compose/Blocks/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Address' 2 | export * from './Country' 3 | export * from './Date' 4 | export * from './DateRange' 5 | export * from './Email' 6 | export * from './FileUpload' 7 | export * from './FullName' 8 | export * from './InputTable' 9 | export * from './LegalTerms' 10 | export * from './LongText' 11 | export * from './MultipleChoice' 12 | export * from './Number' 13 | export * from './OpinionScale' 14 | export * from './PhoneNumber' 15 | export * from './PictureChoice' 16 | export * from './Rating' 17 | export * from './ShortText' 18 | export * from './Signature' 19 | export * from './Statement' 20 | export * from './ThankYou' 21 | export * from './Website' 22 | export * from './Welcome' 23 | export * from './YesNo' 24 | export * from './Payment' 25 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/views/Compose/FakeRadio.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | interface FakeRadioProps extends IComponentProps { 4 | hotkey?: string 5 | label: string | number 6 | } 7 | 8 | export const FakeRadio: FC<FakeRadioProps> = ({ hotkey, label, ...restProps }) => { 9 | return ( 10 | <div className="heyform-radio" {...restProps}> 11 | <div className="heyform-radio-container"> 12 | <div className="heyform-radio-content"> 13 | {hotkey && <div className="heyform-radio-hotkey">{hotkey}</div>} 14 | <div className="heyform-radio-label">{label}</div> 15 | </div> 16 | </div> 17 | </div> 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/views/Compose/FakeSelect.tsx: -------------------------------------------------------------------------------- 1 | import { IconChevronDown } from '@tabler/icons-react' 2 | import type { FC } from 'react' 3 | 4 | export const FakeSelect: FC<IComponentProps> = ({ ...restProps }) => { 5 | return ( 6 | <div className="heyform-select" {...restProps}> 7 | <div className="heyform-select-container"> 8 | <span className="heyform-select-value" /> 9 | <span className="heyform-select-arrow-icon"> 10 | <IconChevronDown /> 11 | </span> 12 | </div> 13 | </div> 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/views/Compose/FakeSubmit.tsx: -------------------------------------------------------------------------------- 1 | import type { FC, ReactNode } from 'react' 2 | 3 | interface FakeSubmitProps extends IComponentProps { 4 | text?: string 5 | icon?: ReactNode 6 | } 7 | 8 | export const FakeSubmit: FC<FakeSubmitProps> = ({ text, icon, ...restProps }) => { 9 | return ( 10 | <div className="heyform-submit-container" {...restProps}> 11 | <div className="heyform-submit-button"> 12 | <span>{text}</span> 13 | {icon} 14 | </div> 15 | </div> 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/views/Compose/FlagIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | interface FlagIconProps { 4 | countryCode?: string 5 | } 6 | 7 | export const FlagIcon: FC<FlagIconProps> = ({ countryCode = 'US' }) => { 8 | return <span className={`flag-icon flag-icon-${countryCode?.toLowerCase()}`} /> 9 | } 10 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/views/LeftSidebar/FieldKindIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import { questionNumber } from '@/pages/form/views/FormComponents' 4 | 5 | import type { FieldIconProps } from '../FieldIcon' 6 | import { FieldIcon } from '../FieldIcon' 7 | 8 | interface FieldKindIconProps extends FieldIconProps { 9 | parentIndex?: number 10 | } 11 | 12 | export const FieldKindIcon: FC<FieldKindIconProps> = ({ parentIndex, index, ...restProps }) => { 13 | return ( 14 | <FieldIcon 15 | className="field-card-icon" 16 | index={questionNumber(index, parentIndex)} 17 | iconOnly={false} 18 | {...restProps} 19 | /> 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/views/LogicFlow/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactFlowProvider } from 'react-flow-renderer' 2 | 3 | import { EdgeArrow } from '@/components' 4 | 5 | import { Flow } from './Flow' 6 | 7 | export const LogicFlow = () => { 8 | return ( 9 | <div className="logic-flow"> 10 | <EdgeArrow /> 11 | <ReactFlowProvider> 12 | <Flow /> 13 | </ReactFlowProvider> 14 | </div> 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/views/RightSidebar/Design/style.scss: -------------------------------------------------------------------------------- 1 | .theme-list { 2 | height: calc(100vh - 176px); 3 | } 4 | 5 | .customize-bottom { 6 | @apply mb-0 p-4 border-t border-gray-200; 7 | } 8 | 9 | .customize-list { 10 | height: calc(100vh - 176px - 74px); 11 | 12 | .right-sidebar-settings-item { 13 | @apply py-0; 14 | } 15 | } 16 | 17 | button.custom-css-help { 18 | @apply ml-2 p-1; 19 | } 20 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Create/views/RightSidebar/Logic/index.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import { Rules } from './Rules' 4 | import { Variables } from './Variables' 5 | 6 | export const Logic: FC = () => { 7 | return ( 8 | <div> 9 | <Variables /> 10 | <Rules /> 11 | </div> 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Render/utils/payment.ts: -------------------------------------------------------------------------------- 1 | import { FieldKindEnum, FormModel } from '@heyform-inc/shared-types-enums' 2 | import { helper } from '@heyform-inc/utils' 3 | 4 | export function isStripeEnabled(form: any): boolean { 5 | return helper.isValid(form.stripe?.accountId) && !!getPaymentField(form) 6 | } 7 | 8 | export function getPaymentField(form: FormModel) { 9 | return form.fields?.find(f => f.kind === FieldKindEnum.PAYMENT) 10 | } 11 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Submissions/views/ExportLink.tsx: -------------------------------------------------------------------------------- 1 | import { IconDownload } from '@tabler/icons-react' 2 | import { FC } from 'react' 3 | import { useTranslation } from 'react-i18next' 4 | 5 | import { Button } from '@/components/ui' 6 | import { useParam } from '@/utils' 7 | 8 | export const ExportLink: FC = () => { 9 | const { t } = useTranslation() 10 | const { formId } = useParam() 11 | 12 | function handleClick() { 13 | window.open(`/export/submissions?formId=${formId}`) 14 | } 15 | 16 | return ( 17 | <Button 18 | className="ml-5" 19 | leading={<IconDownload className="h-6 w-6 text-slate-500" />} 20 | onClick={handleClick} 21 | > 22 | <span className="ml-2">{t('submissions.export')}</span> 23 | </Button> 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Submissions/views/Sheet/SheetHeaderCell.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | 3 | import { SheetKindIcon } from './SheetKindIcon' 4 | import { SheetHeaderCellProps } from './types' 5 | 6 | export const SheetHeaderCell: FC<SheetHeaderCellProps> = ({ column }) => { 7 | return ( 8 | <div className="heygrid-header-cell flex items-center bg-white"> 9 | <SheetKindIcon className="mr-2 h-[22px] w-[22px] p-0.5" kind={column.kind!} /> 10 | <span className="h-full flex-1 truncate">{column.name}</span> 11 | </div> 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Submissions/views/Sheet/SheetKindIcon.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import { FC, useMemo } from 'react' 3 | 4 | import { CUSTOM_FIELDS_CONFIGS, FIELD_CONFIGS } from '@/pages/form/Create/views/FieldConfig' 5 | 6 | import { SheetKindIconProps } from './types' 7 | 8 | const configs = [...FIELD_CONFIGS, ...CUSTOM_FIELDS_CONFIGS] 9 | 10 | export const SheetKindIcon: FC<SheetKindIconProps> = ({ className, kind }) => { 11 | const config = useMemo(() => configs.find(c => c.kind === kind)!, [kind]) 12 | 13 | return ( 14 | <config.icon 15 | className={clsx('rounded', className)} 16 | style={{ 17 | backgroundColor: config?.backgroundColor, 18 | color: config?.textColor 19 | }} 20 | /> 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Submissions/views/Sheet/formatters/ValueFormatter.tsx: -------------------------------------------------------------------------------- 1 | import type { FormatterProps } from '../types' 2 | 3 | export function ValueFormatter<R, SR>(props: FormatterProps<R, SR>) { 4 | try { 5 | return <>{props.row[props.column.key as keyof R]}</> 6 | } catch { 7 | return null 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Submissions/views/Sheet/formatters/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../formatters/SelectCellFormatter' 2 | export * from '../formatters/ValueFormatter' 3 | export * from '../formatters/ToggleGroupFormatter' 4 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Submissions/views/Sheet/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useGridDimensions' 2 | export * from './useViewportColumns' 3 | export * from './useViewportRows' 4 | export * from './useFocusRef' 5 | export * from './useLatestFunc' 6 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Submissions/views/Sheet/hooks/useFocusRef.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useRef } from 'react' 2 | 3 | export function useFocusRef<T extends HTMLOrSVGElement>(isCellSelected: boolean | undefined) { 4 | const ref = useRef<T>(null) 5 | useLayoutEffect(() => { 6 | if (!isCellSelected) return 7 | ref.current?.focus({ preventScroll: true }) 8 | }, [isCellSelected]) 9 | 10 | return ref 11 | } 12 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Submissions/views/Sheet/hooks/useLatestFunc.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react' 2 | 3 | // https://reactjs.org/docs/hooks-faq.html#what-can-i-do-if-my-effect-dependencies-change-too-often 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | export function useLatestFunc<T extends (...args: any[]) => any>(fn: T) { 6 | const ref = useRef(fn) 7 | 8 | useEffect(() => { 9 | ref.current = fn 10 | }) 11 | 12 | return useCallback((...args: Parameters<T>) => { 13 | ref.current(...args) 14 | }, []) 15 | } 16 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Submissions/views/Sheet/sheetCells/AddressCell.tsx: -------------------------------------------------------------------------------- 1 | import { helper } from '@heyform-inc/utils' 2 | import { FC, useMemo } from 'react' 3 | 4 | import { SheetCellProps } from '../types' 5 | 6 | export const AddressCell: FC<SheetCellProps> = ({ column, row }) => { 7 | const value = useMemo(() => { 8 | const v = row[column.key] 9 | 10 | if (helper.isObject(v)) { 11 | return [v.address1, v.address2, v.city, v.state, v.zip].filter(Boolean).join(', ') 12 | } 13 | }, [column.key, row]) 14 | 15 | return ( 16 | <div className="heygrid-cell-text overflow-hidden text-ellipsis whitespace-nowrap">{value}</div> 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Submissions/views/Sheet/sheetCells/DateRangeCell.tsx: -------------------------------------------------------------------------------- 1 | import { helper } from '@heyform-inc/utils' 2 | import { FC, useMemo } from 'react' 3 | 4 | import { SheetCellProps } from '../types' 5 | 6 | export const DateRangeCell: FC<SheetCellProps> = ({ column, row }) => { 7 | const value = useMemo(() => { 8 | const v = row[column.key] 9 | 10 | if (helper.isObject(v)) { 11 | return [v?.start, v?.end].filter(Boolean).join(' - ') 12 | } 13 | }, [column.key, row]) 14 | 15 | return ( 16 | <div className="heygrid-cell-text overflow-hidden text-ellipsis whitespace-nowrap">{value}</div> 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Submissions/views/Sheet/sheetCells/DropPickerCell.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | 3 | import { TagGroup } from '@/components' 4 | 5 | import { SheetCellProps } from '../types' 6 | 7 | export const DropPickerCell: FC<SheetCellProps> = ({ column, row }) => { 8 | const value = row[column.key] 9 | const choice = column.properties?.choices?.find(choice => choice.id === value) 10 | 11 | return ( 12 | <> 13 | {choice && ( 14 | <div className="heygrid-cell-text flex items-center overflow-hidden text-ellipsis whitespace-nowrap"> 15 | <TagGroup className="px-4" tags={[choice]} /> 16 | </div> 17 | )} 18 | </> 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Submissions/views/Sheet/sheetCells/FullNameCell.tsx: -------------------------------------------------------------------------------- 1 | import { helper } from '@heyform-inc/utils' 2 | import { FC, useMemo } from 'react' 3 | 4 | import { SheetCellProps } from '../types' 5 | 6 | export const FullNameCell: FC<SheetCellProps> = ({ column, row }) => { 7 | const value = useMemo(() => { 8 | const v = row[column.key] 9 | 10 | if (helper.isObject(v)) { 11 | return [v.firstName, v.lastName].filter(Boolean).join(', ') 12 | } 13 | }, [column.key, row]) 14 | 15 | return ( 16 | <div className="heygrid-cell-text overflow-hidden text-ellipsis whitespace-nowrap">{value}</div> 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Submissions/views/Sheet/sheetCells/HiddenFieldCell.tsx: -------------------------------------------------------------------------------- 1 | import { helper } from '@heyform-inc/utils' 2 | import { FC, useMemo } from 'react' 3 | 4 | import { SheetCellProps } from '../types' 5 | 6 | export const HiddenFieldCell: FC<SheetCellProps> = ({ column, row }) => { 7 | const value = useMemo(() => { 8 | const v = row[column.name as string] 9 | 10 | if (helper.isString(v) || helper.isNumber(v)) { 11 | return v 12 | } 13 | }, [column.name, row]) 14 | 15 | return ( 16 | <div className="heygrid-cell-text overflow-hidden text-ellipsis whitespace-nowrap">{value}</div> 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Submissions/views/Sheet/sheetCells/OpinionScaleCell.tsx: -------------------------------------------------------------------------------- 1 | import { helper } from '@heyform-inc/utils' 2 | import { FC, useMemo } from 'react' 3 | 4 | import { SheetCellProps } from '../types' 5 | 6 | export const OpinionScaleCell: FC<SheetCellProps> = ({ column, row }) => { 7 | const value = useMemo(() => { 8 | const v = row[column.key] 9 | 10 | return [helper.isString(v) || helper.isNumber(v) ? v : null, column.properties?.total] 11 | .filter(helper.isValid) 12 | .join(' / ') 13 | }, [column.key, column.properties?.total, row]) 14 | 15 | return ( 16 | <div className="heygrid-cell-text overflow-hidden text-ellipsis whitespace-nowrap">{value}</div> 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Submissions/views/Sheet/sheetCells/SignatureCell.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useMemo } from 'react' 2 | 3 | import { getUrlValue } from '@/utils' 4 | 5 | import { SheetCellProps } from '../types' 6 | 7 | export const SignatureCell: FC<SheetCellProps> = ({ column, row }) => { 8 | const value = useMemo(() => getUrlValue(row[column.key]), [column.key, row]) 9 | 10 | return ( 11 | <div className="heygrid-cell-text flex h-10 items-center overflow-hidden px-4 leading-[1px]"> 12 | {value && <img className="h-5 w-11 object-cover" src={value} width={80} height={40} />} 13 | </div> 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Submissions/views/Sheet/sheetCells/SubmitDateCell.tsx: -------------------------------------------------------------------------------- 1 | import { unixDate } from '@heyform-inc/utils' 2 | import { FC } from 'react' 3 | 4 | import { SheetCellProps } from '../types' 5 | 6 | export const SubmitDateCell: FC<SheetCellProps> = ({ row }) => { 7 | const value: number = row.endAt ?? 0 8 | 9 | return ( 10 | <div className="heygrid-cell-text overflow-hidden text-ellipsis whitespace-nowrap"> 11 | {unixDate(value).format('MMM DD, YYYY')} 12 | </div> 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Submissions/views/Sheet/sheetCells/TextCell.tsx: -------------------------------------------------------------------------------- 1 | import { helper } from '@heyform-inc/utils' 2 | import { FC, useMemo } from 'react' 3 | 4 | import { SheetCellProps } from '../types' 5 | 6 | export const TextCell: FC<SheetCellProps> = ({ column, row }) => { 7 | const value = useMemo(() => { 8 | const v = row[column.key] 9 | 10 | if (helper.isString(v) || helper.isNumber(v)) { 11 | return v 12 | } 13 | }, [column.key, row]) 14 | 15 | return ( 16 | <div className="heygrid-cell-text overflow-hidden text-ellipsis whitespace-nowrap">{value}</div> 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Submissions/views/Sheet/sheetCells/UrlCell.tsx: -------------------------------------------------------------------------------- 1 | import { helper } from '@heyform-inc/utils' 2 | import { FC, useMemo } from 'react' 3 | 4 | import { getUrlValue } from '@/utils' 5 | 6 | import { SheetCellProps } from '../types' 7 | 8 | export const UrlCell: FC<SheetCellProps> = ({ column, row }) => { 9 | const value = useMemo(() => getUrlValue(row[column.key]), [column.key, row]) 10 | 11 | return ( 12 | <div className="heygrid-cell-text overflow-hidden text-ellipsis whitespace-nowrap"> 13 | <a href={value} target="_blank" rel="noreferrer"> 14 | {value} 15 | </a> 16 | </div> 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/Submissions/views/Sheet/utils/index.ts: -------------------------------------------------------------------------------- 1 | import type { CalculatedColumn } from '../types' 2 | 3 | export * from '../utils/keyboardUtils' 4 | export * from '../utils/selectedCellUtils' 5 | 6 | export function assertIsValidKeyGetter<R>( 7 | keyGetter: unknown 8 | ): asserts keyGetter is (row: R) => React.Key { 9 | if (typeof keyGetter !== 'function') { 10 | throw new Error('Please specify the rowKeyGetter prop to use selection') 11 | } 12 | } 13 | 14 | export function getCellStyle<R, SR>(column: CalculatedColumn<R, SR>): React.CSSProperties { 15 | return column.frozen 16 | ? { left: `var(--frozen-left-${column.key})` } 17 | : { gridColumnStart: column.idx + 1 } 18 | } 19 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/views/FormComponents/blocks/Statement.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import type { BlockProps } from './Block' 4 | import { Block } from './Block' 5 | import { Form } from './Form' 6 | 7 | export const Statement: FC<BlockProps> = ({ field, ...restProps }) => { 8 | return ( 9 | <Block className="heyform-statement heyform-empty-state" field={field} {...restProps}> 10 | <Form field={field} /> 11 | </Block> 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/views/FormComponents/blocks/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import { useStore } from '../store' 4 | import { Branding } from '../views/Branding' 5 | import type { BlockProps } from './Block' 6 | import { EmptyState } from './EmptyState' 7 | 8 | export const Welcome: FC<BlockProps> = ({ field, ...restProps }) => { 9 | const { dispatch } = useStore() 10 | 11 | function handleClick() { 12 | dispatch({ 13 | type: 'setIsStarted', 14 | payload: { 15 | isStarted: true 16 | } 17 | }) 18 | } 19 | 20 | return ( 21 | <> 22 | <EmptyState {...restProps} className="heyform-welcome" field={field} onClick={handleClick} /> 23 | <Branding /> 24 | </> 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/views/FormComponents/components/FlagIcon.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import type { FC } from 'react' 3 | 4 | import type { IComponentProps } from '@/components/ui/typing' 5 | 6 | export interface FlagIconProps extends IComponentProps { 7 | countryCode?: string 8 | } 9 | 10 | export const FlagIcon: FC<FlagIconProps> = ({ className, countryCode = 'US' }) => { 11 | return <span className={clsx(`fi fi-${countryCode?.toLowerCase()}`, className)} /> 12 | } 13 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/views/FormComponents/components/Icons/CollapseIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import { IComponentProps } from '@/components/ui/typing' 4 | 5 | export const CollapseIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => ( 6 | <svg width="8" height="6" viewBox="0 0 8 6" xmlns="http://www.w3.org/2000/svg" {...props}> 7 | <path d="M4 6l4-6H0z" fillRule="evenodd" fill="currentColor"></path> 8 | </svg> 9 | ) 10 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/views/FormComponents/components/Icons/StarIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | import { IComponentProps } from '@/components/ui/typing' 4 | 5 | export const StarIcon: FC<IComponentProps<HTMLOrSVGElement>> = props => { 6 | return ( 7 | <svg 8 | width="24" 9 | height="24" 10 | viewBox="0 0 24 24" 11 | fill="none" 12 | xmlns="http://www.w3.org/2000/svg" 13 | className="heyform-icon" 14 | {...props} 15 | > 16 | <path 17 | d="M11.9993 2.5L8.9428 8.7388L2 9.74555L7.02945 14.6625L5.8272 21.5L11.9993 18.2096L18.1727 21.5L16.9793 14.6625L22 9.74555L15.0956 8.7388L11.9993 2.5Z" 18 | className="heyform-icon-fill heyform-icon-stroke" 19 | strokeWidth="1" 20 | strokeLinejoin="round" 21 | /> 22 | </svg> 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/views/FormComponents/components/Icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CollapseIcon' 2 | export * from './CrownIcon' 3 | export * from './EmotionIcon' 4 | export * from './LikeIcon' 5 | export * from './LogoIcon' 6 | export * from './StarIcon' 7 | export * from './ThumbsUpIcon' 8 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/views/FormComponents/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CountrySelect' 2 | export * from './DateInput' 3 | export * from './DateRangeInput' 4 | export * from './FileUploader' 5 | export * from './FlagIcon' 6 | export * from './FormField' 7 | export * from './Icons' 8 | export * from './Input' 9 | export * from './Layout' 10 | export * from './PhoneNumberInput' 11 | export * from './Radio' 12 | export * from './RadioGroup' 13 | export * from './SelectHelper' 14 | export * from './SignaturePad' 15 | export * from './Slide' 16 | export * from './Submit' 17 | export * from './TableInput' 18 | export * from './TemporaryError' 19 | export * from './Textarea' 20 | export * from './ChoiceRadio' 21 | export * from './ChoiceRadioGroup' 22 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/views/FormComponents/consts/fileUpload.ts: -------------------------------------------------------------------------------- 1 | export const ACCEPTED_FILE_MIMES = [ 2 | 'image/jpeg', 3 | 'image/png', 4 | 'image/bmp', 5 | 'image/gif', 6 | 'text/plain', 7 | 'text/markdown', 8 | 'application/msword', 9 | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 10 | 'application/vnd.ms-excel', 11 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 12 | 'text/csv', 13 | 'application/vnd.ms-powerpoint', 14 | 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 15 | 'application/pdf', 16 | 'video/mp4', 17 | 'video/x-ms-wmv' 18 | ] 19 | 20 | export const MAX_FILE_SIZE = '10MB' 21 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/views/FormComponents/consts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './country' 2 | export * from './date' 3 | export * from './fileUpload' 4 | export * from './motion' 5 | export * from './payment' 6 | export * from './rating' 7 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/views/FormComponents/consts/motion.ts: -------------------------------------------------------------------------------- 1 | export const MOTION_UNMOUNTED_STATES = ['exited', 'unmounted'] 2 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/views/FormComponents/consts/payment.ts: -------------------------------------------------------------------------------- 1 | import { IMapType } from '@/components/ui/typing' 2 | 3 | export const CURRENCY_SYMBOLS: IMapType = { 4 | EUR: '\u20ac', 5 | GBP: '\xa3', 6 | USD: '#39;, 7 | AUD: 'A#39;, 8 | CAD: 'CA#39;, 9 | CHF: 'CHF ', 10 | NOK: 'NOK ', 11 | SEK: 'SEK ', 12 | DKK: 'DKK ', 13 | MXN: 'MX#39;, 14 | NZD: 'NZ#39;, 15 | BRL: 'R#39; 16 | } 17 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/views/FormComponents/consts/rating.tsx: -------------------------------------------------------------------------------- 1 | import { IMapType } from '@/components/ui/typing' 2 | 3 | import { CrownIcon, EmotionIcon, LikeIcon, StarIcon, ThumbsUpIcon } from '../components/Icons' 4 | 5 | export const RATING_SHAPE_ICONS: IMapType = { 6 | heart: <LikeIcon />, 7 | thumb_up: <ThumbsUpIcon />, 8 | happy: <EmotionIcon />, 9 | crown: <CrownIcon />, 10 | star: <StarIcon /> 11 | } 12 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/views/FormComponents/index.ts: -------------------------------------------------------------------------------- 1 | export * from './consts/payment' 2 | export { default as locales } from './locales' 3 | export * from './Renderer' 4 | export * from './theme' 5 | export * from './utils' 6 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/views/FormComponents/locales/index.ts: -------------------------------------------------------------------------------- 1 | import de from './de' 2 | import en from './en' 3 | import es from './es' 4 | import fr from './fr' 5 | import pl from './pl' 6 | import ptBr from './pt-br' 7 | import tr from './tr' 8 | import zhCn from './zh-cn' 9 | import zhTw from './zh-tw' 10 | import cs from './cs' 11 | 12 | export default { 13 | en: { 14 | translation: en 15 | }, 16 | fr: { 17 | translation: fr 18 | }, 19 | de: { 20 | translation: de 21 | }, 22 | es: { 23 | translation: es 24 | }, 25 | pl: { 26 | translation: pl 27 | }, 28 | 'pt-br': { 29 | translation: ptBr 30 | }, 31 | tr: { 32 | translation: tr 33 | }, 34 | 'zh-cn': { 35 | translation: zhCn 36 | }, 37 | 'zh-tw': { 38 | translation: zhTw 39 | }, 40 | cs: { 41 | translation: cs 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/views/FormComponents/typings.ts: -------------------------------------------------------------------------------- 1 | import { FieldKindEnum, FormField, FormModel } from '@heyform-inc/shared-types-enums' 2 | 3 | export interface IFormField extends FormField { 4 | parent?: IFormField 5 | isTouched?: boolean 6 | } 7 | 8 | export interface IFormModel extends FormModel { 9 | fields: IFormField[] 10 | } 11 | 12 | export interface IPartialFormField { 13 | id: string 14 | index: number 15 | title?: string | any[] 16 | kind: FieldKindEnum 17 | required?: boolean 18 | children?: IPartialFormField[] 19 | } 20 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/views/FormComponents/utils/hook.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from 'react' 2 | import { useTranslation as useReactTranslation } from 'react-i18next' 3 | 4 | import { IMapType } from '@/components/ui/typing' 5 | 6 | import { useStore } from '../store' 7 | 8 | export function useTranslation(overrideLng?: string) { 9 | const { t: _t, i18n } = useReactTranslation() 10 | const { state } = useStore() 11 | 12 | const t = useCallback((key: string, options?: IMapType) => { 13 | return _t(key, { 14 | ...options, 15 | lng: overrideLng || state.locale 16 | }) 17 | }, []) 18 | 19 | return { t, i18n } 20 | } 21 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/views/FormComponents/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './form' 2 | export * from './hook' 3 | export * from './lru' 4 | export * from './message' 5 | export * from './script' 6 | export { default as GlobalTimeout, Timeout } from './timeout' 7 | export * from './browser-language' 8 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/views/FormComponents/utils/message.ts: -------------------------------------------------------------------------------- 1 | export function sendHideModalMessage() { 2 | window.parent?.postMessage( 3 | { 4 | source: 'HEYFORM', 5 | eventName: 'HIDE_EMBED_MODAL' 6 | }, 7 | '*' 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/views/FormComponents/utils/timeout.ts: -------------------------------------------------------------------------------- 1 | import { IMapType } from '@/components/ui/typing' 2 | 3 | interface TimeoutProps { 4 | name: string 5 | duration: number 6 | callback: () => void 7 | } 8 | 9 | export class Timeout { 10 | private readonly caches: IMapType<number> = {} 11 | 12 | add({ name, duration, callback }: TimeoutProps) { 13 | this.caches[name] = setTimeout(callback, duration) 14 | } 15 | 16 | remove(name: string) { 17 | const cache = this.caches[name] 18 | 19 | if (cache) { 20 | clearTimeout(cache) 21 | delete this.caches[name] 22 | } 23 | } 24 | 25 | clear() { 26 | Object.keys(this.caches).forEach(key => { 27 | this.remove(key) 28 | }) 29 | } 30 | } 31 | 32 | export default new Timeout() 33 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/form/views/FormComponents/views/Branding.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | 3 | import { LogoIcon } from '../components' 4 | import { useTranslation } from '../utils' 5 | 6 | export const Branding: FC = () => { 7 | const { t } = useTranslation() 8 | 9 | return ( 10 | <a className="heyform-branding" href="https://heyform.net/?ref=badge" target="_blank"> 11 | <LogoIcon /> {t('Made with')} HeyForm 12 | </a> 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/project/Project/index.scss: -------------------------------------------------------------------------------- 1 | .forms { 2 | tbody { 3 | tr { 4 | @apply cursor-pointer; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/project/views/index.scss: -------------------------------------------------------------------------------- 1 | .form-status { 2 | @apply text-sm text-slate-500 font-medium p-0 bg-transparent; 3 | 4 | &.badge-red { 5 | .badge-dot { 6 | @apply text-red-600; 7 | } 8 | } 9 | 10 | &.badge-green { 11 | .badge-dot { 12 | @apply text-green-500; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/webapp/src/service/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.service' 2 | export * from './form.service' 3 | export * from './project.service' 4 | export * from './unsplash.service' 5 | export * from './user.service' 6 | export * from './workspace.service' 7 | export * from './app.service' 8 | export * from './submission.service' 9 | export * from './template.service' 10 | export * from './integration.service' 11 | export * from './payment.service' 12 | -------------------------------------------------------------------------------- /packages/webapp/src/service/unsplash.service.ts: -------------------------------------------------------------------------------- 1 | import { UNSPLASH_SEARCH_GQL, UNSPLASH_TRACK_DOWNLOAD_GQL } from '@/consts' 2 | import { request } from '@/utils' 3 | 4 | export class UnsplashService { 5 | static async search(keyword?: string) { 6 | return request.query({ 7 | query: UNSPLASH_SEARCH_GQL, 8 | variables: { 9 | input: { 10 | keyword 11 | } 12 | }, 13 | fetchPolicy: 'network-only' 14 | }) 15 | } 16 | 17 | static trackDownload(downloadUrl: string) { 18 | return request.mutate({ 19 | mutation: UNSPLASH_TRACK_DOWNLOAD_GQL, 20 | variables: { 21 | input: { 22 | downloadUrl 23 | } 24 | } 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/webapp/src/store/app.store.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx' 2 | 3 | export class AppStore { 4 | // Email address of user who wants to reset password 5 | resetPasswordEmail = '' 6 | 7 | // Sidebar is open or not 8 | isSidebarOpen = false 9 | 10 | // Plan modal is open or not 11 | isPlanModalOpen = false 12 | 13 | // Open create from modal 14 | isCreateFormOpen = false 15 | 16 | // Form preview is open or not 17 | isFormPreviewOpen = false 18 | 19 | // Form share modal is open or not 20 | isFormShareModalOpen = false 21 | 22 | // Multi-language modal is open or not 23 | isMultiLanguageModalOpen = false 24 | 25 | // User settings is open or not 26 | isUserSettingsOpen = false 27 | 28 | constructor() { 29 | makeAutoObservable(this) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/webapp/src/store/mobxStorage.ts: -------------------------------------------------------------------------------- 1 | import { helper, pickValidValues } from '@heyform-inc/utils' 2 | import { autorun, set, toJS } from 'mobx' 3 | import store2 from 'store2' 4 | 5 | export function mobxStorage(storeInstance: any, storeName: string, fields?: string[]) { 6 | const cache = store2.get(storeName) 7 | 8 | if (helper.isValid(cache)) { 9 | set(storeInstance, cache) 10 | } 11 | 12 | autorun(() => { 13 | let value = toJS(storeInstance) 14 | 15 | if (helper.isValid(fields)) { 16 | value = pickValidValues(value, fields!) 17 | } 18 | 19 | store2.set(storeName, value) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /packages/webapp/src/store/user.store.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx' 2 | 3 | import { UserModel } from '@/models' 4 | 5 | import { mobxStorage } from './mobxStorage' 6 | 7 | export class UserStore { 8 | user = {} as UserModel 9 | 10 | constructor() { 11 | makeAutoObservable(this) 12 | mobxStorage(this, 'UserStore') 13 | } 14 | 15 | setUser(user: UserModel) { 16 | this.user = user 17 | } 18 | 19 | update(updates: Partial<UserModel>) { 20 | Object.assign(this.user, updates) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/webapp/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth' 2 | export * from './hook' 3 | export * from './request' 4 | export * from './helper' 5 | -------------------------------------------------------------------------------- /packages/webapp/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme') 2 | 3 | module.exports = { 4 | content: ['./index.html', './src/**/*.{js,jsx,ts,tsx}'], 5 | theme: { 6 | fontFamily: { 7 | sans: ['Inter', ...defaultTheme.fontFamily.sans], 8 | }, 9 | extend: { 10 | } 11 | }, 12 | variants: { 13 | extend: {} 14 | }, 15 | plugins: [require('@tailwindcss/forms')] 16 | } 17 | -------------------------------------------------------------------------------- /packages/webapp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | }, 7 | "target": "ESNext", 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "lib": ["dom", "dom.iterable", "esnext"], 11 | "types": ["vite/client"], 12 | "allowJs": false, 13 | "skipLibCheck": true, 14 | "esModuleInterop": true, 15 | "allowSyntheticDefaultImports": true, 16 | "strict": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": true, 22 | "jsx": "react-jsx" 23 | }, 24 | "include": ["src"] 25 | } 26 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/server' 3 | - 'packages/webapp' 4 | --------------------------------------------------------------------------------