Feature/add react lib to monorepo #107 (#115)

* add changesets

* add react-app to monorepo
This commit is contained in:
Matti Nannt
2022-10-18 12:24:17 +02:00
committed by GitHub
parent 5a7492536a
commit 28b6410dbb
44 changed files with 12014 additions and 180 deletions

8
.changeset/README.md Normal file
View 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
View 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": []
}

View File

@@ -0,0 +1,5 @@
---
"@snoopforms/react": patch
---
Move react library to monorepo

View File

@@ -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 #

View File

@@ -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
View 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 }}

View File

@@ -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.

View File

@@ -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",

View File

@@ -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"

View File

@@ -1,6 +1,7 @@
{
"name": "database",
"version": "1.0.0",
"private": true,
"license": "MIT",
"main": "./dist/index.js",
"module": "./dist/index.mjs",

View File

@@ -1,6 +1,7 @@
{
"name": "eslint-config-custom",
"version": "0.0.0",
"version": "1.0.0",
"private": true,
"main": "index.js",
"license": "MIT",
"dependencies": {

View File

@@ -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
View File

@@ -0,0 +1,7 @@
*.log
.DS_Store
node_modules
.cache
dist
.parcel-cache
.idea/

View 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.

View File

@@ -0,0 +1,63 @@
# snoopForms React Library
React Library with form- & survey-elements for the snoopForms platform
[![npm package](https://img.shields.io/badge/npm%20i-@snoopforms/react)](https://www.npmjs.com/package/@snoopforms/react) [![version number](https://img.shields.io/npm/v/@snoopforms/react?color=green&label=version)](https://github.com/snoopforms/react/releases) [![Actions Status](https://github.com/snoopForms/snoopforms-react/workflows/Test/badge.svg)](https://github.com/snoopForms/snoopforms-react/actions) [![License](https://img.shields.io/github/license/snoopforms/snoopforms-react)](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**.

View 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"
}
}

View File

@@ -0,0 +1,7 @@
module.exports = {
plugins: {
tailwindcss: {
content: ['./src/**/*.{js,jsx,ts,tsx}', './stories/*'],
},
},
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View File

@@ -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>
);
};

View 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 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>
);
};

View File

@@ -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: [],
};

View File

@@ -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>
);
};

View 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>
);
};

View 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>
);
}
};

View File

@@ -0,0 +1,3 @@
export * from './components/SnoopForm/SnoopForm';
export * from './components/SnoopElement/SnoopElement';
export * from './components/SnoopPage/SnoopPage';

View 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;
}

View File

@@ -0,0 +1,3 @@
export const classNamesConcat = (...classes: any) => {
return classes.filter(Boolean).join(' ');
};

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View 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;
}

View File

@@ -0,0 +1,7 @@
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}', './stories/*'],
theme: {
extend: {},
},
plugins: [require('@tailwindcss/forms')],
};

View File

@@ -0,0 +1,5 @@
{
"extends": "tsconfig/react-library.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

View File

@@ -1,6 +1,6 @@
{
"name": "tailwind-config",
"version": "0.0.0",
"version": "1.0.0",
"private": true,
"main": "index.js",
"devDependencies": {

View File

@@ -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,

View File

@@ -1,6 +1,6 @@
{
"name": "tsconfig",
"version": "0.0.0",
"version": "1.0.0",
"private": true,
"license": "MIT",
"publishConfig": {

View File

@@ -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"
}
}

View File

@@ -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

File diff suppressed because it is too large Load Diff