mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 18:30:32 -06:00
* add changesets * add react-app to monorepo
This commit is contained in:
8
.changeset/README.md
Normal file
8
.changeset/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
11
.changeset/config.json
Normal file
11
.changeset/config.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
5
.changeset/twelve-carpets-eat.md
Normal file
5
.changeset/twelve-carpets-eat.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@snoopforms/react": patch
|
||||
---
|
||||
|
||||
Move react library to monorepo
|
||||
17
.env.docker
17
.env.docker
@@ -11,9 +11,12 @@ NEXTAUTH_SECRET=RANDOM_STRING
|
||||
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
<<<<<<< HEAD
|
||||
# This should always be localhost:3000 (or whatever port your app is running on)
|
||||
NEXTAUTH_URL_INTERNAL=http://localhost:3000
|
||||
|
||||
=======
|
||||
>>>>>>> 7ab8b81 (add basic react package)
|
||||
DATABASE_URL='postgresql://postgres:postgres@postgres:5432/snoopforms?schema=public'
|
||||
|
||||
################
|
||||
@@ -42,6 +45,7 @@ DATABASE_URL='postgresql://postgres:postgres@postgres:5432/snoopforms?schema=pub
|
||||
#####################
|
||||
|
||||
# Email Verification. If you enable Email Verification you have to setup SMTP-Settings, too.
|
||||
<<<<<<< HEAD
|
||||
NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1
|
||||
|
||||
# Password Reset. If you enable Password Reset functionality you have to setup SMTP-Settings, too.
|
||||
@@ -49,15 +53,28 @@ NEXT_PUBLIC_PASSWORD_RESET_DISABLED=1
|
||||
|
||||
# Signup. Disable the ability for new users to create an account.
|
||||
# NEXT_PUBLIC_SIGNUP_DISABLED=1
|
||||
=======
|
||||
EMAIL_VERIFICATION_DISABLED=1
|
||||
|
||||
# Password Reset. If you enable Password Reset functionality you have to setup SMTP-Settings, too.
|
||||
PASSWORD_RESET_DISABLED=1
|
||||
>>>>>>> 7ab8b81 (add basic react package)
|
||||
|
||||
#######################
|
||||
# Additional Options #
|
||||
#######################
|
||||
|
||||
<<<<<<< HEAD
|
||||
# NEXT_PUBLIC_TERMS_URL=https://www.example.com/terms
|
||||
# NEXT_PUBLIC_PRIVACY_URL=https://www.example.com/privacy
|
||||
# NEXT_PUBLIC_IMPRINT_URL=https://www.example.com/imprint
|
||||
# NEXT_PUBLIC_PRIVACY_URL=https://www.example.com/enduserPrivacy
|
||||
=======
|
||||
# TERMS_URL=https://www.example.com/terms
|
||||
# PRIVACY_URL=https://www.example.com/privacy
|
||||
# PUBLIC_IMPRINT_URL=https://www.example.com/imprint
|
||||
# PUBLIC_PRIVACY_URL=https://www.example.com/enduserPrivacy
|
||||
>>>>>>> 7ab8b81 (add basic react package)
|
||||
|
||||
######################
|
||||
# Posthog Tracking #
|
||||
|
||||
@@ -12,9 +12,12 @@ NEXTAUTH_SECRET=RANDOM_STRING
|
||||
# Set this to your public-facing URL, e.g., https://example.com
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
<<<<<<< HEAD
|
||||
# This should always be localhost:3000 (or whatever port your app is running on)
|
||||
NEXTAUTH_URL_INTERNAL=http://localhost:3000
|
||||
|
||||
=======
|
||||
>>>>>>> 7ab8b81 (add basic react package)
|
||||
DATABASE_URL='postgresql://postgres:postgres@localhost:5432/snoopforms?schema=public'
|
||||
# For Docker Compose Production Setup use this Database URL:
|
||||
# DATABASE_URL='postgresql://postgres:postgres@postgres:5432/snoopforms?schema=public'
|
||||
|
||||
40
.github/workflows/release.yml
vendored
Normal file
40
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node.js 16.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.2.2
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
# This expects you to have a script called release which does a build for your packages and calls changeset publish
|
||||
publish: pnpm release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -126,7 +126,12 @@ git clone https://github.com/formbricks/snoopforms.git && cd snoopforms
|
||||
|
||||
```
|
||||
|
||||
<<<<<<< HEAD
|
||||
Create a `.env` file based on `.env.docker` and change all fields according to your setup. This file comes with a basic setup and snoopForms works without making any changes to the file. To enable email sending functionality you need to configure the SMTP settings in the `.env` file. If you configured your email credentials, you can also comment the following lines to enable email verification (`# NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1`) and password reset (`# NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1`).
|
||||
=======
|
||||
Create a `.env` file based on `.env.docker` and change all fields according to your setup. This file comes with a basic setup and snoopForms works without making any changes to the file. To enable email sending functionality you need to configure the SMTP settings in the `.env` file. If you configured your email credentials, you can also comment the following lines to enable email verification (`# EMAIL_VERIFICATION_DISABLED=1`) and password reset (`# PASSWORD_RESET_DISABLED=1`).
|
||||
|
||||
> > > > > > > 7ab8b81 (add basic react package)
|
||||
|
||||
Copy the `.env.docker` file to `.env` and edit it with an editor of your choice if needed.
|
||||
|
||||
|
||||
@@ -8,13 +8,16 @@
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"overrides": {
|
||||
"@types/react": "^18.0.21"
|
||||
},
|
||||
"dependencies": {
|
||||
"@editorjs/editorjs": "^2.25.0",
|
||||
"@editorjs/header": "^2.6.2",
|
||||
"@editorjs/paragraph": "^2.8.0",
|
||||
"@headlessui/react": "^1.7.3",
|
||||
"@heroicons/react": "^2.0.12",
|
||||
"@snoopforms/react": "^0.3.5",
|
||||
"@snoopforms/react": "workspace:*",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"chart.js": "^3.9.1",
|
||||
"crypto": "^1.0.1",
|
||||
@@ -25,13 +28,13 @@
|
||||
"json2csv": "^5.0.7",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"next": "12.3.1",
|
||||
"next-auth": "^4.13.0",
|
||||
"next-auth": "^4.14.0",
|
||||
"nextjs-cors": "^2.1.1",
|
||||
"nodemailer": "^6.8.0",
|
||||
"react": "18.2.0",
|
||||
"react-chartjs-2": "^4.3.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-icons": "^4.6.0",
|
||||
"react-loader-spinner": "^5.3.4",
|
||||
"react-toastify": "^9.0.8",
|
||||
"sanitize-html": "^2.7.2",
|
||||
@@ -45,13 +48,13 @@
|
||||
"@tailwindcss/line-clamp": "^0.4.2",
|
||||
"@tailwindcss/typography": "^0.5.7",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/node": "18.8.3",
|
||||
"@types/react": "^17.0.37",
|
||||
"@types/node": "18.11.0",
|
||||
"@types/react": "^18.0.21",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"database": "workspace:*",
|
||||
"eslint": "8.25.0",
|
||||
"eslint-config-custom": "workspace:*",
|
||||
"postcss": "^8.4.17",
|
||||
"postcss": "^8.4.18",
|
||||
"tailwind-config": "workspace:*",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"ts-node": "^10.9.1",
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
"dev": "turbo run dev --parallel",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||
"generate": "turbo run generate",
|
||||
"lint": "turbo run lint"
|
||||
"lint": "turbo run lint",
|
||||
"release": "turbo run build --filter=web^... && changeset publish"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.22.0",
|
||||
"prettier": "latest",
|
||||
"tsx": "^3.7.1",
|
||||
"turbo": "latest"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "database",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "eslint-config-custom",
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "prettier-config",
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
|
||||
7
packages/snoopforms-react/.gitignore
vendored
Normal file
7
packages/snoopforms-react/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
*.log
|
||||
.DS_Store
|
||||
node_modules
|
||||
.cache
|
||||
dist
|
||||
.parcel-cache
|
||||
.idea/
|
||||
21
packages/snoopforms-react/LICENSE
Normal file
21
packages/snoopforms-react/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Matthias Nannt
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
63
packages/snoopforms-react/README.md
Normal file
63
packages/snoopforms-react/README.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# snoopForms React Library
|
||||
|
||||
React Library with form- & survey-elements for the snoopForms platform
|
||||
|
||||
[](https://www.npmjs.com/package/@snoopforms/react) [](https://github.com/snoopforms/react/releases) [](https://github.com/snoopForms/snoopforms-react/actions) [](https://github.com/snoopForms/snoopforms-react/blob/main/LICENSE)
|
||||
|
||||
<br/>
|
||||
|
||||
> :warning: **Note**: This repository is still in an early stage of development. We love the open source community and want to show what we are working on early. We will update this readme with more information once it is safe to use. Until then, feel free to share your thoughts, contact us, and contribute if you'd like.
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
npm install @snoopforms/react
|
||||
```
|
||||
|
||||
## How to use it 🤓
|
||||
|
||||
Use the SnoopForm components to build your form easily.
|
||||
|
||||
- `SnoopForm:` Use the `SnoopForm` wrapper to make the connection to the SnoopForm Data-Platform.
|
||||
- `SnoopPage:` Use `SnoopPage` to tell the Form where you need an new page. The SnoopForms library will only show the current page to the user. That way you can build long, more complex forms or a Typeform-like form-view, where the page changes after every question.
|
||||
- `SnoopElement:` You can choose your `SnoopElement` from a wide range of pre-coded components, including text, email, checkboxes, radio-buttons, and many more.
|
||||
|
||||
## Example
|
||||
|
||||
```jsx
|
||||
<SnoopForm formId="abcd">
|
||||
<SnoopPage name="basicInfo">
|
||||
<SnoopElement
|
||||
type="text"
|
||||
name="name"
|
||||
label="Your name"
|
||||
help="Please use your real name"
|
||||
required
|
||||
/>
|
||||
<SnoopElement
|
||||
type="textarea"
|
||||
name="about"
|
||||
label="About you"
|
||||
help="e.g. your hobbies etc."
|
||||
required
|
||||
/>
|
||||
<SnoopElement name="submit" type="submit" label="Submit" />
|
||||
</SnoopPage>
|
||||
<SnoopPage name="advancedInfo">
|
||||
<SnoopElement
|
||||
type="checkbox"
|
||||
name="programming-lanuguages"
|
||||
label="What programming languages do you love?"
|
||||
options={['C++', 'Javascript', 'Scala', 'Assembler']}
|
||||
/>
|
||||
<SnoopElement name="submit" type="submit" label="Submit" />
|
||||
</SnoopPage>
|
||||
<SnoopPage name="thankyou" thankyou>
|
||||
<p>Thanks a lot for your time and insights 🙏</p>
|
||||
</SnoopPage>
|
||||
</SnoopForm>
|
||||
```
|
||||
|
||||
## Contribute 🙏
|
||||
|
||||
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
||||
89
packages/snoopforms-react/package.json
Normal file
89
packages/snoopforms-react/package.json
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"version": "0.3.5",
|
||||
"name": "@snoopforms/react",
|
||||
"author": "snoopForms <hola@snoopforms.com>",
|
||||
"description": "React library with form- & survey-elements for the snoopForms platform",
|
||||
"homepage": "https://snoopforms.com",
|
||||
"keywords": [
|
||||
"react",
|
||||
"forms",
|
||||
"snoop",
|
||||
"snoopforms",
|
||||
"checkbox",
|
||||
"survey"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format esm,cjs --dts --external react && tailwindcss -i ./src/styles.css -o ./dist/styles.css",
|
||||
"dev": "concurrently \"tsup src/index.ts --format esm,cjs --dts --external react --watch\" \"tailwindcss -i ./src/styles.css -o ./dist/styles.css --watch\"",
|
||||
"clean": "rm -rf dist",
|
||||
"size": "size-limit",
|
||||
"analyze": "size-limit --why",
|
||||
"build-tailwind": "cross-env NODE_ENV=production npx tailwindcss -i ./tailwind.css -o ./dist/styles.css --minify"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "tsdx lint"
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 80,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5"
|
||||
},
|
||||
"module": "dist/react.esm.js",
|
||||
"size-limit": [
|
||||
{
|
||||
"path": "dist/react.cjs.production.min.js",
|
||||
"limit": "10 KB"
|
||||
},
|
||||
{
|
||||
"path": "dist/react.esm.js",
|
||||
"limit": "10 KB"
|
||||
}
|
||||
],
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.3",
|
||||
"@size-limit/preset-small-lib": "^8.1.0",
|
||||
"@storybook/addon-essentials": "^6.5.12",
|
||||
"@storybook/addon-info": "^5.3.21",
|
||||
"@storybook/addon-links": "^6.5.12",
|
||||
"@storybook/addon-postcss": "^2.0.0",
|
||||
"@storybook/addons": "^6.5.12",
|
||||
"@storybook/react": "^6.5.12",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"babel-loader": "^8.2.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint-config-custom": "workspace:*",
|
||||
"husky": "^8.0.1",
|
||||
"postcss": "^8.4.18",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-is": "^18.2.0",
|
||||
"size-limit": "^8.1.0",
|
||||
"tailwind-config": "workspace:*",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"tslib": "^2.4.0",
|
||||
"tsconfig": "workspace:*",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.3"
|
||||
}
|
||||
}
|
||||
7
packages/snoopforms-react/postcss.config.js
Normal file
7
packages/snoopforms-react/postcss.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}', './stories/*'],
|
||||
},
|
||||
},
|
||||
};
|
||||
126
packages/snoopforms-react/src/components/Elements/Cards.tsx
Normal file
126
packages/snoopforms-react/src/components/Elements/Cards.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { RadioGroup } from '@headlessui/react';
|
||||
import React, { FC, useContext, useEffect, useState } from 'react';
|
||||
import { getOptionValue, setSubmissionValue } from '../../lib/elements';
|
||||
import { classNamesConcat } from '../../lib/utils';
|
||||
import { ClassNames, Option } from '../../types';
|
||||
import {
|
||||
SubmissionContext,
|
||||
SubmitHandlerContext,
|
||||
} from '../SnoopForm/SnoopForm';
|
||||
import { PageContext } from '../SnoopPage/SnoopPage';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
label?: string;
|
||||
help?: string;
|
||||
cols?: number;
|
||||
autoSubmit?: boolean;
|
||||
options: (Option | string)[];
|
||||
placeholder?: string;
|
||||
classNames: ClassNames;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export const Cards: FC<Props> = ({
|
||||
name,
|
||||
label,
|
||||
help,
|
||||
cols,
|
||||
autoSubmit,
|
||||
options,
|
||||
classNames,
|
||||
}) => {
|
||||
const { submission, setSubmission }: any = useContext(SubmissionContext);
|
||||
const handleSubmit = useContext(SubmitHandlerContext);
|
||||
const pageName = useContext(PageContext);
|
||||
const [triggerSubmit, setTriggerSubmit] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerSubmit) {
|
||||
handleSubmit(pageName);
|
||||
setTriggerSubmit(false);
|
||||
}
|
||||
}, [triggerSubmit]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{label && (
|
||||
<label
|
||||
className={
|
||||
classNames.label || 'block text-sm font-medium text-gray-700'
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<RadioGroup
|
||||
value={submission[pageName] ? submission[pageName][name] : undefined}
|
||||
onChange={(v: string) => {
|
||||
setSubmissionValue(getOptionValue(v), pageName, name, setSubmission);
|
||||
if (autoSubmit) {
|
||||
// trigger submit at next rerender to await setSubmissionValue()
|
||||
setTriggerSubmit(true);
|
||||
}
|
||||
}}
|
||||
className="mt-2"
|
||||
>
|
||||
<RadioGroup.Label className="sr-only">
|
||||
Choose an option
|
||||
</RadioGroup.Label>
|
||||
<div
|
||||
className={classNamesConcat(
|
||||
'grid gap-3',
|
||||
(cols && cols === 1) || options.length === 1
|
||||
? 'grid-cols-1'
|
||||
: (cols && cols === 2) || options.length === 2
|
||||
? 'grid-cols-2'
|
||||
: (cols && cols === 3) || options.length === 3
|
||||
? 'grid-cols-3'
|
||||
: (cols && cols === 4) || options.length === 4
|
||||
? 'grid-cols-4'
|
||||
: (cols && cols === 5) || options.length === 5
|
||||
? 'grid-cols-5'
|
||||
: (cols && cols === 6) || options.length === 6
|
||||
? 'grid-cols-6'
|
||||
: (cols && cols === 7) || options.length === 7
|
||||
? 'grid-cols-7'
|
||||
: (cols && cols === 8) || options.length === 8
|
||||
? 'grid-cols-8'
|
||||
: (cols && cols === 9) || options.length === 9
|
||||
? 'grid-cols-9'
|
||||
: cols === 10
|
||||
? 'grid-cols-10'
|
||||
: 'grid-cols-1 sm:grid-cols-6'
|
||||
)}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<RadioGroup.Option
|
||||
key={getOptionValue(option)}
|
||||
id={`${name}-${getOptionValue(option)}`}
|
||||
value={option}
|
||||
className={({ active, checked }) =>
|
||||
classNamesConcat(
|
||||
'cursor-pointer focus:outline-none',
|
||||
active ? 'ring-2 ring-offset-2 ring-gray-500' : '',
|
||||
checked
|
||||
? 'bg-gray-600 border-transparent text-white hover:bg-gray-700'
|
||||
: 'bg-white border-gray-200 text-gray-900 hover:bg-gray-50',
|
||||
'border rounded-md py-3 px-3 flex items-center justify-center text-sm font-medium uppercase sm:flex-1'
|
||||
)
|
||||
}
|
||||
>
|
||||
<RadioGroup.Label as="span">
|
||||
{getOptionValue(option)}
|
||||
</RadioGroup.Label>
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
{help && (
|
||||
<p className={classNames.help || 'mt-2 text-sm text-gray-500'}>
|
||||
{help}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
105
packages/snoopforms-react/src/components/Elements/Checkbox.tsx
Normal file
105
packages/snoopforms-react/src/components/Elements/Checkbox.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React, { FC, useContext, useEffect, useState } from 'react';
|
||||
import { setSubmissionValue } from '../../lib/elements';
|
||||
import { ClassNames } from '../../types';
|
||||
import { SubmissionContext } from '../SnoopForm/SnoopForm';
|
||||
import { PageContext } from '../SnoopPage/SnoopPage';
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
label?: string;
|
||||
help?: string;
|
||||
options: (Option | string)[];
|
||||
placeholder?: string;
|
||||
classNames: ClassNames;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export const Checkbox: FC<Props> = ({
|
||||
name,
|
||||
label,
|
||||
help,
|
||||
options,
|
||||
classNames,
|
||||
}) => {
|
||||
const [checked, setChecked] = useState<string[]>([]);
|
||||
const { setSubmission }: any = useContext(SubmissionContext);
|
||||
const pageName = useContext(PageContext);
|
||||
|
||||
useEffect(() => {
|
||||
setSubmissionValue(checked, pageName, name, setSubmission);
|
||||
}, [checked]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{label && (
|
||||
<label
|
||||
className={
|
||||
classNames.label || 'block text-sm font-medium text-gray-700'
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div className="mt-2 space-y-2">
|
||||
{options.map((option) => (
|
||||
<div
|
||||
className="relative flex items-start"
|
||||
key={typeof option === 'object' ? option.value : option}
|
||||
>
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
id={typeof option === 'object' ? option.value : option}
|
||||
name={typeof option === 'object' ? option.value : option}
|
||||
type="checkbox"
|
||||
className={
|
||||
classNames.element ||
|
||||
'focus:ring-slate-500 h-4 w-4 text-slate-600 border-gray-300 rounded-sm'
|
||||
}
|
||||
checked={
|
||||
typeof option === 'object'
|
||||
? checked.includes(option.value)
|
||||
: checked.includes(option)
|
||||
}
|
||||
onChange={(e) => {
|
||||
const newChecked: string[] = [...checked];
|
||||
const value =
|
||||
typeof option === 'object' ? option.value : option;
|
||||
if (e.target.checked) {
|
||||
newChecked.push(value);
|
||||
} else {
|
||||
const idx = newChecked.findIndex((v) => v === value);
|
||||
if (idx >= 0) {
|
||||
newChecked.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
setChecked(newChecked);
|
||||
setSubmissionValue(newChecked, pageName, name, setSubmission);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 text-base">
|
||||
<label
|
||||
htmlFor={typeof option === 'object' ? option.value : option}
|
||||
className={
|
||||
classNames.elementLabel || 'font-medium text-gray-700'
|
||||
}
|
||||
>
|
||||
{typeof option === 'object' ? option.label : option}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{help && (
|
||||
<p className={classNames.help || 'mt-2 text-sm text-gray-500'}>
|
||||
{help}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
76
packages/snoopforms-react/src/components/Elements/Email.tsx
Normal file
76
packages/snoopforms-react/src/components/Elements/Email.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React, { FC, useContext } from 'react';
|
||||
import { setSubmissionValue } from '../../lib/elements';
|
||||
import { classNamesConcat } from '../../lib/utils';
|
||||
import { ClassNames } from '../../types';
|
||||
import { SubmissionContext } from '../SnoopForm/SnoopForm';
|
||||
import { PageContext } from '../SnoopPage/SnoopPage';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
label?: string;
|
||||
help?: string;
|
||||
Icon?: React.ReactNode;
|
||||
placeholder?: string;
|
||||
classNames: ClassNames;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
export const Email: FC<Props> = ({
|
||||
name,
|
||||
label,
|
||||
help,
|
||||
Icon,
|
||||
classNames,
|
||||
placeholder,
|
||||
required,
|
||||
}) => {
|
||||
const { setSubmission } = useContext(SubmissionContext);
|
||||
const pageName = useContext(PageContext);
|
||||
return (
|
||||
<div>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={name}
|
||||
className={
|
||||
classNames.label || 'block text-sm font-medium text-gray-700'
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div className="relative mt-1 rounded-md shadow-sm">
|
||||
{Icon && (
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<div className="w-5 h-5 text-gray-400 ">{Icon}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="email"
|
||||
pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
|
||||
onInvalid={(e: any) =>
|
||||
e.target.setCustomValidity('please enter a valid email address')
|
||||
}
|
||||
onInput={(e: any) => e.target.setCustomValidity('')}
|
||||
name={name}
|
||||
id={`input-${name}`}
|
||||
className={classNamesConcat(
|
||||
Icon ? 'pl-10' : '',
|
||||
classNames.element ||
|
||||
'block w-full border-gray-300 rounded-md focus:ring-slate-500 focus:border-slate-500 sm:text-sm'
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) =>
|
||||
setSubmissionValue(e.target.value, pageName, name, setSubmission)
|
||||
}
|
||||
required={required}
|
||||
/>
|
||||
</div>
|
||||
{help && (
|
||||
<p className={classNames.help || 'mt-2 text-sm text-gray-500'}>
|
||||
{help}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
70
packages/snoopforms-react/src/components/Elements/Number.tsx
Normal file
70
packages/snoopforms-react/src/components/Elements/Number.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React, { FC, useContext } from 'react';
|
||||
import { setSubmissionValue } from '../../lib/elements';
|
||||
import { classNamesConcat } from '../../lib/utils';
|
||||
import { ClassNames } from '../../types';
|
||||
import { SubmissionContext } from '../SnoopForm/SnoopForm';
|
||||
import { PageContext } from '../SnoopPage/SnoopPage';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
label?: string;
|
||||
help?: string;
|
||||
Icon?: React.ReactNode;
|
||||
placeholder?: string;
|
||||
classNames: ClassNames;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
export const Number: FC<Props> = ({
|
||||
name,
|
||||
label,
|
||||
help,
|
||||
Icon,
|
||||
classNames,
|
||||
placeholder,
|
||||
required,
|
||||
}) => {
|
||||
const { setSubmission } = useContext(SubmissionContext);
|
||||
const pageName = useContext(PageContext);
|
||||
return (
|
||||
<div>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={name}
|
||||
className={
|
||||
classNames.label || 'block text-sm font-medium text-gray-700'
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div className="relative mt-1 rounded-md shadow-sm">
|
||||
{Icon && (
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<div className="w-5 h-5 text-gray-400 ">{Icon}</div>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
type="number"
|
||||
name={name}
|
||||
id={`input-${name}`}
|
||||
className={classNamesConcat(
|
||||
Icon ? 'pl-10' : '',
|
||||
classNames.element ||
|
||||
'block w-full border-gray-300 rounded-md focus:ring-slate-500 focus:border-slate-500 sm:text-sm'
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) =>
|
||||
setSubmissionValue(e.target.value, pageName, name, setSubmission)
|
||||
}
|
||||
required={required}
|
||||
/>
|
||||
</div>
|
||||
{help && (
|
||||
<p className={classNames.help || 'mt-2 text-sm text-gray-500'}>
|
||||
{help}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
71
packages/snoopforms-react/src/components/Elements/Phone.tsx
Normal file
71
packages/snoopforms-react/src/components/Elements/Phone.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React, { FC, useContext } from 'react';
|
||||
import { setSubmissionValue } from '../../lib/elements';
|
||||
import { classNamesConcat } from '../../lib/utils';
|
||||
import { ClassNames } from '../../types';
|
||||
import { SubmissionContext } from '../SnoopForm/SnoopForm';
|
||||
import { PageContext } from '../SnoopPage/SnoopPage';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
label?: string;
|
||||
help?: string;
|
||||
Icon?: React.ReactNode;
|
||||
placeholder?: string;
|
||||
classNames: ClassNames;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
export const Phone: FC<Props> = ({
|
||||
name,
|
||||
label,
|
||||
help,
|
||||
Icon,
|
||||
classNames,
|
||||
placeholder,
|
||||
required,
|
||||
}) => {
|
||||
const { setSubmission } = useContext(SubmissionContext);
|
||||
const pageName = useContext(PageContext);
|
||||
return (
|
||||
<div>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={name}
|
||||
className={
|
||||
classNames.label || 'block text-sm font-medium text-gray-700'
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div className="relative mt-1 rounded-md shadow-sm">
|
||||
{Icon && (
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<div className="w-5 h-5 text-gray-400 ">{Icon}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="tel"
|
||||
name={name}
|
||||
id={`input-${name}`}
|
||||
className={classNamesConcat(
|
||||
Icon ? 'pl-10' : '',
|
||||
classNames.element ||
|
||||
'block w-full border-gray-300 rounded-md focus:ring-slate-500 focus:border-slate-500 sm:text-sm'
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) =>
|
||||
setSubmissionValue(e.target.value, pageName, name, setSubmission)
|
||||
}
|
||||
required={required}
|
||||
/>
|
||||
</div>
|
||||
{help && (
|
||||
<p className={classNames.help || 'mt-2 text-sm text-gray-500'}>
|
||||
{help}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
82
packages/snoopforms-react/src/components/Elements/Radio.tsx
Normal file
82
packages/snoopforms-react/src/components/Elements/Radio.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { FC, useContext } from 'react';
|
||||
import { getOptionValue, setSubmissionValue } from '../../lib/elements';
|
||||
import { ClassNames, Option } from '../../types';
|
||||
import { SubmissionContext } from '../SnoopForm/SnoopForm';
|
||||
import { PageContext } from '../SnoopPage/SnoopPage';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
label?: string;
|
||||
help?: string;
|
||||
options: (Option | string)[];
|
||||
placeholder?: string;
|
||||
classNames: ClassNames;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export const Radio: FC<Props> = ({
|
||||
name,
|
||||
label,
|
||||
help,
|
||||
options,
|
||||
classNames,
|
||||
}) => {
|
||||
const { setSubmission }: any = useContext(SubmissionContext);
|
||||
const pageName = useContext(PageContext);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{label && (
|
||||
<label
|
||||
className={
|
||||
classNames.label || 'block text-sm font-medium text-gray-700'
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<fieldset className="mt-2">
|
||||
<legend className="sr-only">Please choose an option</legend>
|
||||
<div className="space-y-2">
|
||||
{options.map((option) => (
|
||||
<div key={getOptionValue(option)} className="flex items-center">
|
||||
<input
|
||||
id={`${name}-${getOptionValue(option)}`}
|
||||
name={name}
|
||||
type="radio"
|
||||
className={
|
||||
classNames.element ||
|
||||
'focus:ring-slate-500 h-4 w-4 text-slate-600 border-gray-300'
|
||||
}
|
||||
onClick={() =>
|
||||
setSubmissionValue(
|
||||
getOptionValue(option),
|
||||
pageName,
|
||||
name,
|
||||
setSubmission
|
||||
)
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`${name}-${
|
||||
typeof option === 'object' ? option.value : option
|
||||
}`}
|
||||
className={
|
||||
classNames.elementLabel ||
|
||||
'block ml-3 text-base font-medium text-gray-700'
|
||||
}
|
||||
>
|
||||
{typeof option === 'object' ? option.label : option}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
{help && (
|
||||
<p className={classNames.help || 'mt-2 text-sm text-gray-500'}>
|
||||
{help}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
22
packages/snoopforms-react/src/components/Elements/Submit.tsx
Normal file
22
packages/snoopforms-react/src/components/Elements/Submit.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React, { FC } from 'react';
|
||||
import { classNamesConcat } from '../../lib/utils';
|
||||
import { ClassNames } from '../../types';
|
||||
|
||||
interface Props {
|
||||
label?: string;
|
||||
classNames?: ClassNames;
|
||||
}
|
||||
|
||||
export const Submit: FC<Props> = ({ classNames, label }) => {
|
||||
return (
|
||||
<button
|
||||
type="submit"
|
||||
className={classNamesConcat(
|
||||
classNames?.button ||
|
||||
'inline-flex items-center px-3 py-2 text-sm font-medium leading-4 text-white border border-transparent rounded-md shadow-sm bg-slate-600 hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500'
|
||||
)}
|
||||
>
|
||||
{label || 'Submit'}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
71
packages/snoopforms-react/src/components/Elements/Text.tsx
Normal file
71
packages/snoopforms-react/src/components/Elements/Text.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React, { FC, useContext } from 'react';
|
||||
import { setSubmissionValue } from '../../lib/elements';
|
||||
import { classNamesConcat } from '../../lib/utils';
|
||||
import { ClassNames } from '../../types';
|
||||
import { SubmissionContext } from '../SnoopForm/SnoopForm';
|
||||
import { PageContext } from '../SnoopPage/SnoopPage';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
label?: string;
|
||||
help?: string;
|
||||
Icon?: React.ReactNode;
|
||||
placeholder?: string;
|
||||
classNames: ClassNames;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
export const Text: FC<Props> = ({
|
||||
name,
|
||||
label,
|
||||
help,
|
||||
Icon,
|
||||
classNames,
|
||||
placeholder,
|
||||
required,
|
||||
}) => {
|
||||
const { setSubmission } = useContext(SubmissionContext);
|
||||
const pageName = useContext(PageContext);
|
||||
return (
|
||||
<div>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={name}
|
||||
className={
|
||||
classNames.label || 'block text-sm font-medium text-gray-700'
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div className={'relative mt-1 rounded-md shadow-sm'}>
|
||||
{Icon && (
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<div className="w-5 h-5 text-gray-400">{Icon}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name={name}
|
||||
id={`input-${name}`}
|
||||
className={classNamesConcat(
|
||||
Icon ? 'pl-10' : '',
|
||||
classNames.element ||
|
||||
'block w-full border-gray-300 rounded-md focus:ring-slate-500 focus:border-slate-500 sm:text-sm'
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) =>
|
||||
setSubmissionValue(e.target.value, pageName, name, setSubmission)
|
||||
}
|
||||
required={required}
|
||||
/>
|
||||
</div>
|
||||
{help && (
|
||||
<p className={classNames.help || 'mt-2 text-sm text-gray-500'}>
|
||||
{help}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import React, { FC, useContext } from 'react';
|
||||
import { setSubmissionValue } from '../../lib/elements';
|
||||
import { SubmissionContext } from '../SnoopForm/SnoopForm';
|
||||
import { PageContext } from '../SnoopPage/SnoopPage';
|
||||
import { ClassNames } from '../../types';
|
||||
import { classNamesConcat } from '../../lib/utils';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
label?: string;
|
||||
help?: string;
|
||||
placeholder?: string;
|
||||
rows?: number;
|
||||
classNames: ClassNames;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
export const Textarea: FC<Props> = ({
|
||||
name,
|
||||
label,
|
||||
help,
|
||||
classNames,
|
||||
placeholder,
|
||||
rows,
|
||||
required,
|
||||
}) => {
|
||||
const { setSubmission } = useContext(SubmissionContext);
|
||||
const pageName = useContext(PageContext);
|
||||
return (
|
||||
<div>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={name}
|
||||
className={
|
||||
classNames.label || 'block text-sm font-medium text-gray-700'
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div className="mt-1">
|
||||
<textarea
|
||||
rows={rows}
|
||||
name={name}
|
||||
id={`input-${name}`}
|
||||
className={classNamesConcat(
|
||||
'block w-full border border-gray-300 rounded-md shadow-sm focus:ring-slate-500 focus:border-slate-500 sm:text-sm',
|
||||
classNames.element
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) =>
|
||||
setSubmissionValue(e.target.value, pageName, name, setSubmission)
|
||||
}
|
||||
required={required}
|
||||
/>
|
||||
</div>
|
||||
{help && (
|
||||
<p className={classNames.help || 'mt-2 text-sm text-gray-500'}>
|
||||
{help}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { FC, useContext } from 'react';
|
||||
import { setSubmissionValue } from '../../lib/elements';
|
||||
import { classNamesConcat } from '../../lib/utils';
|
||||
import { ClassNames } from '../../types';
|
||||
import { SubmissionContext } from '../SnoopForm/SnoopForm';
|
||||
import { PageContext } from '../SnoopPage/SnoopPage';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
label?: string;
|
||||
help?: string;
|
||||
Icon?: React.ReactNode;
|
||||
placeholder?: string;
|
||||
classNames: ClassNames;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
export const Website: FC<Props> = ({
|
||||
name,
|
||||
label,
|
||||
help,
|
||||
Icon,
|
||||
classNames,
|
||||
placeholder,
|
||||
required,
|
||||
}) => {
|
||||
const { setSubmission } = useContext(SubmissionContext);
|
||||
const pageName = useContext(PageContext);
|
||||
return (
|
||||
<div>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={name}
|
||||
className={
|
||||
classNames.label || 'block text-sm font-medium text-gray-700'
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div className="relative mt-1 rounded-md shadow-sm">
|
||||
{Icon && (
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<div className="w-5 h-5 text-gray-400 ">{Icon}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="url"
|
||||
pattern="https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)"
|
||||
onInvalid={(e: any) =>
|
||||
e.target.setCustomValidity('please provide a valid website address')
|
||||
}
|
||||
onInput={(e: any) => e.target.setCustomValidity('')}
|
||||
name={name}
|
||||
id={`input-${name}`}
|
||||
className={classNamesConcat(
|
||||
Icon ? 'pl-10' : '',
|
||||
classNames.element ||
|
||||
'block w-full border-gray-300 rounded-md focus:ring-slate-500 focus:border-slate-500 sm:text-sm'
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) =>
|
||||
setSubmissionValue(e.target.value, pageName, name, setSubmission)
|
||||
}
|
||||
required={required}
|
||||
/>
|
||||
</div>
|
||||
{help && (
|
||||
<p className={classNames.help || 'mt-2 text-sm text-gray-500'}>
|
||||
{help}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Meta, Story } from '@storybook/react';
|
||||
import React from 'react';
|
||||
import { SnoopElement, SnoopForm, SnoopPage } from '../src';
|
||||
import { SnoopElementProps } from '../src/components/SnoopElement/SnoopElement';
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'Snoop/SnoopElement',
|
||||
component: SnoopElement,
|
||||
argTypes: {
|
||||
type: {
|
||||
defaultValue: 'text',
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
controls: { expanded: true },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
const Template: Story<SnoopElementProps> = (args) => (
|
||||
<SnoopForm localOnly={true}>
|
||||
<SnoopPage name="snoopElement">
|
||||
<SnoopElement {...args} />
|
||||
</SnoopPage>
|
||||
</SnoopForm>
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
type: 'text',
|
||||
name: 'myInput',
|
||||
options: [],
|
||||
};
|
||||
@@ -0,0 +1,188 @@
|
||||
import React, { FC, useContext, useEffect } from 'react';
|
||||
import { getOptionsSchema } from '../../lib/elements';
|
||||
import { ClassNames } from '../../types';
|
||||
import { Cards } from '../Elements/Cards';
|
||||
import { Checkbox } from '../Elements/Checkbox';
|
||||
import { Email } from '../Elements/Email';
|
||||
import { Number } from '../Elements/Number';
|
||||
import { Phone } from '../Elements/Phone';
|
||||
import { Radio } from '../Elements/Radio';
|
||||
import { Submit } from '../Elements/Submit';
|
||||
import { Text } from '../Elements/Text';
|
||||
import { Textarea } from '../Elements/Textarea';
|
||||
import { Website } from '../Elements/Website';
|
||||
import { CurrentPageContext, SchemaContext } from '../SnoopForm/SnoopForm';
|
||||
import { PageContext } from '../SnoopPage/SnoopPage';
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface SnoopElementProps {
|
||||
type: string;
|
||||
name: string;
|
||||
label?: string;
|
||||
help?: string;
|
||||
icon?: React.ReactNode;
|
||||
placeholder?: string;
|
||||
classNames?: ClassNames;
|
||||
required?: boolean;
|
||||
options?: Option[] | string[];
|
||||
cols?: number;
|
||||
rows?: number;
|
||||
autoSubmit?: boolean;
|
||||
}
|
||||
|
||||
export const SnoopElement: FC<SnoopElementProps> = ({
|
||||
type,
|
||||
name,
|
||||
label = undefined,
|
||||
help = undefined,
|
||||
icon,
|
||||
placeholder,
|
||||
classNames = {},
|
||||
required = false,
|
||||
options,
|
||||
cols,
|
||||
rows,
|
||||
autoSubmit = false,
|
||||
}) => {
|
||||
const { schema, setSchema } = useContext(SchemaContext);
|
||||
const pageName = useContext(PageContext);
|
||||
const { currentPageIdx } = useContext(CurrentPageContext);
|
||||
|
||||
useEffect(() => {
|
||||
setSchema((schema: any) => {
|
||||
if (pageName === '') {
|
||||
console.warn(
|
||||
`🦝 SnoopForms: An Element must always be a child of a page!`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const newSchema = { ...schema };
|
||||
let pageIdx = newSchema.pages.findIndex((p: any) => p.name === pageName);
|
||||
if (pageIdx === -1) {
|
||||
console.warn(`🦝 SnoopForms: Error accessing page`);
|
||||
return;
|
||||
}
|
||||
let elementIdx = newSchema.pages[pageIdx].elements.findIndex(
|
||||
(e: any) => e.name === name
|
||||
);
|
||||
if (elementIdx === -1) {
|
||||
newSchema.pages[pageIdx].elements.push({ name });
|
||||
elementIdx = newSchema.pages[pageIdx].elements.length - 1;
|
||||
}
|
||||
newSchema.pages[pageIdx].elements[elementIdx].type = type;
|
||||
newSchema.pages[pageIdx].elements[elementIdx].label = label;
|
||||
newSchema.pages[pageIdx].elements[elementIdx].help = help;
|
||||
if (['checkbox', 'radio'].includes(type)) {
|
||||
newSchema.pages[pageIdx].elements[elementIdx].options =
|
||||
getOptionsSchema(options);
|
||||
}
|
||||
return newSchema;
|
||||
});
|
||||
}, [name, setSchema, pageName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{currentPageIdx ===
|
||||
schema.pages.findIndex((p: any) => p.name === pageName) && (
|
||||
<div>
|
||||
{type === 'cards' ? (
|
||||
<Cards
|
||||
name={name}
|
||||
label={label}
|
||||
help={help}
|
||||
cols={cols}
|
||||
classNames={classNames}
|
||||
required={required}
|
||||
options={options || []}
|
||||
autoSubmit={autoSubmit}
|
||||
/>
|
||||
) : type === 'checkbox' ? (
|
||||
<Checkbox
|
||||
name={name}
|
||||
label={label}
|
||||
help={help}
|
||||
classNames={classNames}
|
||||
required={required}
|
||||
options={options || []}
|
||||
/>
|
||||
) : type === 'email' ? (
|
||||
<Email
|
||||
name={name}
|
||||
label={label}
|
||||
help={help}
|
||||
Icon={icon}
|
||||
placeholder={placeholder}
|
||||
classNames={classNames}
|
||||
required={required}
|
||||
/>
|
||||
) : type === 'number' ? (
|
||||
<Number
|
||||
name={name}
|
||||
label={label}
|
||||
help={help}
|
||||
Icon={icon}
|
||||
placeholder={placeholder}
|
||||
classNames={classNames}
|
||||
required={required}
|
||||
/>
|
||||
) : type === 'phone' ? (
|
||||
<Phone
|
||||
name={name}
|
||||
label={label}
|
||||
help={help}
|
||||
Icon={icon}
|
||||
placeholder={placeholder}
|
||||
classNames={classNames}
|
||||
required={required}
|
||||
/>
|
||||
) : type === 'radio' ? (
|
||||
<Radio
|
||||
name={name}
|
||||
label={label}
|
||||
help={help}
|
||||
classNames={classNames}
|
||||
required={required}
|
||||
options={options || []}
|
||||
/>
|
||||
) : type === 'submit' ? (
|
||||
<Submit label={label} classNames={classNames} />
|
||||
) : type === 'text' ? (
|
||||
<Text
|
||||
name={name}
|
||||
label={label}
|
||||
help={help}
|
||||
Icon={icon}
|
||||
placeholder={placeholder}
|
||||
classNames={classNames}
|
||||
required={required}
|
||||
/>
|
||||
) : type === 'textarea' ? (
|
||||
<Textarea
|
||||
name={name}
|
||||
label={label}
|
||||
help={help}
|
||||
rows={rows}
|
||||
placeholder={placeholder}
|
||||
classNames={classNames}
|
||||
required={required}
|
||||
/>
|
||||
) : type === 'website' ? (
|
||||
<Website
|
||||
name={name}
|
||||
label={label}
|
||||
help={help}
|
||||
Icon={icon}
|
||||
placeholder={placeholder}
|
||||
classNames={classNames}
|
||||
required={required}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
138
packages/snoopforms-react/src/components/SnoopForm/SnoopForm.tsx
Normal file
138
packages/snoopforms-react/src/components/SnoopForm/SnoopForm.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import React, { createContext, FC, ReactNode, useState } from 'react';
|
||||
import { classNamesConcat } from '../../lib/utils';
|
||||
|
||||
export const SchemaContext = createContext({
|
||||
schema: { pages: [] },
|
||||
setSchema: (schema: any) => {
|
||||
console.log(schema);
|
||||
},
|
||||
});
|
||||
|
||||
export const SubmissionContext = createContext({
|
||||
submission: {},
|
||||
setSubmission: (submission: any) => {
|
||||
console.log(submission);
|
||||
},
|
||||
});
|
||||
|
||||
export const CurrentPageContext = createContext({
|
||||
currentPageIdx: 0,
|
||||
setCurrentPageIdx: (currentPageIdx: number) => {
|
||||
console.log(currentPageIdx);
|
||||
},
|
||||
});
|
||||
|
||||
export const SubmitHandlerContext = createContext((pageName: string) => {
|
||||
console.log(pageName);
|
||||
});
|
||||
|
||||
interface onSubmitProps {
|
||||
submission: any;
|
||||
schema: any;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
domain?: string;
|
||||
formId?: string;
|
||||
protocol?: 'http' | 'https';
|
||||
localOnly?: boolean;
|
||||
className?: string;
|
||||
onSubmit?: (obj: onSubmitProps) => void;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const SnoopForm: FC<Props> = ({
|
||||
domain = 'app.snoopforms.com',
|
||||
formId,
|
||||
protocol = 'https',
|
||||
localOnly = false,
|
||||
className = '',
|
||||
onSubmit = (): any => {},
|
||||
children,
|
||||
}) => {
|
||||
const [schema, setSchema] = useState<any>({ pages: [] });
|
||||
const [submission, setSubmission] = useState<any>({});
|
||||
const [currentPageIdx, setCurrentPageIdx] = useState(0);
|
||||
const [submissionSessionId, setSubmissionSessionId] = useState('');
|
||||
|
||||
const handleSubmit = async (pageName: string) => {
|
||||
let _submissionSessionId = submissionSessionId;
|
||||
if (!localOnly) {
|
||||
// create answer session if it don't exist
|
||||
try {
|
||||
if (!formId) {
|
||||
console.warn(
|
||||
`🦝 SnoopForms: formId not set. Skipping sending submission to snoopHub.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!_submissionSessionId) {
|
||||
// create new submissionSession in snoopHub
|
||||
|
||||
const submissionSessionRes: any = await fetch(
|
||||
`${protocol}://${domain}/api/forms/${formId}/submissionSessions`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
}
|
||||
);
|
||||
const submissionSession = await submissionSessionRes.json();
|
||||
_submissionSessionId = submissionSession.id;
|
||||
setSubmissionSessionId(_submissionSessionId);
|
||||
}
|
||||
// send answer to snoop platform
|
||||
await fetch(`${protocol}://${domain}/api/forms/${formId}/event`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
events: [
|
||||
{
|
||||
type: 'pageSubmission',
|
||||
data: {
|
||||
pageName,
|
||||
submissionSessionId: _submissionSessionId,
|
||||
submission: submission[pageName],
|
||||
},
|
||||
},
|
||||
// update schema
|
||||
// TODO: do conditionally only when requested by the snoopHub
|
||||
{ type: 'updateSchema', data: schema },
|
||||
],
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`🦝 SnoopForms: Unable to send submission to snoopHub. Error: ${e}`
|
||||
);
|
||||
}
|
||||
}
|
||||
const maxPageIdx = schema.pages.length - 1;
|
||||
const hasThankYou = schema.pages[maxPageIdx].type === 'thankyou';
|
||||
if (currentPageIdx < maxPageIdx) {
|
||||
setCurrentPageIdx(currentPageIdx + 1);
|
||||
}
|
||||
if (
|
||||
(!hasThankYou && currentPageIdx === maxPageIdx) ||
|
||||
(hasThankYou && currentPageIdx === maxPageIdx - 1)
|
||||
) {
|
||||
return onSubmit({ submission, schema });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SubmitHandlerContext.Provider value={handleSubmit}>
|
||||
<SchemaContext.Provider value={{ schema, setSchema }}>
|
||||
<SubmissionContext.Provider value={{ submission, setSubmission }}>
|
||||
<CurrentPageContext.Provider
|
||||
value={{ currentPageIdx, setCurrentPageIdx }}
|
||||
>
|
||||
<div className={classNamesConcat('max-w-lg', className)}>
|
||||
{children}
|
||||
</div>
|
||||
</CurrentPageContext.Provider>
|
||||
</SubmissionContext.Provider>
|
||||
</SchemaContext.Provider>
|
||||
</SubmitHandlerContext.Provider>
|
||||
);
|
||||
};
|
||||
100
packages/snoopforms-react/src/components/SnoopPage/SnoopPage.tsx
Normal file
100
packages/snoopforms-react/src/components/SnoopPage/SnoopPage.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React, {
|
||||
createContext,
|
||||
FC,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { classNamesConcat } from '../../lib/utils';
|
||||
import {
|
||||
CurrentPageContext,
|
||||
SchemaContext,
|
||||
SubmitHandlerContext,
|
||||
} from '../SnoopForm/SnoopForm';
|
||||
|
||||
export const PageContext = createContext('');
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
thankyou?: boolean;
|
||||
}
|
||||
|
||||
export const SnoopPage: FC<Props> = ({
|
||||
name,
|
||||
className,
|
||||
children,
|
||||
thankyou = false,
|
||||
}) => {
|
||||
const { schema, setSchema } = useContext<any>(SchemaContext);
|
||||
const handleSubmit = useContext(SubmitHandlerContext);
|
||||
const [initializing, setInitializing] = useState(true);
|
||||
const { currentPageIdx } = useContext(CurrentPageContext);
|
||||
|
||||
useEffect(() => {
|
||||
setSchema((schema: any) => {
|
||||
const newSchema = { ...schema };
|
||||
let pageIdx = newSchema.pages.findIndex((p: any) => p.name === name);
|
||||
if (pageIdx !== -1) {
|
||||
console.warn(
|
||||
`🦝 SnoopForms: Page with the name "${name}" already exists`
|
||||
);
|
||||
return newSchema;
|
||||
}
|
||||
newSchema.pages.push({
|
||||
name,
|
||||
type: thankyou ? 'thankyou' : 'form',
|
||||
elements: [],
|
||||
});
|
||||
|
||||
return newSchema;
|
||||
});
|
||||
}, [name]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initializing) {
|
||||
let pageIdx = schema.pages.findIndex((p: any) => p.name === name);
|
||||
if (pageIdx !== -1) {
|
||||
setInitializing(false);
|
||||
}
|
||||
}
|
||||
}, [schema]);
|
||||
|
||||
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
handleSubmit(name);
|
||||
};
|
||||
|
||||
if (initializing) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
if (thankyou) {
|
||||
return (
|
||||
<PageContext.Provider value={name}>
|
||||
{currentPageIdx ===
|
||||
schema.pages.findIndex((p: any) => p.name === name) && children}
|
||||
</PageContext.Provider>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<PageContext.Provider value={name}>
|
||||
<form
|
||||
className={classNamesConcat(
|
||||
currentPageIdx ===
|
||||
schema.pages.findIndex((p: any) => p.name === name)
|
||||
? 'block'
|
||||
: 'hidden',
|
||||
'space-y-6',
|
||||
className
|
||||
)}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{children}
|
||||
</form>
|
||||
</PageContext.Provider>
|
||||
);
|
||||
}
|
||||
};
|
||||
3
packages/snoopforms-react/src/index.ts
Normal file
3
packages/snoopforms-react/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './components/SnoopForm/SnoopForm';
|
||||
export * from './components/SnoopElement/SnoopElement';
|
||||
export * from './components/SnoopPage/SnoopPage';
|
||||
40
packages/snoopforms-react/src/lib/elements.ts
Normal file
40
packages/snoopforms-react/src/lib/elements.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Option } from '../types';
|
||||
|
||||
export const setSubmissionValue = (
|
||||
v: any,
|
||||
pageName: string,
|
||||
name: string,
|
||||
setSubmission: (s: any) => void
|
||||
) => {
|
||||
setSubmission((submission: any) => {
|
||||
const newSubmission = { ...submission };
|
||||
if (!(pageName in newSubmission)) {
|
||||
newSubmission[pageName] = {};
|
||||
}
|
||||
newSubmission[pageName][name] = v;
|
||||
return newSubmission;
|
||||
});
|
||||
};
|
||||
|
||||
export const getOptionsSchema = (options: any[] | undefined) => {
|
||||
const newOptions = [];
|
||||
if (options) {
|
||||
for (const option of options) {
|
||||
if (typeof option === 'string') {
|
||||
newOptions.push({ label: option, value: option });
|
||||
}
|
||||
if (
|
||||
typeof option === 'object' &&
|
||||
'value' in option &&
|
||||
'label' in option
|
||||
) {
|
||||
newOptions.push({ label: option.label, value: option.value });
|
||||
}
|
||||
}
|
||||
}
|
||||
return newOptions;
|
||||
};
|
||||
|
||||
export function getOptionValue(option: string | Option) {
|
||||
return typeof option === 'object' ? option.value : option;
|
||||
}
|
||||
3
packages/snoopforms-react/src/lib/utils.ts
Normal file
3
packages/snoopforms-react/src/lib/utils.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const classNamesConcat = (...classes: any) => {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
};
|
||||
3
packages/snoopforms-react/src/styles.css
Normal file
3
packages/snoopforms-react/src/styles.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
14
packages/snoopforms-react/src/types.ts
Normal file
14
packages/snoopforms-react/src/types.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface ClassNames {
|
||||
label?: string;
|
||||
help?: string;
|
||||
element?: string;
|
||||
radioOption?: string | ((bag: any) => string) | undefined;
|
||||
radioGroup?: string;
|
||||
elementLabel?: string;
|
||||
button?: string;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
7
packages/snoopforms-react/tailwind.config.js
Normal file
7
packages/snoopforms-react/tailwind.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}', './stories/*'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require('@tailwindcss/forms')],
|
||||
};
|
||||
5
packages/snoopforms-react/tsconfig.json
Normal file
5
packages/snoopforms-react/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "tsconfig/react-library.json",
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tailwind-config",
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": ["DOM", "DOM.iterable", "ESNext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tsconfig",
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
"display": "React Library",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2015"],
|
||||
"lib": ["ES2015", "DOM"],
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"jsx": "react-jsx"
|
||||
"jsx": "react"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "ui",
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
|
||||
10582
pnpm-lock.yaml
generated
10582
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user