Feature/monorepo #95 (#105)

Move repository into a monorepo with turborepo and pnpm.
This is a big change in the way the code is organized, used and deployed.
This commit is contained in:
Matti Nannt
2022-10-13 09:46:43 +02:00
committed by GitHub
parent 2d63249f63
commit 5c378bc8ce
210 changed files with 6064 additions and 5021 deletions

View File

@@ -1,14 +0,0 @@
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
ARG VARIANT=16-bullseye
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment if you want to install an additional version of node using nvm
# ARG EXTRA_NODE_VERSION=10
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
# [Optional] Uncomment if you want to install more global node modules
# RUN su node -c "npm install -g <your-package-list-here>"

View File

@@ -1,31 +0,0 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/javascript-node-postgres
// Update the VARIANT arg in docker-compose.yml to pick a Node.js version
{
"name": "Node.js & PostgreSQL",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"dbaeumer.vscode-eslint"
]
}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// This can be used to network with other containers or with the host.
"forwardPorts": [
// frontend
3000,
// postgres
5432
],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "yarn install && yarn prisma migrate dev",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node"
}

View File

@@ -1,63 +0,0 @@
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
args:
VARIANT: 16-bullseye
environment:
DATABASE_URL: postgresql://postgres:postgres@db:5432/snoopformsdev?schema=public
NEXTAUTH_URL: http://localhost:3000
NEXT_TELEMETRY_DISABLED: 1
MAIL_FROM: noreply@example.com
SMTP_HOST: mailhog
SMTP_PORT: 1025
SMTP_USER: smtpUser
SMTP_PASSWORD: smtpPassword
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
volumes:
- ..:/workspace:cached
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity
# # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
# network_mode: service:db
# Uncomment the next line to use a non-root user for all processes.
# user: node
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
db:
image: postgres:13-alpine
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
# network_mode: service:app
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: snoopformsdev
mailhog:
image: mailhog/mailhog
# network_mode: service:app
logging:
driver: 'none' # disable saving logs
ports:
- 8025:8025 # web ui
# - 1025:1025 # smtp server
volumes:
postgres-data:

View File

@@ -1,32 +1,74 @@
# ------------ MANDATORY (CHANGE ACCORDING TO YOUR SETUP) ------------
########################################################################
# ------------ MANDATORY (CHANGE ACCORDING TO YOUR SETUP) ------------#
########################################################################
############
# Basics #
############
NEXTAUTH_SECRET=RANDOM_STRING
NEXTAUTH_URL=http://localhost:3000
DATABASE_URL='postgresql://user@localhost:5432/snoopforms?schema=public'
# For Docker Compose Setup use this Database URL:
DATABASE_URL='postgresql://postgres:postgres@postgres:5432/snoopforms?schema=public'
# For Docker Compose Production Setup use this Database URL:
# DATABASE_URL='postgresql://postgres:postgres@postgres:5432/snoopforms?schema=public'
################
# Mail Setup #
################
# Necessary if email verification and password reset are enabled.
# See optional configurations below if you want to disable these features.
MAIL_FROM=noreply@example.com
SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_SECURE_ENABLED=0 # Enable for TLS (port 465)
SMTP_USER=smtpUser
SMTP_PASSWORD=smtpPassword
########################################################################
# ------------------------------ OPTIONAL -----------------------------#
########################################################################
# Uncomment the variables you would like to use and customize the values.
#####################
# Disable Features #
#####################
# Email Verification. If you enable Email Verification you have to setup SMTP-Settings, too.
EMAIL_VERIFICATION_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
# PASSWORD_RESET_DISABLED=1
NEXT_TELEMETRY_DISABLED=1
# ------------ OPTIONAL ------------
## MAIL SETUP
# MAIL_FROM=noreply@example.com
# SMTP_HOST=smtp.example.com
# SMTP_PORT=587
# SMTP_USER=smtpUser
# SMTP_PASSWORD=smtpPassword
#######################
# Additional Options #
#######################
# 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
# PUBLIC_PRIVACY_URL=https://www.example.com/enduserPrivacy
######################
# Posthog Tracking #
######################
# POSTHOG_API_HOST=https://app.posthog.com
# POSTHOG_API_KEY=<YOUR POSTHOG API KEY>
###############
# Telemetry #
###############
# We also track anononymous usage telemetry on the server to improve snoopForms.
# That way we can see how many people use snoopForms.
# We can't identify you, or your users and only receive the number of submissions on your instance.
# You help us a lot, if you leave this activated.
# If you still want to opt-out, uncomment the next line.
# TELEMETRY_DISABLED=1

10
.eslintrc.js Normal file
View File

@@ -0,0 +1,10 @@
module.exports = {
root: true,
// This tells ESLint to load the config from the package `eslint-config-custom`
extends: ["custom"],
settings: {
next: {
rootDir: ["apps/*/"],
},
},
};

View File

@@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}

View File

@@ -10,6 +10,10 @@ jobs:
node: ["16.x"]
os: [ubuntu-latest, windows-latest, macOS-latest]
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
steps:
- name: Checkout repo
uses: actions/checkout@v2
@@ -19,11 +23,14 @@ jobs:
with:
node-version: ${{ matrix.node }}
- name: Install deps and build (with cache)
uses: bahmutov/npm-install@v1
- name: Install pnpm
uses: pnpm/action-setup@v2.2.2
- name: Install dependencies
run: pnpm install
- name: Lint
run: yarn lint
run: pnpm lint
- name: Build
run: yarn build
run: pnpm build

26
.gitignore vendored
View File

@@ -1,30 +1,30 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
node_modules
.pnp
.pnp.js
.pnpm-store/
# testing
/coverage
coverage
# next.js
/.next/
/out/
.next/
out/
build
# production
/build
# node
dist/
# misc
.DS_Store
*.pem
.idea/
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env
@@ -32,9 +32,7 @@ yarn-error.log*
.env.development.local
.env.test.local
.env.production.local
!packages/database/.env
# vercel
.vercel
# typescript
*.tsbuildinfo
# turbo
.turbo

6
.npmrc Normal file
View File

@@ -0,0 +1,6 @@
auto-install-peers=true
link-workspace-packages = true
shamefully-hoist = true
shared-workspace-shrinkwrap = true
access = public
enable-pre-post-scripts = true

1
.prettierrc.js Normal file
View File

@@ -0,0 +1 @@
module.exports = require("./packages/prettier-config/prettier-preset");

View File

@@ -1,39 +0,0 @@
# Install dependencies only when needed
FROM node:16-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
# Rebuild the source code only when needed
FROM node:16-alpine AS builder
WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
RUN yarn prisma generate
RUN yarn build && yarn install --production --ignore-scripts --prefer-offline
# Production image, copy all the files and run next
FROM node:16-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/prisma ./prisma
USER nextjs
EXPOSE 3000
ENV NEXT_TELEMETRY_DISABLED 1
CMD ["yarn", "start"]

View File

@@ -19,10 +19,6 @@
<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.
<br/>
## About snoopForms
<img width="937" alt="snoopForms-architecture" src="https://user-images.githubusercontent.com/675065/182550268-09794c9e-1187-470e-b795-697ceb2a93b8.svg">
@@ -49,86 +45,78 @@ With snoopForms you can build complex multi-page forms in minutes using either o
- [TailwindCSS](https://tailwindcss.com/)
- [Prisma](https://prisma.io/)
## Getting started
## Cloud vs. self-hosted
you can develop in a VS Code [dev container](https://code.visualstudio.com/docs/remote/containers) or using any editor/IDE with local tool installation.
We offer you a ready hosted and maintained version of snoopForms on [snoopforms.com](https://snoopforms.com). It is always up to date and offers a generous free plan. If you want to try snoopForms, or save yourself the hassle and stress of self-hosting, this is the place to start.
### Getting started using VS Code dev container
The version of snoopForms you'll find in this repository is the same version that runs in the cloud, and you can easily host it yourself on your servers. See the readme below for the deployment instructions.
You need
(In the future we may develop additional features that aren't in the free Open-Source version)
- Docker, e.g. [Docker Desktop](https://www.docker.com/products/docker-desktop)
- [VS Code](https://code.visualstudio.com/download) with the extension [Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) (`ms-vscode-remote.remote-containers`)
## Get started with development
Either use the command `Open Folder in Container...` in the cloned repo, or use `Remote Containers: Clone Repository in Container Volume...`, for example to inspect a PR.
This repository is a monorepository using [Turborepo](https://turborepo.org/) and [pnpm](https://pnpm.io/). It contains the snoopForms [server application](https://github.com/formbricks/snoopforms/tree/main/apps/web), the [react library](https://github.com/formbricks/snoopforms/tree/main/packages/react) and other helper packages like database or UI library.
The dev container comes with
- Node.JS, yarn etc pre-installed
- a `postgres` container and environment variables preset to reach it,
- a `mailhog` container that acts as a mock SMTP server and shows received mails in a web UI (forwarded to your host's `localhost:8025`)
upon start, it executes the `yarn install` and `yarn prisma migrate dev` automatically once.
When your dev container is ready, you can simply hit `F5` to start the application in debug mode.
### Getting started using local development setup
### How to run locally
To get the project running locally on your machine you need to have the following development tools installed:
- Node.JS (we recommend v16)
- Yarn
- PostgreSQL
- [pnpm](https://pnpm.io/)
- [Docker](https://www.docker.com/) (to run PostgreSQL / MailHog)
1. Clone the project:
```
git clone https://github.com/formbricks/snoopforms.git && cd snoopforms
git clone https://github.com/formbricks/snoopforms.git
```
2. Install Node.JS packages via yarn. Don't have yarn? Use `npm install --global yarn`.
and move into the directory
```
yarn install
cd snoopforms
```
3. Make sure you have a running database instance, e.g. by using docker. A quick and dirty instance can be spun up via:
2. Install Node.JS packages via pnpm. Don't have pnpm? Get it [here](https://pnpm.io/installation)
```
docker run --name snoopformsDB -p 5432:5432 -e POSTGRES_USER=snoopforms -e POSTGRES_PASSWORD=password -e POSTGRES_DB=snoopforms -d postgres
pnpm install
```
4. Create a `.env` file based on `.env.example` and change it according to your setup. Make sure the `DATABASE_URL` variable is set correctly according to your local database.
3. To make the process of installing a dev dependencies easier, we offer a [`docker-compose.yml`](https://docs.docker.com/compose/) file to deploy a PostgreSQL server locally with a new database named `turborepo` (To change this update the `MYSQL_DATABASE` environment variable in the `docker-compose.yml` file)
- a `postgres` container and environment variables preset to reach it,
- a `mailhog` container that acts as a mock SMTP server and shows received mails in a web UI (forwarded to your host's `localhost:8025`)
```
docker-compose -f docker-compose.dev.yml up -d
```
4. Create a `.env` file based on `.env.example` and change it according to your setup. If you are using a cloud based database or another mail server, you will need to update the `DATABASE_URL` and SMTP settings in your `.env` accordingly.
```
cp .env.example .env
```
For the example above, use the following:
5. Make sure your PostgreSQL Database Server is running. Then let prisma set up the database for you:
```
DATABASE_URL='postgresql://snoopforms:password@localhost:5432/snoopforms?schema=public'
pnpm dlx prisma migrate dev
```
5. Use the code editor of your choice to edit the .env file. You need to change all fields according to your setup.
6. Make sure your PostgreSQL Database Server is running. Then let prisma set up the database for you:
6. Start the development server:
```
yarn prisma migrate dev
```
7. Start the development server:
```
yarn dev
pnpm dev
```
**You can now access the app on [https://localhost:3000](https://localhost:3000)**. You will be automatically redirected to the login. To use your local installation of snoopForms, create a new account.
## Deployment
For viewing the confirmation email and other emails the system sends you, you can access mailhog at [https://localhost:8025](https://localhost:8025)
The easiest way to deploy the snoopHub on your own machine is using Docker. This requires Docker and the docker compose plugin on your system to work.
## Deployment for Production Setup
The easiest way to deploy snoopForms on your own machine is using Docker. This requires Docker and the docker compose plugin on your system to work.
Clone the repository:
@@ -138,15 +126,17 @@ git clone https://github.com/formbricks/snoopforms.git && cd snoopforms
```
Create a `.env` file based on `.env.example` and change all fields according to your setup. The SMTP-credentials are essential for verification emails to work during user signup.
Create a `.env` file based on `.env.example` and change all fields according to your setup. You need to uncomment the right line for the `DATABASE_URL` for the database connection to work. Also you need to configure the SMTP settings for the signup process with verification emails to work. If you don't have a mail server for sending email, you need to disable email verification (`EMAIL_VERIFICATION_DISABLED=1`) and password reset (`PASSWORD_RESET_DISABLED=1`).
Copy the `.env.example` file to `.env` and edit it with an editor of your choice.
```
cp .env.example .env && nano .env
cp .env.example .env
```
Start the docker compose process to build and spin up the snoopForms container as well as the postgres database.
Start the docker compose process to build and spin up the snoopForms container as well as the PostgreSQL database.
```bash

4
apps/web/.eslintrc.js Normal file
View File

@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ["custom"],
};

40
apps/web/.gitignore vendored Normal file
View File

@@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
.idea/
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
# typescript
*.tsbuildinfo

38
apps/web/Dockerfile Normal file
View File

@@ -0,0 +1,38 @@
# Add lockfile and package.json's of isolated subworkspace
FROM node:16-alpine AS installer
RUN apk update
RUN apk --no-cache add curl libc6-compat
RUN curl -fsSL "https://github.com/pnpm/pnpm/releases/latest/download/pnpm-linuxstatic-x64" -o /bin/pnpm; chmod +x /bin/pnpm;
WORKDIR /app
# First install the dependencies (as they change less often)
COPY . .
RUN pnpm install
# Build the project
RUN pnpm dlx prisma generate
RUN pnpm turbo run build --filter=web...
FROM node:16-alpine AS runner
RUN apk --no-cache add curl libc6-compat
RUN curl -fsSL "https://github.com/pnpm/pnpm/releases/latest/download/pnpm-linuxstatic-x64" -o /bin/pnpm; chmod +x /bin/pnpm;
# Don't run production as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs
WORKDIR /home/nextjs
COPY --from=installer /app/apps/web/next.config.js .
COPY --from=installer /app/apps/web/package.json .
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
COPY --from=installer --chown=nextjs:nodejs /app/packages/database/prisma/schema.prisma ./packages/database/prisma/schema.prisma
CMD pnpm dlx prisma migrate deploy && node apps/web/server.js

View File

@@ -1,10 +1,5 @@
import { Menu, Transition } from "@headlessui/react";
import {
DocumentPlusIcon,
PlusIcon,
CommandLineIcon,
SquaresPlusIcon,
} from "@heroicons/react/24/outline";
import { DocumentPlusIcon, PlusIcon, CommandLineIcon, SquaresPlusIcon } from "@heroicons/react/24/outline";
import { EllipsisHorizontalIcon, TrashIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
import { Fragment, useState } from "react";
@@ -47,18 +42,17 @@ export default function FormList() {
hintText="Start by creating a form."
buttonText="create form"
borderStyles="border-4 border-dotted border-red"
hasButton={true}
>
<DocumentPlusIcon className="w-24 h-24 mx-auto text-ui-gray-medium stroke-thin" />
hasButton={true}>
<DocumentPlusIcon className="text-ui-gray-medium stroke-thin mx-auto h-24 w-24" />
</EmptyPageFiller>
</div>
) : (
<ul className="grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 place-content-stretch ">
<ul className="grid grid-cols-2 place-content-stretch gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 ">
<button onClick={() => newForm()}>
<li className="h-56 col-span-1">
<div className="flex items-center justify-center h-full overflow-hidden font-light text-white rounded-md shadow bg-snoopfade">
<li className="col-span-1 h-56">
<div className="bg-snoopfade flex h-full items-center justify-center overflow-hidden rounded-md font-light text-white shadow">
<div className="px-4 py-8 sm:p-14">
<PlusIcon className="mx-auto w-14 h-14 stroke-thin" />
<PlusIcon className="stroke-thin mx-auto h-14 w-14" />
create form
</div>
</div>
@@ -67,48 +61,40 @@ export default function FormList() {
{forms
.sort((a, b) => b.updatedAt - a.updatedAt)
.map((form, formIdx) => (
<li key={form.id} className="relative h-56 col-span-1">
<div className="flex flex-col justify-between h-full bg-white rounded-md shadow">
<li key={form.id} className="relative col-span-1 h-56">
<div className="flex h-full flex-col justify-between rounded-md bg-white shadow">
<div className="p-6">
<p className="text-lg line-clamp-3">{form.name}</p>
<p className="line-clamp-3 text-lg">{form.name}</p>
</div>
<Link href={`/forms/${form.id}`}>
<a className="absolute w-full h-full" />
<a className="absolute h-full w-full" />
</Link>
<div className="divide-y divide-ui-gray-light ">
<div className="inline-flex px-2 py-1 mb-2 ml-4 text-sm rounded-sm bg-ui-gray-light text-ui-gray-dark">
<div className="divide-ui-gray-light divide-y ">
<div className="bg-ui-gray-light text-ui-gray-dark mb-2 ml-4 inline-flex rounded-sm px-2 py-1 text-sm">
{form.formType == "NOCODE" ? (
<div className="flex">
<SquaresPlusIcon className="w-4 h-4 my-auto mr-1" />
<SquaresPlusIcon className="my-auto mr-1 h-4 w-4" />
No-Code
</div>
) : (
<div className="flex">
<CommandLineIcon className="w-4 h-4 my-auto mr-1" />
<CommandLineIcon className="my-auto mr-1 h-4 w-4" />
Code
</div>
)}
</div>
<div className="flex justify-between px-4 py-2 text-right sm:px-6">
<p className="text-xs text-ui-gray-medium ">
<p className="text-ui-gray-medium text-xs ">
{form._count?.submissionSessions} responses
</p>
<Menu
as="div"
className="relative z-10 inline-block text-left"
>
<Menu as="div" className="relative z-10 inline-block text-left">
{({ open }) => (
<>
<div>
<Menu.Button className="flex items-center p-2 -m-2 rounded-full text-red">
<span className="sr-only">
Open options
</span>
<EllipsisHorizontalIcon
className="w-5 h-5"
aria-hidden="true"
/>
<Menu.Button className="text-red -m-2 flex items-center rounded-full p-2">
<span className="sr-only">Open options</span>
<EllipsisHorizontalIcon className="h-5 w-5" aria-hidden="true" />
</Menu.Button>
</div>
@@ -120,28 +106,23 @@ export default function FormList() {
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
leaveTo="transform opacity-0 scale-95">
<Menu.Items
static
className="absolute left-0 w-56 px-1 mt-2 origin-top-right bg-white rounded-sm shadow-lg"
>
className="absolute left-0 mt-2 w-56 origin-top-right rounded-sm bg-white px-1 shadow-lg">
<div className="py-1">
<Menu.Item>
{({ active }) => (
<button
onClick={() =>
deleteForm(form, formIdx)
}
onClick={() => deleteForm(form, formIdx)}
className={classNames(
active
? "bg-ui-gray-light rounded-sm text-ui-black"
? "bg-ui-gray-light text-ui-black rounded-sm"
: "text-ui-gray-dark",
"flex px-4 py-2 text-sm w-full"
)}
>
"flex w-full px-4 py-2 text-sm"
)}>
<TrashIcon
className="w-5 h-5 mr-3 text-ui-gray-dark"
className="text-ui-gray-dark mr-3 h-5 w-5"
aria-hidden="true"
/>
<span>Delete Form</span>

View File

@@ -2,13 +2,13 @@ import { TailSpin } from "react-loader-spinner";
export default function Loading() {
return (
<div className="min-h-screen px-4 py-16 bg-white sm:px-6 sm:py-24 md:grid md:place-items-center lg:px-8">
<div className="min-h-screen bg-white px-4 py-16 sm:px-6 sm:py-24 md:grid md:place-items-center lg:px-8">
<div className="mx-auto max-w-max">
<main>
<div className="flex justify-center">
<TailSpin color="#1f2937" height={30} width={30} />
</div>
<p className="mt-5 text-sm text-ui-gray-dark">Loading...</p>
<p className="text-ui-gray-dark mt-5 text-sm">Loading...</p>
</main>
</div>
</div>

View File

@@ -10,9 +10,8 @@ export default function LoadingModal({ isLoading }) {
static
className="fixed inset-0 z-10 overflow-y-auto"
open={isLoading}
onClose={() => {}}
>
<div className="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
onClose={() => {}}>
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
@@ -20,16 +19,12 @@ export default function LoadingModal({ isLoading }) {
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-20" />
leaveTo="opacity-0">
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-20 transition-opacity" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span
className="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true"
>
<span className="hidden sm:inline-block sm:h-screen sm:align-middle" aria-hidden="true">
&#8203;
</span>
<Transition.Child
@@ -39,9 +34,8 @@ export default function LoadingModal({ isLoading }) {
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div className="inline-flex items-center justify-center px-4 py-20 pb-4 overflow-hidden text-left align-bottom transition-all transform rounded-lg sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<div className="inline-flex transform items-center justify-center overflow-hidden rounded-lg px-4 py-20 pb-4 text-left align-bottom transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6 sm:align-middle">
<TailSpin color="#000" height={50} width={50} />
</div>
</Transition.Child>

View File

@@ -1,11 +1,9 @@
export default function MessagePage({ text }) {
return (
<div className="min-h-screen px-4 py-16 bg-white sm:px-6 sm:py-24 md:grid md:place-items-center lg:px-8">
<div className="min-h-screen bg-white px-4 py-16 sm:px-6 sm:py-24 md:grid md:place-items-center lg:px-8">
<div className="mx-auto max-w-max">
<main>
<div className="flex justify-center text-sm text-ui-gray-dark">
{text}
</div>
<div className="text-ui-gray-dark flex justify-center text-sm">{text}</div>
</main>
</div>
</div>

View File

@@ -14,13 +14,12 @@ export default function Modal({ open, setOpen, children }) {
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" />
leaveTo="opacity-0">
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex items-end justify-center min-h-full p-4 text-center sm:items-center sm:p-0">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
@@ -28,22 +27,18 @@ export default function Modal({ open, setOpen, children }) {
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative px-4 pt-5 pb-4 overflow-hidden text-left transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:max-w-xl lg:max-w-3xl sm:w-full sm:p-6">
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-xl sm:p-6 lg:max-w-3xl">
<div className="absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
<button
type="button"
className="text-gray-400 bg-white rounded-md hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
onClick={() => setOpen(false)}
>
className="rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
onClick={() => setOpen(false)}>
<span className="sr-only">Close</span>
<XMarkIcon className="w-6 h-6" aria-hidden="true" />
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
<div className="flex-col sm:flex sm:items-start">
{children}
</div>
<div className="flex-col sm:flex sm:items-start">{children}</div>
</Dialog.Panel>
</Transition.Child>
</div>

View File

@@ -26,7 +26,7 @@ const StandardButton: React.FC<Props> = ({
return (
<button
className={classNames(
`inline-flex items-center rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500`,
`inline-flex items-center rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2`,
disabled ? "disabled:opacity-50" : "",
fullwidth ? " w-full justify-center " : "",
small ? "px-2.5 py-1.5 text-xs" : "px-5 py-3 text-sm",
@@ -35,8 +35,7 @@ const StandardButton: React.FC<Props> = ({
)}
onClick={onClick}
disabled={disabled}
{...rest}
>
{...rest}>
{children}
</button>
);

View File

@@ -21,13 +21,13 @@ import SettingsModal from "./SettingsModal";
let Editor = dynamic(() => import("../editorjs/Editor"), {
ssr: false,
});
/* import Editor from "../editorjs/Editor"; */
export default function Builder({ formId }) {
const router = useRouter();
const editorRef = useRef<EditorJS | null>();
const { isLoadingForm } = useForm(formId);
const { noCodeForm, isLoadingNoCodeForm, mutateNoCodeForm } =
useNoCodeForm(formId);
const { noCodeForm, isLoadingNoCodeForm, mutateNoCodeForm } = useNoCodeForm(formId);
const [openShareModal, setOpenShareModal] = useState(false);
const [openSettingsModal, setOpenSettingsModal] = useState(false);
const [loading, setLoading] = useState(false);
@@ -37,9 +37,7 @@ export default function Builder({ formId }) {
submitLabel: "Submit",
});
const block = editorRef.current.blocks.insert("paragraph");
editorRef.current.caret.setToBlock(
editorRef.current.blocks.getBlockIndex(block.id)
);
editorRef.current.caret.setToBlock(editorRef.current.blocks.getBlockIndex(block.id));
};
const initAction = async (editor: EditorJS) => {
@@ -57,9 +55,7 @@ export default function Builder({ formId }) {
text: "Thanks a lot for your time and insights 🙏",
});
editor.blocks.delete(0); // remove defaultBlock
editor.caret.setToBlock(
editorRef.current.blocks.getBlockIndex(focusBlock.id)
);
editor.caret.setToBlock(editorRef.current.blocks.getBlockIndex(focusBlock.id));
};
const publishChanges = async () => {
@@ -72,11 +68,7 @@ export default function Builder({ formId }) {
await persistNoCodeForm(newNoCodeForm);
mutateNoCodeForm(newNoCodeForm);
setLoading(false);
toast(
firstPublish
? "Your form is now published 🎉"
: "Your changes are now published 🎉"
);
toast(firstPublish ? "Your form is now published 🎉" : "Your changes are now published 🎉");
}, 500);
};
@@ -123,8 +115,8 @@ export default function Builder({ formId }) {
return (
<>
<SecondNavBar navItems={noCodeSecondNavigation} />
<div className="w-full h-full mb-20 overflow-auto bg-white">
<div className="flex justify-center w-full pt-10 pb-56">
<div className="mb-20 h-full w-full overflow-auto bg-white">
<div className="flex w-full justify-center pt-10 pb-56">
<LimitedWidth>
{Editor && (
<Editor
@@ -138,16 +130,8 @@ export default function Builder({ formId }) {
</LimitedWidth>
</div>
</div>
<ShareModal
open={openShareModal}
setOpen={setOpenShareModal}
formId={formId}
/>
<SettingsModal
open={openSettingsModal}
setOpen={setOpenSettingsModal}
formId={formId}
/>
<ShareModal open={openShareModal} setOpen={setOpenShareModal} formId={formId} />
<SettingsModal open={openSettingsModal} setOpen={setOpenSettingsModal} formId={formId} />
<LoadingModal isLoading={loading} />
</>
);

View File

@@ -3,12 +3,7 @@ import { TrashIcon } from "@heroicons/react/24/solid";
import { MdWavingHand } from "react-icons/md";
import { classNames } from "../../lib/utils";
export default function PageToolbar({
page,
pageIdx,
deletePageAction,
setPageType,
}) {
export default function PageToolbar({ page, pageIdx, deletePageAction, setPageType }) {
return (
<div className="relative">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
@@ -29,14 +24,13 @@ export default function PageToolbar({
page.type === "thankyou"
? "bg-red-400 text-white hover:bg-red-500"
: "bg-white text-gray-400 hover:bg-gray-50",
"has-tooltip relative inline-flex items-center px-4 py-2 text-sm font-medium border border-gray-300 rounded-l-md focus:z-10 focus:outline-none focus:ring-1 focus:ring-red-500 focus:border-red-500"
)}
>
"has-tooltip relative inline-flex items-center rounded-l-md border border-gray-300 px-4 py-2 text-sm font-medium focus:z-10 focus:border-red-500 focus:outline-none focus:ring-1 focus:ring-red-500"
)}>
<span className="sr-only">Thank You Page</span>
<span className="w-32 p-1 -mt-16 -ml-10 text-xs text-white bg-gray-600 rounded shadow-lg tooltip">
<span className="tooltip -mt-16 -ml-10 w-32 rounded bg-gray-600 p-1 text-xs text-white shadow-lg">
Is Thank You Page
</span>
<MdWavingHand className="w-4 h-4" aria-hidden="true" />
<MdWavingHand className="h-4 w-4" aria-hidden="true" />
</button>
<button
type="button"
@@ -45,13 +39,12 @@ export default function PageToolbar({
deletePageAction(pageIdx);
}
}}
className="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-400 bg-white border border-gray-300 has-tooltip rounded-r-md hover:bg-gray-50 focus:z-10 focus:outline-none focus:ring-1 focus:ring-red-500 focus:border-red-500"
>
className="has-tooltip relative inline-flex items-center rounded-r-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-400 hover:bg-gray-50 focus:z-10 focus:border-red-500 focus:outline-none focus:ring-1 focus:ring-red-500">
<span className="sr-only">Delete</span>
<span className="w-24 p-1 -mt-16 -ml-8 text-xs text-white bg-gray-600 rounded shadow-lg tooltip">
<span className="tooltip -mt-16 -ml-8 w-24 rounded bg-gray-600 p-1 text-xs text-white shadow-lg">
Delete Page
</span>
<TrashIcon className="w-4 h-4" aria-hidden="true" />
<TrashIcon className="h-4 w-4" aria-hidden="true" />
</button>
</span>
</div>

View File

@@ -9,8 +9,7 @@ import { toast } from "react-toastify";
import { classNames } from "../../lib/utils";
export default function SettingsModal({ open, setOpen, formId }) {
const { noCodeForm, isLoadingNoCodeForm, mutateNoCodeForm } =
useNoCodeForm(formId);
const { noCodeForm, isLoadingNoCodeForm, mutateNoCodeForm } = useNoCodeForm(formId);
const [loading, setLoading] = useState(false);
const toggleClose = async () => {
@@ -43,13 +42,12 @@ export default function SettingsModal({ open, setOpen, formId }) {
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" />
leaveTo="opacity-0">
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex items-end justify-center min-h-full p-4 text-center sm:items-center sm:p-0">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
@@ -57,50 +55,32 @@ export default function SettingsModal({ open, setOpen, formId }) {
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative px-4 pt-5 pb-4 overflow-hidden text-left transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:max-w-4xl sm:w-full sm:p-6">
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-4xl sm:p-6">
<div className="absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
<button
type="button"
className="text-gray-400 bg-white rounded-md hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
onClick={() => setOpen(false)}
>
className="rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
onClick={() => setOpen(false)}>
<span className="sr-only">Close</span>
<XMarkIcon className="w-6 h-6" aria-hidden="true" />
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
<div className="px-4 py-5 sm:p-6">
<div className="mb-4">
<h1 className="text-2xl font-medium leading-6 text-gray-900">
Settings
</h1>
<h1 className="text-2xl font-medium leading-6 text-gray-900">Settings</h1>
</div>
<h3 className="text-lg font-medium leading-6 text-gray-900">
Access
</h3>
<div className="w-full mt-2 text-sm text-gray-500">
<Switch.Group
as="div"
className="flex items-center justify-between w-full"
>
<span className="flex flex-col flex-grow">
<Switch.Label
as="span"
className="text-sm font-medium text-gray-900"
passive={true}
>
<h3 className="text-lg font-medium leading-6 text-gray-900">Access</h3>
<div className="mt-2 w-full text-sm text-gray-500">
<Switch.Group as="div" className="flex w-full items-center justify-between">
<span className="flex flex-grow flex-col">
<Switch.Label as="span" className="text-sm font-medium text-gray-900" passive={true}>
Close form for new submissions?
</Switch.Label>
<Switch.Description
as="span"
className="text-sm text-gray-500"
>
<Switch.Description as="span" className="text-sm text-gray-500">
Your form is currently{" "}
<span className="font-bold">
{noCodeForm.closed ? "closed" : "open"}
</span>{" "}
for submissions.
<span className="font-bold">{noCodeForm.closed ? "closed" : "open"}</span> for
submissions.
</Switch.Description>
</span>
{loading ? (
@@ -111,16 +91,13 @@ export default function SettingsModal({ open, setOpen, formId }) {
onChange={() => toggleClose()}
className={classNames(
noCodeForm.closed ? "bg-red-600" : "bg-gray-200",
"relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
)}
>
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
)}>
<span
aria-hidden="true"
className={classNames(
noCodeForm.closed
? "translate-x-5"
: "translate-x-0",
"pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
noCodeForm.closed ? "translate-x-5" : "translate-x-0",
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
)}
/>
</Switch>

View File

@@ -29,13 +29,12 @@ export default function ShareModal({ open, setOpen, formId }) {
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" />
leaveTo="opacity-0">
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex items-end justify-center min-h-full p-4 text-center sm:items-center sm:p-0">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
@@ -43,47 +42,36 @@ export default function ShareModal({ open, setOpen, formId }) {
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative px-4 pt-5 pb-4 overflow-hidden text-left transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:max-w-4xl sm:w-full sm:p-6">
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-4xl sm:p-6">
<div className="absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
<button
type="button"
className="text-gray-400 bg-white rounded-md hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
onClick={() => setOpen(false)}
>
className="rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
onClick={() => setOpen(false)}>
<span className="sr-only">Close</span>
<XMarkIcon className="w-6 h-6" aria-hidden="true" />
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
{!noCodeForm.published ? (
<div className="p-4 border border-gray-700 rounded-md bg-ui-gray-light">
<div className="bg-ui-gray-light rounded-md border border-gray-700 p-4">
<div className="flex">
<div className="flex-shrink-0">
<InformationCircleIcon
className="w-5 h-5 text-blue-400"
aria-hidden="true"
/>
<InformationCircleIcon className="h-5 w-5 text-blue-400" aria-hidden="true" />
</div>
<div className="flex-1 ml-3 md:flex md:justify-between">
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm text-gray-700">
You haven&apos;t published this form yet. Please
publish this form to share it with others and get the
first submissions.
You haven&apos;t published this form yet. Please publish this form to share it with
others and get the first submissions.
</p>
</div>
</div>
</div>
) : (
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg font-medium leading-6 text-gray-900">
Share your form
</h3>
<div className="max-w-xl mt-2 text-sm text-gray-500">
<p>
Let your participants fill out your form by accessing it
via the public link.
</p>
<h3 className="text-lg font-medium leading-6 text-gray-900">Share your form</h3>
<div className="mt-2 max-w-xl text-sm text-gray-500">
<p>Let your participants fill out your form by accessing it via the public link.</p>
</div>
<div className="mt-5 sm:flex sm:items-center">
<div className="w-full sm:max-w-xs">
@@ -94,7 +82,7 @@ export default function ShareModal({ open, setOpen, formId }) {
id="surveyLink"
type="text"
placeholder="Enter your email"
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 sm:text-sm"
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-red-500 focus:ring-red-500 sm:text-sm"
value={getPublicFormUrl()}
disabled
/>
@@ -104,8 +92,7 @@ export default function ShareModal({ open, setOpen, formId }) {
navigator.clipboard.writeText(getPublicFormUrl());
toast("Link copied to clipboard 🙌");
}}
className="inline-flex items-center justify-center w-full px-4 py-2 mt-3 font-medium text-white bg-gray-800 border border-transparent rounded-md shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
>
className="mt-3 inline-flex w-full items-center justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 font-medium text-white shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
Copy
</button>
</div>

View File

@@ -25,15 +25,8 @@ interface EditorProps {
initAction: (editor: EditorJS) => void;
}
const Editor = ({
id,
autofocus = false,
editorRef,
formId,
initAction,
}: EditorProps) => {
const { noCodeForm, isLoadingNoCodeForm, mutateNoCodeForm } =
useNoCodeForm(formId);
const Editor = ({ id, autofocus = false, editorRef, formId, initAction }: EditorProps) => {
const { noCodeForm, isLoadingNoCodeForm, mutateNoCodeForm } = useNoCodeForm(formId);
const keyPressListener = useCallback((e) => {
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
@@ -102,8 +95,7 @@ const Editor = ({
class: Paragraph,
inlineToolbar: true,
config: {
placeholder:
"Start with your content or hit tab-key to insert block",
placeholder: "Start with your content or hit tab-key to insert block",
},
},
header: {

View File

@@ -25,13 +25,7 @@ export default class EmailQuestion implements BlockTool {
};
}
constructor({
data,
}: {
api: API;
config?: ToolConfig;
data?: EmailQuestionData;
}) {
constructor({ data }: { api: API; config?: ToolConfig; data?: EmailQuestionData }) {
this.wrapper = undefined;
this.settings = [
{
@@ -50,16 +44,11 @@ export default class EmailQuestion implements BlockTool {
save(block: HTMLDivElement) {
return {
...this.data,
label: (
block.firstElementChild.firstElementChild
.firstElementChild as HTMLInputElement
).value,
label: (block.firstElementChild.firstElementChild.firstElementChild as HTMLInputElement).value,
placeholder: (
(block.firstElementChild.childNodes[1] as HTMLInputElement)
.lastElementChild as HTMLInputElement
(block.firstElementChild.childNodes[1] as HTMLInputElement).lastElementChild as HTMLInputElement
).value,
help: (block.firstElementChild.lastElementChild as HTMLInputElement)
.value,
help: (block.firstElementChild.lastElementChild as HTMLInputElement).value,
};
}
@@ -70,10 +59,7 @@ export default class EmailQuestion implements BlockTool {
let button = document.createElement("div");
button.classList.add("cdx-settings-button");
button.classList.toggle(
"cdx-settings-button--active",
this.data[tune.name]
);
button.classList.toggle("cdx-settings-button--active", this.data[tune.name]);
button.innerHTML = tune.icon;
wrapper.appendChild(button);
@@ -96,10 +82,7 @@ export default class EmailQuestion implements BlockTool {
if (tune === "required") {
this.data.required = !this.data.required;
this.wrapper.childNodes[0].childNodes[0].childNodes[1].textContent = this
.data.required
? "*"
: "";
this.wrapper.childNodes[0].childNodes[0].childNodes[1].textContent = this.data.required ? "*" : "";
}
}
@@ -107,29 +90,26 @@ export default class EmailQuestion implements BlockTool {
this.wrapper = document.createElement("div");
const toolView = (
<div className="pb-5">
<div className="relative font-bold leading-7 text-gray-800 text-md sm:truncate">
<div className="text-md relative font-bold leading-7 text-gray-800 sm:truncate">
<input
type="text"
id="label"
defaultValue={this.data.label}
className="w-full p-0 border-0 border-transparent ring-0 focus:ring-0 placeholder:text-gray-300"
className="w-full border-0 border-transparent p-0 ring-0 placeholder:text-gray-300 focus:ring-0"
placeholder="Your Question"
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-red-500 pointer-events-none">
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-red-500">
*
</div>
</div>
<div className="relative max-w-sm mt-1 rounded-md shadow-sm">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<EnvelopeIcon
className="w-5 h-5 text-gray-400"
aria-hidden="true"
/>
<div className="relative mt-1 max-w-sm rounded-md shadow-sm">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<EnvelopeIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
type="text"
name="email"
className="block w-full pl-10 text-gray-300 border-gray-300 rounded-md sm:text-sm"
className="block w-full rounded-md border-gray-300 pl-10 text-gray-300 sm:text-sm"
defaultValue={this.data.placeholder}
/>
</div>
@@ -137,7 +117,7 @@ export default class EmailQuestion implements BlockTool {
type="text"
id="help-text"
defaultValue={this.data.help}
className="block w-full max-w-sm p-0 mt-2 text-sm font-light text-gray-500 border-0 border-transparent ring-0 focus:ring-0 placeholder:text-gray-300"
className="mt-2 block w-full max-w-sm border-0 border-transparent p-0 text-sm font-light text-gray-500 ring-0 placeholder:text-gray-300 focus:ring-0"
placeholder="optional help text"
/>
</div>

View File

@@ -74,10 +74,7 @@ export default class MultipleChoiceQuestion implements BlockTool {
let button = document.createElement("div");
button.classList.add("cdx-settings-button");
button.classList.toggle(
"cdx-settings-button--active",
this.data[tune.name]
);
button.classList.toggle("cdx-settings-button--active", this.data[tune.name]);
button.innerHTML = tune.icon;
wrapper.appendChild(button);

View File

@@ -74,50 +74,43 @@ const SingleChoiceQuestion = (props) => {
return (
<div className="pb-5">
<div className="relative font-bold leading-7 text-gray-800 text-md sm:truncate">
<div className="text-md relative font-bold leading-7 text-gray-800 sm:truncate">
<input
type="text"
id="label"
defaultValue={choiceData.label}
onBlur={onInputChange("label")}
className="w-full p-0 border-0 border-transparent ring-0 focus:ring-0 placeholder:text-gray-300"
className="w-full border-0 border-transparent p-0 ring-0 placeholder:text-gray-300 focus:ring-0"
placeholder="Your Question"
/>
{choiceData.required && (
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-red-500 pointer-events-none">
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-red-500">
*
</div>
)}
</div>
<div className="mt-2 space-y-2">
{choiceData.options.map((option, optionIdx) => (
<div
key={option.label}
className="relative flex items-start pr-2 hover:bg-gray-50 hover:rounded"
>
<span className="flex items-center w-full text-sm ">
<div key={option.label} className="relative flex items-start pr-2 hover:rounded hover:bg-gray-50">
<span className="flex w-full items-center text-sm ">
<span
className={classNames(
choiceData.multipleChoice ? "rounded-sm" : "rounded-full",
"flex items-center justify-center w-4 h-4 border border-gray-300"
"flex h-4 w-4 items-center justify-center border border-gray-300"
)}
aria-hidden="true"
>
<span className="rounded-full w-1.5 h-1.5" />
aria-hidden="true">
<span className="h-1.5 w-1.5 rounded-full" />
</span>
<input
type="text"
defaultValue={option.label}
onBlur={onOptionChange(optionIdx, "label")}
className="w-full p-0 ml-3 font-medium text-gray-900 bg-transparent border-0 border-transparent outline-none focus:ring-0 focus:outline-none placeholder:text-gray-300"
className="ml-3 w-full border-0 border-transparent bg-transparent p-0 font-medium text-gray-900 outline-none placeholder:text-gray-300 focus:outline-none focus:ring-0"
placeholder={`Option ${optionIdx + 1}`}
/>
{optionIdx !== 0 && (
<button
onClick={() => onDeleteOption(optionIdx)}
className="pl-4 right-3"
>
<TrashIcon className="w-4 h-4 text-gray-300" />
<button onClick={() => onDeleteOption(optionIdx)} className="right-3 pl-4">
<TrashIcon className="h-4 w-4 text-gray-300" />
</button>
)}
</span>
@@ -129,14 +122,13 @@ const SingleChoiceQuestion = (props) => {
id="help-text"
defaultValue={choiceData.help}
onBlur={onInputChange("help")}
className="block w-full max-w-sm p-0 mt-2 text-sm font-light text-gray-500 border-0 border-transparent ring-0 focus:ring-0 placeholder:text-gray-300"
className="mt-2 block w-full max-w-sm border-0 border-transparent p-0 text-sm font-light text-gray-500 ring-0 placeholder:text-gray-300 focus:ring-0"
placeholder="optional help text"
/>
<div className="relative z-0 flex mt-2 divide-x divide-gray-200">
<div className="relative z-0 mt-2 flex divide-x divide-gray-200">
<button
className="mr-3 justify-center mt-2 inline-flex items-center px-2.5 py-1.5 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none"
onClick={onAddOption}
>
className="mr-3 mt-2 inline-flex items-center justify-center rounded border border-gray-300 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none"
onClick={onAddOption}>
Add option
</button>
<Switch.Group as="div" className="flex items-center pl-3">
@@ -151,21 +143,18 @@ const SingleChoiceQuestion = (props) => {
}}
className={classNames(
choiceData.multipleChoice ? "bg-red-600" : "bg-gray-200",
"relative inline-flex flex-shrink-0 h-4 w-7 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
)}
>
"relative inline-flex h-4 w-7 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
)}>
<span
aria-hidden="true"
className={classNames(
choiceData.multipleChoice ? "translate-x-3" : "translate-x-0",
"pointer-events-none inline-block h-3 w-3 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
"pointer-events-none inline-block h-3 w-3 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
)}
/>
</Switch>
<Switch.Label as="span" className="ml-3">
<span className="text-sm font-medium text-gray-700">
Multiple Selection{" "}
</span>
<span className="text-sm font-medium text-gray-700">Multiple Selection </span>
{/* <span className="text-sm text-gray-500">(Save 10%)</span> */}
</Switch.Label>
</Switch.Group>

View File

@@ -24,13 +24,7 @@ export default class NumberQuestion implements BlockTool {
};
}
constructor({
data,
}: {
api: API;
config?: ToolConfig;
data?: NumberQuestionData;
}) {
constructor({ data }: { api: API; config?: ToolConfig; data?: NumberQuestionData }) {
this.wrapper = undefined;
this.settings = [
{
@@ -49,14 +43,9 @@ export default class NumberQuestion implements BlockTool {
save(block: HTMLDivElement) {
return {
...this.data,
label: (
block.firstElementChild.firstElementChild
.firstElementChild as HTMLInputElement
).value,
placeholder: (block.firstElementChild.childNodes[1] as HTMLInputElement)
.value,
help: (block.firstElementChild.lastElementChild as HTMLInputElement)
.value,
label: (block.firstElementChild.firstElementChild.firstElementChild as HTMLInputElement).value,
placeholder: (block.firstElementChild.childNodes[1] as HTMLInputElement).value,
help: (block.firstElementChild.lastElementChild as HTMLInputElement).value,
};
}
@@ -67,10 +56,7 @@ export default class NumberQuestion implements BlockTool {
let button = document.createElement("div");
button.classList.add("cdx-settings-button");
button.classList.toggle(
"cdx-settings-button--active",
this.data[tune.name]
);
button.classList.toggle("cdx-settings-button--active", this.data[tune.name]);
button.innerHTML = tune.icon;
wrapper.appendChild(button);
@@ -93,10 +79,7 @@ export default class NumberQuestion implements BlockTool {
if (tune === "required") {
this.data.required = !this.data.required;
this.wrapper.childNodes[0].childNodes[0].childNodes[1].textContent = this
.data.required
? "*"
: "";
this.wrapper.childNodes[0].childNodes[0].childNodes[1].textContent = this.data.required ? "*" : "";
}
}
@@ -104,21 +87,21 @@ export default class NumberQuestion implements BlockTool {
this.wrapper = document.createElement("div");
const toolView = (
<div className="pb-5">
<div className="relative font-bold leading-7 text-gray-800 text-md sm:truncate">
<div className="text-md relative font-bold leading-7 text-gray-800 sm:truncate">
<input
type="text"
id="label"
defaultValue={this.data.label}
className="w-full p-0 border-0 border-transparent ring-0 focus:ring-0 placeholder:text-gray-300"
className="w-full border-0 border-transparent p-0 ring-0 placeholder:text-gray-300 focus:ring-0"
placeholder="Your Question"
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-red-500 pointer-events-none">
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-red-500">
*
</div>
</div>
<input
type="text"
className="block w-full max-w-sm mt-1 text-sm text-gray-400 border-gray-300 rounded-md shadow-sm placeholder:text-gray-300"
className="mt-1 block w-full max-w-sm rounded-md border-gray-300 text-sm text-gray-400 shadow-sm placeholder:text-gray-300"
placeholder="optional placeholder"
defaultValue={this.data.placeholder}
/>
@@ -126,7 +109,7 @@ export default class NumberQuestion implements BlockTool {
type="text"
id="help-text"
defaultValue={this.data.help}
className="block w-full max-w-sm p-0 mt-2 text-sm font-light text-gray-500 border-0 border-transparent ring-0 focus:ring-0 placeholder:text-gray-300"
className="mt-2 block w-full max-w-sm border-0 border-transparent p-0 text-sm font-light text-gray-500 ring-0 placeholder:text-gray-300 focus:ring-0"
placeholder="optional help text"
/>
</div>

View File

@@ -18,24 +18,14 @@ export default class PageTransition implements BlockTool {
};
} */
constructor({
data,
api,
}: {
api: API;
config?: ToolConfig;
data?: PageTransitionData;
}) {
constructor({ data, api }: { api: API; config?: ToolConfig; data?: PageTransitionData }) {
this.api = api;
this.submitLabel = data.submitLabel || "Submit";
}
save(block: HTMLDivElement) {
return {
submitLabel: (
block.firstElementChild.firstElementChild
.firstElementChild as HTMLElement
).innerHTML,
submitLabel: (block.firstElementChild.firstElementChild.firstElementChild as HTMLElement).innerHTML,
};
}
@@ -43,28 +33,22 @@ export default class PageTransition implements BlockTool {
const container = document.createElement("div");
const toolView = (
<div className="relative mt-16 mb-8">
<div className="absolute inline-flex items-center px-4 py-3 text-sm font-medium text-white bg-gray-700 border border-transparent rounded-md shadow-sm left -top-14 hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
<div className="left absolute -top-14 inline-flex items-center rounded-md border border-transparent bg-gray-700 px-4 py-3 text-sm font-medium text-white shadow-sm hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2">
<div
contentEditable
id="label"
defaultValue={this.submitLabel}
className="p-0 bg-transparent border-transparent ring-0 active:ring-0 focus:border-transparent focus:ring-0 focus:outline-none placeholder:text-opacity-5"
>
className="border-transparent bg-transparent p-0 ring-0 placeholder:text-opacity-5 focus:border-transparent focus:outline-none focus:ring-0 active:ring-0">
{this.submitLabel}
</div>
{/* <ArrowRightIcon className="w-5 h-5 ml-2 -mr-1" aria-hidden="true" /> */}
</div>
<div className="relative my-4">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center">
<span className="px-2 text-sm text-gray-500 bg-white">
Next Page
</span>
<span className="bg-white px-2 text-sm text-gray-500">Next Page</span>
</div>
</div>
</div>

View File

@@ -25,13 +25,7 @@ export default class PhoneQuestion implements BlockTool {
};
}
constructor({
data,
}: {
api: API;
config?: ToolConfig;
data?: PhoneQuestionData;
}) {
constructor({ data }: { api: API; config?: ToolConfig; data?: PhoneQuestionData }) {
this.wrapper = undefined;
this.settings = [
{
@@ -50,16 +44,11 @@ export default class PhoneQuestion implements BlockTool {
save(block: HTMLDivElement) {
return {
...this.data,
label: (
block.firstElementChild.firstElementChild
.firstElementChild as HTMLInputElement
).value,
label: (block.firstElementChild.firstElementChild.firstElementChild as HTMLInputElement).value,
placeholder: (
(block.firstElementChild.childNodes[1] as HTMLInputElement)
.lastElementChild as HTMLInputElement
(block.firstElementChild.childNodes[1] as HTMLInputElement).lastElementChild as HTMLInputElement
).value,
help: (block.firstElementChild.lastElementChild as HTMLInputElement)
.value,
help: (block.firstElementChild.lastElementChild as HTMLInputElement).value,
};
}
@@ -70,10 +59,7 @@ export default class PhoneQuestion implements BlockTool {
let button = document.createElement("div");
button.classList.add("cdx-settings-button");
button.classList.toggle(
"cdx-settings-button--active",
this.data[tune.name]
);
button.classList.toggle("cdx-settings-button--active", this.data[tune.name]);
button.innerHTML = tune.icon;
wrapper.appendChild(button);
@@ -96,10 +82,7 @@ export default class PhoneQuestion implements BlockTool {
if (tune === "required") {
this.data.required = !this.data.required;
this.wrapper.childNodes[0].childNodes[0].childNodes[1].textContent = this
.data.required
? "*"
: "";
this.wrapper.childNodes[0].childNodes[0].childNodes[1].textContent = this.data.required ? "*" : "";
}
}
@@ -107,26 +90,26 @@ export default class PhoneQuestion implements BlockTool {
this.wrapper = document.createElement("div");
const toolView = (
<div className="pb-5">
<div className="relative font-bold leading-7 text-gray-800 text-md sm:truncate">
<div className="text-md relative font-bold leading-7 text-gray-800 sm:truncate">
<input
type="text"
id="label"
defaultValue={this.data.label}
className="w-full p-0 border-0 border-transparent ring-0 focus:ring-0 placeholder:text-gray-300"
className="w-full border-0 border-transparent p-0 ring-0 placeholder:text-gray-300 focus:ring-0"
placeholder="Your Question"
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-red-500 pointer-events-none">
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-red-500">
*
</div>
</div>
<div className="relative max-w-sm mt-1 rounded-md shadow-sm">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<PhoneIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
<div className="relative mt-1 max-w-sm rounded-md shadow-sm">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<PhoneIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
type="text"
name="website"
className="block w-full pl-10 text-gray-300 border-gray-300 rounded-md sm:text-sm"
className="block w-full rounded-md border-gray-300 pl-10 text-gray-300 sm:text-sm"
defaultValue={this.data.placeholder}
/>
</div>
@@ -134,7 +117,7 @@ export default class PhoneQuestion implements BlockTool {
type="text"
id="help-text"
defaultValue={this.data.help}
className="block w-full max-w-sm p-0 mt-2 text-sm font-light text-gray-500 border-0 border-transparent ring-0 focus:ring-0 placeholder:text-gray-300"
className="mt-2 block w-full max-w-sm border-0 border-transparent p-0 text-sm font-light text-gray-500 ring-0 placeholder:text-gray-300 focus:ring-0"
placeholder="optional help text"
/>
</div>

View File

@@ -22,13 +22,7 @@ export default class TextQuestion implements BlockTool {
};
}
constructor({
data,
}: {
api: API;
config?: ToolConfig;
data?: TextQuestionData;
}) {
constructor({ data }: { api: API; config?: ToolConfig; data?: TextQuestionData }) {
this.wrapper = undefined;
this.settings = [
{
@@ -47,14 +41,9 @@ export default class TextQuestion implements BlockTool {
save(block: HTMLDivElement) {
return {
...this.data,
label: (
block.firstElementChild.firstElementChild
.firstElementChild as HTMLInputElement
).value,
placeholder: (block.firstElementChild.childNodes[1] as HTMLInputElement)
.value,
help: (block.firstElementChild.lastElementChild as HTMLInputElement)
.value,
label: (block.firstElementChild.firstElementChild.firstElementChild as HTMLInputElement).value,
placeholder: (block.firstElementChild.childNodes[1] as HTMLInputElement).value,
help: (block.firstElementChild.lastElementChild as HTMLInputElement).value,
};
}
@@ -65,10 +54,7 @@ export default class TextQuestion implements BlockTool {
let button = document.createElement("div");
button.classList.add("cdx-settings-button");
button.classList.toggle(
"cdx-settings-button--active",
this.data[tune.name]
);
button.classList.toggle("cdx-settings-button--active", this.data[tune.name]);
button.innerHTML = tune.icon;
wrapper.appendChild(button);
@@ -91,10 +77,7 @@ export default class TextQuestion implements BlockTool {
if (tune === "required") {
this.data.required = !this.data.required;
this.wrapper.childNodes[0].childNodes[0].childNodes[1].textContent = this
.data.required
? "*"
: "";
this.wrapper.childNodes[0].childNodes[0].childNodes[1].textContent = this.data.required ? "*" : "";
}
}
@@ -102,21 +85,21 @@ export default class TextQuestion implements BlockTool {
this.wrapper = document.createElement("div");
const toolView = (
<div className="pb-5">
<div className="relative font-bold leading-7 text-gray-800 text-md sm:truncate">
<div className="text-md relative font-bold leading-7 text-gray-800 sm:truncate">
<input
type="text"
id="label"
defaultValue={this.data.label}
className="w-full p-0 border-0 border-transparent ring-0 focus:ring-0 placeholder:text-gray-300"
className="w-full border-0 border-transparent p-0 ring-0 placeholder:text-gray-300 focus:ring-0"
placeholder="Your Question"
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-red-500 pointer-events-none">
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-red-500">
*
</div>
</div>
<input
type="text"
className="block w-full max-w-sm mt-1 text-sm text-gray-400 border-gray-300 rounded-md shadow-sm placeholder:text-gray-300"
className="mt-1 block w-full max-w-sm rounded-md border-gray-300 text-sm text-gray-400 shadow-sm placeholder:text-gray-300"
placeholder="optional placeholder"
defaultValue={this.data.placeholder}
/>
@@ -124,7 +107,7 @@ export default class TextQuestion implements BlockTool {
type="text"
id="help-text"
defaultValue={this.data.help}
className="block w-full max-w-sm p-0 mt-2 text-sm font-light text-gray-500 border-0 border-transparent ring-0 focus:ring-0 placeholder:text-gray-300"
className="mt-2 block w-full max-w-sm border-0 border-transparent p-0 text-sm font-light text-gray-500 ring-0 placeholder:text-gray-300 focus:ring-0"
placeholder="optional help text"
/>
</div>

View File

@@ -22,13 +22,7 @@ export default class TextareaQuestion implements BlockTool {
};
}
constructor({
data,
}: {
api: API;
config?: ToolConfig;
data?: TextareaQuestionData;
}) {
constructor({ data }: { api: API; config?: ToolConfig; data?: TextareaQuestionData }) {
this.wrapper = undefined;
this.settings = [
{
@@ -47,14 +41,9 @@ export default class TextareaQuestion implements BlockTool {
save(block: HTMLDivElement) {
return {
...this.data,
label: (
block.firstElementChild.firstElementChild
.firstElementChild as HTMLInputElement
).value,
placeholder: (block.firstElementChild.childNodes[1] as HTMLInputElement)
.value,
help: (block.firstElementChild.lastElementChild as HTMLInputElement)
.value,
label: (block.firstElementChild.firstElementChild.firstElementChild as HTMLInputElement).value,
placeholder: (block.firstElementChild.childNodes[1] as HTMLInputElement).value,
help: (block.firstElementChild.lastElementChild as HTMLInputElement).value,
};
}
@@ -65,10 +54,7 @@ export default class TextareaQuestion implements BlockTool {
let button = document.createElement("div");
button.classList.add("cdx-settings-button");
button.classList.toggle(
"cdx-settings-button--active",
this.data[tune.name]
);
button.classList.toggle("cdx-settings-button--active", this.data[tune.name]);
button.innerHTML = tune.icon;
wrapper.appendChild(button);
@@ -91,10 +77,7 @@ export default class TextareaQuestion implements BlockTool {
if (tune === "required") {
this.data.required = !this.data.required;
this.wrapper.childNodes[0].childNodes[0].childNodes[1].textContent = this
.data.required
? "*"
: "";
this.wrapper.childNodes[0].childNodes[0].childNodes[1].textContent = this.data.required ? "*" : "";
}
}
@@ -102,21 +85,21 @@ export default class TextareaQuestion implements BlockTool {
this.wrapper = document.createElement("div");
const toolView = (
<div className="pb-5">
<div className="relative font-bold leading-7 text-gray-800 text-md sm:truncate">
<div className="text-md relative font-bold leading-7 text-gray-800 sm:truncate">
<input
type="text"
id="label"
defaultValue={this.data.label}
className="w-full p-0 border-0 border-transparent ring-0 focus:ring-0 placeholder:text-gray-300"
className="w-full border-0 border-transparent p-0 ring-0 placeholder:text-gray-300 focus:ring-0"
placeholder="Your Question"
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-red-500 pointer-events-none">
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-red-500">
*
</div>
</div>
<textarea
rows={4}
className="block w-full max-w-sm mt-1 text-sm text-gray-400 border-gray-300 rounded-md shadow-sm placeholder:text-gray-300"
className="mt-1 block w-full max-w-sm rounded-md border-gray-300 text-sm text-gray-400 shadow-sm placeholder:text-gray-300"
placeholder="optional placeholder"
defaultValue={this.data.placeholder}
/>
@@ -124,7 +107,7 @@ export default class TextareaQuestion implements BlockTool {
type="text"
id="help-text"
defaultValue={this.data.help}
className="block w-full max-w-sm p-0 mt-2 text-sm font-light text-gray-500 border-0 border-transparent ring-0 focus:ring-0 placeholder:text-gray-300"
className="mt-2 block w-full max-w-sm border-0 border-transparent p-0 text-sm font-light text-gray-500 ring-0 placeholder:text-gray-300 focus:ring-0"
placeholder="optional help text"
/>
</div>

View File

@@ -25,13 +25,7 @@ export default class WebsiteQuestion implements BlockTool {
};
}
constructor({
data,
}: {
api: API;
config?: ToolConfig;
data?: WebsiteQuestionData;
}) {
constructor({ data }: { api: API; config?: ToolConfig; data?: WebsiteQuestionData }) {
this.wrapper = undefined;
this.settings = [
{
@@ -50,16 +44,11 @@ export default class WebsiteQuestion implements BlockTool {
save(block: HTMLDivElement) {
return {
...this.data,
label: (
block.firstElementChild.firstElementChild
.firstElementChild as HTMLInputElement
).value,
label: (block.firstElementChild.firstElementChild.firstElementChild as HTMLInputElement).value,
placeholder: (
(block.firstElementChild.childNodes[1] as HTMLInputElement)
.lastElementChild as HTMLInputElement
(block.firstElementChild.childNodes[1] as HTMLInputElement).lastElementChild as HTMLInputElement
).value,
help: (block.firstElementChild.lastElementChild as HTMLInputElement)
.value,
help: (block.firstElementChild.lastElementChild as HTMLInputElement).value,
};
}
@@ -70,10 +59,7 @@ export default class WebsiteQuestion implements BlockTool {
let button = document.createElement("div");
button.classList.add("cdx-settings-button");
button.classList.toggle(
"cdx-settings-button--active",
this.data[tune.name]
);
button.classList.toggle("cdx-settings-button--active", this.data[tune.name]);
button.innerHTML = tune.icon;
wrapper.appendChild(button);
@@ -96,10 +82,7 @@ export default class WebsiteQuestion implements BlockTool {
if (tune === "required") {
this.data.required = !this.data.required;
this.wrapper.childNodes[0].childNodes[0].childNodes[1].textContent = this
.data.required
? "*"
: "";
this.wrapper.childNodes[0].childNodes[0].childNodes[1].textContent = this.data.required ? "*" : "";
}
}
@@ -107,29 +90,26 @@ export default class WebsiteQuestion implements BlockTool {
this.wrapper = document.createElement("div");
const toolView = (
<div className="pb-5">
<div className="relative font-bold leading-7 text-gray-800 text-md sm:truncate">
<div className="text-md relative font-bold leading-7 text-gray-800 sm:truncate">
<input
type="text"
id="label"
defaultValue={this.data.label}
className="w-full p-0 border-0 border-transparent ring-0 focus:ring-0 placeholder:text-gray-300"
className="w-full border-0 border-transparent p-0 ring-0 placeholder:text-gray-300 focus:ring-0"
placeholder="Your Question"
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-red-500 pointer-events-none">
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-red-500">
*
</div>
</div>
<div className="relative max-w-sm mt-1 rounded-md shadow-sm">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<GlobeAltIcon
className="w-5 h-5 text-gray-400"
aria-hidden="true"
/>
<div className="relative mt-1 max-w-sm rounded-md shadow-sm">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<GlobeAltIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
type="text"
name="website"
className="block w-full pl-10 text-gray-300 border-gray-300 rounded-md sm:text-sm"
className="block w-full rounded-md border-gray-300 pl-10 text-gray-300 sm:text-sm"
defaultValue={this.data.placeholder}
/>
</div>
@@ -137,7 +117,7 @@ export default class WebsiteQuestion implements BlockTool {
type="text"
id="help-text"
defaultValue={this.data.help}
className="block w-full max-w-sm p-0 mt-2 text-sm font-light text-gray-500 border-0 border-transparent ring-0 focus:ring-0 placeholder:text-gray-300"
className="mt-2 block w-full max-w-sm border-0 border-transparent p-0 text-sm font-light text-gray-500 ring-0 placeholder:text-gray-300 focus:ring-0"
placeholder="optional help text"
/>
</div>

View File

@@ -44,26 +44,24 @@ export default function FormCode({ formId }) {
return (
<>
<div className="mx-auto mt-8">
<h1 className="text-3xl font-bold leading-tight text-ui-gray-dark">
Connect your form
</h1>
<h1 className="text-ui-gray-dark text-3xl font-bold leading-tight">Connect your form</h1>
</div>
<div className="mt-4 mb-12">
<p className="text-ui-gray-dark">
To send all form submissions to this dashboard, update the form ID in
the <code>{"<snoopForm>"}</code> component.
To send all form submissions to this dashboard, update the form ID in the{" "}
<code>{"<snoopForm>"}</code> component.
</p>
</div>
<div className="grid grid-cols-2 gap-10">
<div>
<label htmlFor="formId" className="block text-base text-ui-gray-dark">
<label htmlFor="formId" className="text-ui-gray-dark block text-base">
Your form ID
</label>
<div className="mt-3">
<input
id="formId"
type="text"
className="w-full mb-3 border-gray-300 rounded-sm shadow-sm text-md disabled:bg-gray-100"
className="text-md mb-3 w-full rounded-sm border-gray-300 shadow-sm disabled:bg-gray-100"
value={formId}
disabled
/>
@@ -73,13 +71,12 @@ export default function FormCode({ formId }) {
navigator.clipboard.writeText(formId);
toast("Copied form ID to clipboard");
}}
fullwidth
>
fullwidth>
copy
</StandardButton>
</div>
</div>
<div className="p-8 font-light text-gray-200 bg-black rounded-md">
<div className="rounded-md bg-black p-8 font-light text-gray-200">
<p>
<code>
{"<"}
@@ -91,10 +88,7 @@ export default function FormCode({ formId }) {
<code>{`domain="${window?.location.host}"`}</code>
</p>
<p>
<code>{`protocol="${window?.location.protocol.replace(
":",
""
)}"`}</code>
<code>{`protocol="${window?.location.protocol.replace(":", "")}"`}</code>
</p>
<p>
<code>{`formId="${formId}"`}</code>
@@ -115,57 +109,42 @@ export default function FormCode({ formId }) {
</div>
</div>
<div className="mt-16">
<h2 className="text-xl font-bold text-ui-gray-dark">Code your form</h2>
<h2 className="text-ui-gray-dark text-xl font-bold">Code your form</h2>
<div className="mt-4 mb-12">
<p className="text-ui-gray-dark">
Build your form with the code library of your choice. Manage your
data in this dashboard.
Build your form with the code library of your choice. Manage your data in this dashboard.
</p>
</div>
<ul
role="list"
className="grid grid-cols-1 gap-5 mt-3 sm:gap-6 sm:grid-cols-2"
>
<ul role="list" className="mt-3 grid grid-cols-1 gap-5 sm:grid-cols-2 sm:gap-6">
{libs.map((lib) => (
<Link key={lib.id} href={lib.href}>
<a
className="flex col-span-1 rounded-md shadow-sm"
target={lib.target || ""}
rel="noreferrer"
>
<a className="col-span-1 flex rounded-md shadow-sm" target={lib.target || ""} rel="noreferrer">
<li
className={classNames(
lib.comingSoon
? "text-ui-gray-medium"
: "shadow-sm text-ui-gray-dark hover:text-black",
"flex col-span-1 rounded-md w-full"
)}
>
lib.comingSoon ? "text-ui-gray-medium" : "text-ui-gray-dark shadow-sm hover:text-black",
"col-span-1 flex w-full rounded-md"
)}>
<div
className={classNames(
lib.bgColor,
"flex-shrink-0 flex items-center justify-center w-20 text-white text-sm font-medium rounded-l-md"
)}
>
"flex w-20 flex-shrink-0 items-center justify-center rounded-l-md text-sm font-medium text-white"
)}>
<lib.icon
className={classNames(
lib.comingSoon
? "text-ui-gray-medium"
: "text-white stroke-1",
"w-10 h-10"
lib.comingSoon ? "text-ui-gray-medium" : "stroke-1 text-white",
"h-10 w-10"
)}
/>
</div>
<div
className={classNames(
lib.comingSoon ? "border-dashed" : "",
"flex items-center justify-between flex-1 truncate bg-white rounded-r-md"
)}
>
<div className="inline-flex px-4 py-6 text-lg truncate">
"flex flex-1 items-center justify-between truncate rounded-r-md bg-white"
)}>
<div className="inline-flex truncate px-4 py-6 text-lg">
<p className="font-light">{lib.name}</p>
{lib.comingSoon && (
<div className="p-1 px-3 ml-3 bg-green-100 rounded">
<div className="ml-3 rounded bg-green-100 p-1 px-3">
<p className="text-xs text-black">coming soon</p>
</div>
)}
@@ -177,11 +156,11 @@ export default function FormCode({ formId }) {
))}
</ul>
<div className="my-12 font-light text-center text-ui-gray-medium">
<div className="text-ui-gray-medium my-12 text-center font-light">
<p>
Your form is running? Go to{" "}
<Link href={`/forms/${formId}/preview`}>
<a className="underline text-red">Pipelines</a>
<a className="text-red underline">Pipelines</a>
</Link>
</p>
</div>

View File

@@ -13,14 +13,12 @@ const formTypes = [
{
id: "NOCODE",
title: "No-Code Builder",
description:
"Use the Notion-like builder to build your form without a single line of code.",
description: "Use the Notion-like builder to build your form without a single line of code.",
},
{
id: "CODE",
title: "Code",
description:
"Use the snoopReact library to code the form yourself and manage the data here.",
description: "Use the snoopReact library to code the form yourself and manage the data here.",
additionalDescription: "",
},
];
@@ -30,10 +28,7 @@ type FormOnboardingModalProps = {
setOpen: (v: boolean) => void;
};
export default function NewFormModal({
open,
setOpen,
}: FormOnboardingModalProps) {
export default function NewFormModal({ open, setOpen }: FormOnboardingModalProps) {
const router = useRouter();
const [name, setName] = useState("");
const [formType, setFormType] = useState(formTypes[0]);
@@ -60,13 +55,12 @@ export default function NewFormModal({
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-30 backdrop-blur-md" />
leaveTo="opacity-0">
<div className="fixed inset-0 bg-gray-500 bg-opacity-30 backdrop-blur-md transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex items-end justify-center min-h-full p-4 text-center sm:items-center sm:p-0">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
@@ -74,40 +68,32 @@ export default function NewFormModal({
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative px-4 pt-5 pb-4 text-left transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:max-w-lg sm:w-full sm:p-6">
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<Dialog.Panel className="relative transform rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<div className="absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
<button
type="button"
className="text-gray-400 bg-white rounded-md hover:text-gray-500 focus:outline-none focus:ring-0 focus:ring-offset-2"
onClick={() => setOpen(false)}
>
className="rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-0 focus:ring-offset-2"
onClick={() => setOpen(false)}>
<span className="sr-only">Close</span>
<XMarkIcon className="w-6 h-6" aria-hidden="true" />
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
<div className="flex flex-row justify-between">
<h2 className="flex-none p-2 text-xl font-bold text-ui-gray-dark">
Create new form
</h2>
<h2 className="text-ui-gray-dark flex-none p-2 text-xl font-bold">Create new form</h2>
</div>
<form
onSubmit={(e) => createFormAction(e)}
className="inline-block w-full p-2 overflow-hidden text-left align-bottom transition-all transform sm:align-middle"
>
className="inline-block w-full transform overflow-hidden p-2 text-left align-bottom transition-all sm:align-middle">
<div>
<label
htmlFor="email"
className="text-sm font-light text-ui-gray-dark"
>
<label htmlFor="email" className="text-ui-gray-dark text-sm font-light">
Name your form
</label>
<div className="mt-2">
<input
type="text"
name="name"
className="block w-full p-2 mb-6 border-none rounded bg-ui-gray-light focus:ring-2 focus:ring-red sm:text-sm placeholder:font-extralight placeholder:text-ui-gray-medium"
className="bg-ui-gray-light focus:ring-red placeholder:text-ui-gray-medium mb-6 block w-full rounded border-none p-2 placeholder:font-extralight focus:ring-2 sm:text-sm"
placeholder="e.g. Customer Research Survey"
value={name}
onChange={(e) => setName(e.target.value)}
@@ -118,11 +104,11 @@ export default function NewFormModal({
</div>
<RadioGroup value={formType} onChange={setFormType}>
<RadioGroup.Label className="text-sm font-light text-ui-gray-dark">
<RadioGroup.Label className="text-ui-gray-dark text-sm font-light">
How do you build your form?
</RadioGroup.Label>
<div className="grid grid-cols-1 mt-4 gap-y-6 sm:grid-cols-2 sm:gap-x-4">
<div className="mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:gap-x-4">
{formTypes.map((formType) => (
<RadioGroup.Option
key={formType.id}
@@ -130,42 +116,34 @@ export default function NewFormModal({
className={({ checked, active }) =>
classNames(
checked ? "border-transparent" : "",
active
? "border-red ring-2 ring-red"
: "bg-ui-gray-lighter",
"relative bg-white border rounded shadow-sm p-4 flex cursor-pointer focus:outline-none"
active ? "border-red ring-red ring-2" : "bg-ui-gray-lighter",
"relative flex cursor-pointer rounded border bg-white p-4 shadow-sm focus:outline-none"
)
}
>
}>
{({ checked, active }) => (
<>
<span className="flex flex-1">
<span className="flex flex-col">
<RadioGroup.Label
as="span"
className="block font-bold text-md text-ui-gray-dark"
>
className="text-md text-ui-gray-dark block font-bold">
{formType.title}
</RadioGroup.Label>
<RadioGroup.Description
as="span"
className="flex items-center mt-1 text-xs whitespace-pre-wrap text-ui-gray-dark"
>
className="text-ui-gray-dark mt-1 flex items-center whitespace-pre-wrap text-xs">
{formType.description}
</RadioGroup.Description>
</span>
</span>
<CheckCircleIcon
className={classNames(
!checked ? "hidden" : "",
"h-5 w-5 text-red"
)}
className={classNames(!checked ? "hidden" : "", "text-red h-5 w-5")}
aria-hidden="true"
/>
<div
className={classNames(
checked ? "hidden" : "",
"h-4 w-4 rounded-full border-2 border-ui-gray-light"
"border-ui-gray-light h-4 w-4 rounded-full border-2"
)}
aria-hidden="true"
/>
@@ -173,7 +151,7 @@ export default function NewFormModal({
className={classNames(
active ? "border" : "border-2",
checked ? "border-red" : "border-transparent",
"absolute -inset-px rounded pointer-events-none"
"pointer-events-none absolute -inset-px rounded"
)}
aria-hidden="true"
/>
@@ -186,7 +164,7 @@ export default function NewFormModal({
<div className="mt-5 sm:mt-6">
<StandardButton fullwidth type="submit">
create form
<BsPlus className="w-6 h-6 ml-1"></BsPlus>
<BsPlus className="ml-1 h-6 w-6"></BsPlus>
</StandardButton>
</div>
</form>

View File

@@ -1,8 +1,4 @@
import {
GlobeAltIcon,
EnvelopeIcon,
PhoneIcon,
} from "@heroicons/react/24/solid";
import { GlobeAltIcon, EnvelopeIcon, PhoneIcon } from "@heroicons/react/24/solid";
import sanitizeHtml from "sanitize-html";
import { SnoopElement, SnoopForm, SnoopPage } from "@snoopforms/react";
import { useMemo } from "react";
@@ -48,14 +44,9 @@ export default function App({ id = "", formId, blocks, localOnly = false }) {
protocol={window.location.protocol === "http:" ? "http" : "https"}
formId={formId}
localOnly={localOnly}
className="w-full max-w-3xl mx-auto space-y-6"
>
className="mx-auto w-full max-w-3xl space-y-6">
{pages.map((page, pageIdx) => (
<SnoopPage
key={page.id}
name={page.id}
thankyou={pageIdx === pages.length - 1}
>
<SnoopPage key={page.id} name={page.id} thankyou={pageIdx === pages.length - 1}>
{page.blocks.map((block) => (
<div key={block.id}>
{block.type === "paragraph" ? (
@@ -63,8 +54,7 @@ export default function App({ id = "", formId, blocks, localOnly = false }) {
className="ce-paragraph"
dangerouslySetInnerHTML={{
__html: sanitizeHtml(block.data.text),
}}
></div>
}}></div>
) : block.type === "header" ? (
block.data.level === 1 ? (
<h1 className="ce-header">{block.data.text}</h1>
@@ -81,8 +71,7 @@ export default function App({ id = "", formId, blocks, localOnly = false }) {
help={block.data.help}
placeholder={block.data.placeholder}
classNames={{
label:
"mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate",
label: "mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate",
}}
required={block.data.required}
/>
@@ -95,8 +84,7 @@ export default function App({ id = "", formId, blocks, localOnly = false }) {
help={block.data.help}
placeholder={block.data.placeholder}
classNames={{
label:
"mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate",
label: "mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate",
}}
required={block.data.required}
/>
@@ -107,15 +95,13 @@ export default function App({ id = "", formId, blocks, localOnly = false }) {
label={block.data.label}
help={block.data.help}
placeholder={block.data.placeholder}
icon={<EnvelopeIcon className="w-5 h-5" />}
icon={<EnvelopeIcon className="h-5 w-5" />}
classNames={{
label:
"mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate",
label: "mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate",
}}
required={block.data.required}
/>
) : block.type === "multipleChoiceQuestion" &&
block.data.multipleChoice ? (
) : block.type === "multipleChoiceQuestion" && block.data.multipleChoice ? (
<SnoopElement
type="checkbox"
name={block.id}
@@ -123,13 +109,11 @@ export default function App({ id = "", formId, blocks, localOnly = false }) {
help={block.data.help}
options={block.data.options.map((o) => o.label)}
classNames={{
label:
"mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate",
label: "mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate",
}}
required={block.data.required}
/>
) : block.type === "multipleChoiceQuestion" &&
!block.data.multipleChoice ? (
) : block.type === "multipleChoiceQuestion" && !block.data.multipleChoice ? (
<SnoopElement
type="radio"
name={block.id}
@@ -137,8 +121,7 @@ export default function App({ id = "", formId, blocks, localOnly = false }) {
help={block.data.help}
options={block.data.options.map((o) => o.label)}
classNames={{
label:
"mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate",
label: "mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate",
}}
required={block.data.required}
/>
@@ -150,8 +133,7 @@ export default function App({ id = "", formId, blocks, localOnly = false }) {
help={block.data.help}
placeholder={block.data.placeholder}
classNames={{
label:
"mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate",
label: "mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate",
}}
required={block.data.required}
/>
@@ -162,10 +144,9 @@ export default function App({ id = "", formId, blocks, localOnly = false }) {
label={block.data.label}
help={block.data.help}
placeholder={block.data.placeholder}
icon={<PhoneIcon className="w-5 h-5" />}
icon={<PhoneIcon className="h-5 w-5" />}
classNames={{
label:
"mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate",
label: "mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate",
}}
required={block.data.required}
/>
@@ -186,10 +167,9 @@ export default function App({ id = "", formId, blocks, localOnly = false }) {
label={block.data.label}
help={block.data.help}
placeholder={block.data.placeholder}
icon={<GlobeAltIcon className="w-5 h-5" />}
icon={<GlobeAltIcon className="h-5 w-5" />}
classNames={{
label:
"mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate",
label: "mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate",
}}
required={block.data.required}
/>

View File

@@ -32,31 +32,22 @@ export default function BaseLayoutManagement({
<div
className={classNames(
bgClass,
limitHeightScreen
? "h-screen max-h-screen overflow-hidden"
: "min-h-screen",
limitHeightScreen ? "h-screen max-h-screen overflow-hidden" : "min-h-screen",
"flex h-full"
)}
>
)}>
<div
className={classNames(
limitHeightScreen ? "max-h-full" : "h-full",
"flex flex-col flex-1 w-full"
)}
>
className={classNames(limitHeightScreen ? "max-h-full" : "h-full", "flex w-full flex-1 flex-col")}>
<header className="w-full">
<div className="relative z-10 flex flex-shrink-0 h-16 bg-white border-b shadow-sm border-ui-gray-light">
<div className="border-ui-gray-light relative z-10 flex h-16 flex-shrink-0 border-b bg-white shadow-sm">
<div className="grid w-full grid-cols-2 sm:grid-cols-3">
<div className="flex-1 hidden space-x-8 sm:flex">
<div className="hidden flex-1 space-x-8 sm:flex">
<NewFormNavButton />
<MenuBreadcrumbs breadcrumbs={breadcrumbs} />
</div>
<div className="flex flex-1">
{steps && (
<MenuSteps steps={steps} currentStep={currentStep} />
)}
{steps && <MenuSteps steps={steps} currentStep={currentStep} />}
</div>
<div className="flex items-center justify-end flex-1 space-x-2 text-right sm:space-x-4">
<div className="flex flex-1 items-center justify-end space-x-2 text-right sm:space-x-4">
<div className="mr-6">
<MenuProfile />
</div>

View File

@@ -23,15 +23,11 @@ const EmptyPageFiller: React.FC<Props> = ({
return (
<div
className={
`bg-white border border-ui-gray-light text-center p-8 mx-auto mt-8 rounded-lg ` +
borderStyles
}
>
`border-ui-gray-light mx-auto mt-8 rounded-lg border bg-white p-8 text-center ` + borderStyles
}>
{children}
<h3 className="mt-5 text-base font-bold text-ui-gray-medium">
{alertText}
</h3>
<p className="mt-1 text-xs font-light text-ui-gray-medium">{hintText}</p>
<h3 className="text-ui-gray-medium mt-5 text-base font-bold">{alertText}</h3>
<p className="text-ui-gray-medium mt-1 text-xs font-light">{hintText}</p>
{hasButton && (
<div className="mt-6">
<StandardButton onClick={onClick}>{buttonText}</StandardButton>

View File

@@ -3,7 +3,7 @@ interface Props {
}
const FullWidth: React.FC<Props> = ({ children }) => {
return <main className="w-full h-full">{children}</main>;
return <main className="h-full w-full">{children}</main>;
};
export default FullWidth;

View File

@@ -23,31 +23,25 @@ export default function LayoutShare({ formId, resetApp, children }) {
<title>Form Preview</title>
</Head>
<div className="flex min-h-screen overflow-hidden bg-gray-50">
<div className="flex flex-col flex-1 overflow-hidden">
<div className="flex flex-1 flex-col overflow-hidden">
<header className="w-full">
<div className="relative z-10 flex flex-shrink-0 h-16 bg-white border-b shadow-sm border-ui-gray-light">
<div className="border-ui-gray-light relative z-10 flex h-16 flex-shrink-0 border-b bg-white shadow-sm">
<div className="flex flex-1 px-4 sm:px-6">
<div className="flex items-center flex-1">
<div className="flex flex-1 items-center">
<Link href={`/forms/${formId}/form`}>
<a>
<ArrowLeftIcon className="w-6 h-6" aria-hidden="true" />
<ArrowLeftIcon className="h-6 w-6" aria-hidden="true" />
</a>
</Link>
</div>
<p className="flex items-center justify-center flex-1 text-gray-600">
Preview
</p>
<div className="flex items-center justify-end flex-1 space-x-2 text-right sm:ml-6 sm:space-x-4">
<p className="flex flex-1 items-center justify-center text-gray-600">Preview</p>
<div className="flex flex-1 items-center justify-end space-x-2 text-right sm:ml-6 sm:space-x-4">
<button
type="button"
onClick={() => resetApp()}
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-red-600 border border-transparent rounded-md shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
className="inline-flex items-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2">
Restart
<ArrowPathIcon
className="w-5 h-5 ml-2 -mr-1"
aria-hidden="true"
/>
<ArrowPathIcon className="ml-2 -mr-1 h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>

View File

@@ -3,7 +3,7 @@ interface Props {
}
const LimitedWidth: React.FC<Props> = ({ children }) => {
return <main className="w-full h-full max-w-5xl mx-auto">{children}</main>;
return <main className="mx-auto h-full w-full max-w-5xl">{children}</main>;
};
export default LimitedWidth;

View File

@@ -3,17 +3,14 @@ import Link from "next/link";
export default function MenuBreadcrumbs({ breadcrumbs }) {
return (
<div className="hidden overflow-hidden sm:flex sm:flex-1 text-ellipsis">
<div className="hidden overflow-hidden text-ellipsis sm:flex sm:flex-1">
<nav className="hidden lg:flex" aria-label="Breadcrumb">
<ol className="flex items-center space-x-4">
<li>
<div>
<Link href="/forms/">
<a className="text-ui-gray-dark hover:text-ui-gray-dark">
<HomeIcon
className="flex-shrink-0 w-5 h-5"
aria-hidden="true"
/>
<HomeIcon className="h-5 w-5 flex-shrink-0" aria-hidden="true" />
<span className="sr-only">Home</span>
</a>
</Link>
@@ -23,18 +20,16 @@ export default function MenuBreadcrumbs({ breadcrumbs }) {
<li key={crumb.name}>
<div className="flex items-center">
<svg
className="flex-shrink-0 w-5 h-5 text-ui-gray-medium"
className="text-ui-gray-medium h-5 w-5 flex-shrink-0"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 20 20"
aria-hidden="true"
>
aria-hidden="true">
<path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z" />
</svg>
<a
href={crumb.href}
className="ml-4 text-sm font-medium truncate text-ui-gray-dark hover:text-ui-gray-dark"
>
className="text-ui-gray-dark hover:text-ui-gray-dark ml-4 truncate text-sm font-medium">
{crumb.name}
</a>
</div>

View File

@@ -11,9 +11,9 @@ export default function MenuProfile({}) {
{({ open }) => (
<>
<div className="inline-flex items-center ">
<Menu.Button className="flex ml-3 text-sm bg-white rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
<Menu.Button className="ml-3 flex rounded-full bg-white text-sm focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2">
<span className="sr-only">Open user menu</span>
<div className="w-8 h-8">
<div className="h-8 w-8">
<Image
className="rounded-full"
src="/img/avatar-placeholder.png"
@@ -32,27 +32,19 @@ export default function MenuProfile({}) {
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
leaveTo="transform opacity-0 scale-95">
<Menu.Items
static
className="absolute right-0 w-48 p-1 mt-2 origin-top-right bg-white rounded-sm shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
>
className="absolute right-0 mt-2 w-48 origin-top-right rounded-sm bg-white p-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<Menu.Item>
{({ active }) => (
<button
onClick={() => signOut({ callbackUrl: "/" })}
className={classNames(
active
? "bg-ui-gray-light rounded-sm text-ui-black"
: "text-ui-gray-dark",
"flex px-4 py-2 text-sm w-full"
)}
>
<ArrowLeftOnRectangleIcon
className="w-5 h-5 mr-3 text-ui-gray-dark"
aria-hidden="true"
/>
active ? "bg-ui-gray-light text-ui-black rounded-sm" : "text-ui-gray-dark",
"flex w-full px-4 py-2 text-sm"
)}>
<ArrowLeftOnRectangleIcon className="text-ui-gray-dark mr-3 h-5 w-5" aria-hidden="true" />
Sign Out
</button>
)}

View File

@@ -16,7 +16,7 @@ type MenuStepsProps = {
export default function MenuSteps({ steps, currentStep }: MenuStepsProps) {
const router = useRouter();
return (
<div className="flex items-center flex-1 justify-left sm:justify-center">
<div className="justify-left flex flex-1 items-center sm:justify-center">
<div className="w-full sm:hidden">
<label htmlFor="steps" className="sr-only">
Select a view
@@ -24,13 +24,12 @@ export default function MenuSteps({ steps, currentStep }: MenuStepsProps) {
<select
id="steps"
name="steps"
className="block w-full py-2 pl-3 pr-10 text-base rounded-md border-ui-gray-medium focus:outline-none focus:ring-red focus:border-red sm:text-sm"
className="border-ui-gray-medium focus:ring-red focus:border-red block w-full rounded-md py-2 pl-3 pr-10 text-base focus:outline-none sm:text-sm"
defaultValue={steps.find((step) => step.id === currentStep).name}
onChange={(e) => {
const stepId = e.target.children[e.target.selectedIndex].id;
router.push(steps.find((s) => s.id === stepId).href);
}}
>
}}>
{steps.map((step) => (
<option key={step.name} id={step.id}>
{step.name}
@@ -39,18 +38,17 @@ export default function MenuSteps({ steps, currentStep }: MenuStepsProps) {
</select>
</div>
<div className="hidden sm:block">
<nav className="flex -mb-px space-x-8" aria-label="steps">
<nav className="-mb-px flex space-x-8" aria-label="steps">
{steps.map((step) => (
<Link key={step.name} href={step.href}>
<a
className={classNames(
step.id === currentStep
? "border-red text-red"
: "border-transparent text-ui-gray-dark hover:text-ui-gray-dark hover:border-ui-gray-medium",
"whitespace-nowrap py-5 px-1 border-b-2 font-medium text-sm"
: "text-ui-gray-dark hover:text-ui-gray-dark hover:border-ui-gray-medium border-transparent",
"whitespace-nowrap border-b-2 py-5 px-1 text-sm font-medium"
)}
aria-current={step.id === currentStep ? "page" : undefined}
>
aria-current={step.id === currentStep ? "page" : undefined}>
{step.name}
</a>
</Link>

View File

@@ -8,13 +8,12 @@ export default function NewFormNavButton({}) {
<>
<button
type="button"
className="items-center hidden text-sm border-r border-ui-gray-light sm:flex bg-ui-gray-lighter text-ui-gray-dark hover:text-white hover:bg-red-500"
onClick={() => setOpenNewFormModal(true)}
>
className="border-ui-gray-light bg-ui-gray-lighter text-ui-gray-dark hidden items-center border-r text-sm hover:bg-red-500 hover:text-white sm:flex"
onClick={() => setOpenNewFormModal(true)}>
<nav className="hidden sm:flex" aria-label="Breadcrumb">
<ol className="flex items-center space-x-4">
<li>
<div className="inline-flex items-center px-6 py-2 text-sm font-medium leading-4 bg-transparent border border-transparent hover:text-white focus:outline-none">
<div className="inline-flex items-center border border-transparent bg-transparent px-6 py-2 text-sm font-medium leading-4 hover:text-white focus:outline-none">
<PlusIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
create form
</div>

View File

@@ -17,27 +17,24 @@ interface Props {
// button component, consuming props
const SecondNavBar: React.FC<Props> = ({ navItems, currentItemId }) => {
return (
<div className="flex items-center justify-center flex-shrink-0 border-b border-ui-gray-light bg-ui-gray-lighter">
<div className="border-ui-gray-light bg-ui-gray-lighter flex flex-shrink-0 items-center justify-center border-b">
<nav className="flex space-x-10" aria-label="resultModes">
{navItems.map((navItem) => (
<button
key={navItem.id}
className={classNames(
`h-16 text-xs border-b-2 border-transparent`,
`h-16 border-b-2 border-transparent text-xs`,
!navItem.disabled &&
(navItem.id === currentItemId
? "text-red border-b-2 border-red"
: "hover:border-gray-300 text-ui-gray-dark hover:text-red bg-transparent"),
? "text-red border-red border-b-2"
: "text-ui-gray-dark hover:text-red bg-transparent hover:border-gray-300"),
navItem.disabled
? "text-ui-gray-medium"
: "hover:border-b-2 hover:border-red text-ui-gray-dark hover:text-red"
: "hover:border-red text-ui-gray-dark hover:text-red hover:border-b-2"
)}
onClick={navItem.onClick}
disabled={navItem.disabled}
>
{navItem.Icon && (
<navItem.Icon className="w-6 h-6 mx-auto mb-1 stroke-1" />
)}
disabled={navItem.disabled}>
{navItem.Icon && <navItem.Icon className="mx-auto mb-1 h-6 w-6 stroke-1" />}
{navItem.label}
</button>
))}

View File

@@ -45,30 +45,26 @@ export default function AddPipelineModal({ open, setOpen }) {
<>
{typeId === null ? (
<>
<h2 className="mb-6 text-xl font-bold text-ui-gray-dark">
<h2 className="text-ui-gray-dark mb-6 text-xl font-bold">
Please choose a pipeline you want to add
</h2>
{availablePipelines.map((pipeline) => (
<div
className="w-full bg-white border shadow border-ui-gray-light sm:rounded"
key={pipeline.title}
>
className="border-ui-gray-light w-full border bg-white shadow sm:rounded"
key={pipeline.title}>
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg font-medium leading-6 text-gray-900">
{pipeline.title}
</h3>
<h3 className="text-lg font-medium leading-6 text-gray-900">{pipeline.title}</h3>
<div className="mt-2 sm:flex sm:items-start sm:justify-between">
<div className="max-w-xl text-sm text-gray-500">
<p>{pipeline.description}</p>
</div>
<div className="mt-5 sm:mt-0 sm:ml-6 sm:flex-shrink-0 sm:flex sm:items-center">
<div className="mt-5 sm:mt-0 sm:ml-6 sm:flex sm:flex-shrink-0 sm:items-center">
<button
type="button"
onClick={() => {
setTypeId(pipeline.typeId);
}}
className="inline-flex items-center px-4 py-2 font-medium text-white bg-red-600 border border-transparent rounded-md shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
>
className="inline-flex items-center rounded-md border border-transparent bg-red-600 px-4 py-2 font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 sm:text-sm">
Select
</button>
</div>
@@ -78,26 +74,19 @@ export default function AddPipelineModal({ open, setOpen }) {
))}
</>
) : (
<form
className="w-full space-y-8 divide-y divide-gray-200"
onSubmit={handleSubmit}
>
{typeId === "WEBHOOK" ? (
<WebhookSettings pipeline={pipeline} setPipeline={setPipeline} />
) : null}
<form className="w-full space-y-8 divide-y divide-gray-200" onSubmit={handleSubmit}>
{typeId === "WEBHOOK" ? <WebhookSettings pipeline={pipeline} setPipeline={setPipeline} /> : null}
<div className="pt-5">
<div className="flex justify-end">
<button
type="button"
onClick={() => setOpen(false)}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2">
Cancel
</button>
<button
type="submit"
className="inline-flex justify-center px-4 py-2 ml-3 text-sm font-medium text-white bg-red-600 border border-transparent rounded-md shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
className="ml-3 inline-flex justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2">
Create
</button>
</div>

View File

@@ -1,22 +1,10 @@
import {
persistPipeline,
usePipeline,
usePipelines,
} from "../../lib/pipelines";
import { persistPipeline, usePipeline, usePipelines } from "../../lib/pipelines";
import Loading from "../Loading";
import Modal from "../Modal";
import { WebhookSettings } from "./webhook";
export default function UpdatePipelineModal({
open,
setOpen,
formId,
pipelineId,
}) {
const { pipeline, isLoadingPipeline, mutatePipeline } = usePipeline(
formId,
pipelineId
);
export default function UpdatePipelineModal({ open, setOpen, formId, pipelineId }) {
const { pipeline, isLoadingPipeline, mutatePipeline } = usePipeline(formId, pipelineId);
const { pipelines, mutatePipelines } = usePipelines(formId);
const handleSubmit = async (e) => {
@@ -36,29 +24,21 @@ export default function UpdatePipelineModal({
{isLoadingPipeline ? (
<Loading />
) : (
<form
className="w-full space-y-8 divide-y divide-gray-200"
onSubmit={handleSubmit}
>
<form className="w-full space-y-8 divide-y divide-gray-200" onSubmit={handleSubmit}>
{pipeline.type === "WEBHOOK" ? (
<WebhookSettings
pipeline={pipeline}
setPipeline={(p) => mutatePipeline(p, false)}
/>
<WebhookSettings pipeline={pipeline} setPipeline={(p) => mutatePipeline(p, false)} />
) : null}
<div className="pt-5">
<div className="flex justify-end">
<button
type="button"
onClick={() => setOpen(false)}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2">
Cancel
</button>
<button
type="submit"
className="inline-flex justify-center px-4 py-2 ml-3 text-sm font-medium text-white bg-red-600 border border-transparent rounded-md shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
className="ml-3 inline-flex justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2">
Update
</button>
</div>

View File

@@ -31,20 +31,14 @@ export function WebhookSettings({ pipeline, setPipeline }) {
return (
<div className="space-y-8 divide-y divide-gray-200">
<div>
<h2 className="mb-3 text-xl font-bold text-ui-gray-dark">
Configure Webhook
</h2>
<h2 className="text-ui-gray-dark mb-3 text-xl font-bold">Configure Webhook</h2>
<p className="mt-1 text-sm text-gray-500">
Configure your webhook. To learn more about how webhooks work, please
check out our docs.
Configure your webhook. To learn more about how webhooks work, please check out our docs.
</p>
<div className="grid grid-cols-1 mt-6 gap-y-6 gap-x-4 sm:grid-cols-6">
<div className="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
<div className="sm:col-span-4">
<label
htmlFor="name"
className="block text-sm font-medium text-gray-700"
>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Webhook Name
</label>
<div className="mt-1">
@@ -54,17 +48,14 @@ export function WebhookSettings({ pipeline, setPipeline }) {
id="name"
value={pipeline.name || ""}
onChange={(e) => updateField("name", e.target.value)}
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 sm:text-sm"
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-red-500 focus:ring-red-500 sm:text-sm"
required
/>
</div>
</div>
<div className="sm:col-span-4">
<label
htmlFor="endpointUrl"
className="block text-sm font-medium text-gray-700"
>
<label htmlFor="endpointUrl" className="block text-sm font-medium text-gray-700">
Endpoint URL
</label>
<div className="mt-1">
@@ -72,18 +63,14 @@ export function WebhookSettings({ pipeline, setPipeline }) {
type="url"
pattern="^https:\/\/(.*)"
onInvalid={(e: any) =>
e.target.setCustomValidity(
"please provide a valid website address with https"
)
e.target.setCustomValidity("please provide a valid website address with https")
}
onInput={(e: any) => e.target.setCustomValidity("")}
name="endpointUrl"
id="endpointUrl"
value={pipeline.data.endpointUrl || ""}
onChange={(e) =>
updateField("endpointUrl", e.target.value, "data")
}
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 sm:text-sm"
onChange={(e) => updateField("endpointUrl", e.target.value, "data")}
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-red-500 focus:ring-red-500 sm:text-sm"
required
/>
</div>
@@ -92,10 +79,7 @@ export function WebhookSettings({ pipeline, setPipeline }) {
</p>
</div>
<div className="sm:col-span-4">
<label
htmlFor="secret"
className="block text-sm font-medium text-gray-700"
>
<label htmlFor="secret" className="block text-sm font-medium text-gray-700">
Secret
</label>
<div className="mt-1">
@@ -105,12 +89,11 @@ export function WebhookSettings({ pipeline, setPipeline }) {
id="secret"
value={pipeline.data.secret || ""}
onChange={(e) => updateField("secret", e.target.value, "data")}
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 sm:text-sm"
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-red-500 focus:ring-red-500 sm:text-sm"
/>
</div>
<p className="mt-2 text-xs text-gray-500" id="email-description">
We sign all event notification payloads with a SHA256 signature
using this secret
We sign all event notification payloads with a SHA256 signature using this secret
</p>
</div>
</div>
@@ -118,41 +101,31 @@ export function WebhookSettings({ pipeline, setPipeline }) {
<div className="pt-8">
<div>
<h3 className="text-lg font-medium leading-6 text-gray-900">
Advanced Settings
</h3>
<p className="mt-1 text-sm text-gray-500">
Set up this webhook to fit your needs.
</p>
<h3 className="text-lg font-medium leading-6 text-gray-900">Advanced Settings</h3>
<p className="mt-1 text-sm text-gray-500">Set up this webhook to fit your needs.</p>
</div>
<div className="mt-6">
<fieldset>
<legend className="sr-only">Events</legend>
<div
className="text-base font-medium text-gray-900"
aria-hidden="true"
>
<div className="text-base font-medium text-gray-900" aria-hidden="true">
Events
</div>
<div className="mt-4 space-y-4">
{eventTypes.map((eventType) => (
<div key={eventType.id}>
<div className="relative flex items-start">
<div className="flex items-center h-5">
<div className="flex h-5 items-center">
<input
id="comments"
name="comments"
type="checkbox"
checked={pipeline.events.includes(eventType.id)}
onChange={() => toggleEvent(eventType.id)}
className="w-4 h-4 text-red-600 border-gray-300 rounded-sm focus:ring-red-500"
className="h-4 w-4 rounded-sm border-gray-300 text-red-600 focus:ring-red-500"
/>
</div>
<div className="ml-3 text-sm">
<label
htmlFor="comments"
className="font-medium text-gray-700"
>
<label htmlFor="comments" className="font-medium text-gray-700">
{eventType.name}
</label>
<p className="text-gray-500">{eventType.description}</p>
@@ -166,14 +139,11 @@ export function WebhookSettings({ pipeline, setPipeline }) {
<div className="mt-6">
<fieldset>
<legend className="sr-only">Conditions</legend>
<div
className="text-base font-medium text-gray-900"
aria-hidden="true"
>
<div className="text-base font-medium text-gray-900" aria-hidden="true">
Conditions
</div>
<div className="mt-4 space-y-4">
<div className="px-2 py-5 border border-gray-100 rounded-sm bg-gray-50">
<div className="rounded-sm border border-gray-100 bg-gray-50 px-2 py-5">
<p className="flex justify-center text-xs text-gray-600">
conditional data piping coming soon
</p>

View File

@@ -1,17 +1,10 @@
import { Pipeline, Prisma } from "@prisma/client";
import crypto from "crypto";
import { ApiEvent } from "../../../lib/types";
export async function handleWebhook(pipeline: Pipeline, event: ApiEvent) {
if (
pipeline.data.hasOwnProperty("endpointUrl") &&
pipeline.data.hasOwnProperty("secret")
) {
if (
event.type === "pageSubmission" &&
pipeline.events.includes("PAGE_SUBMISSION")
) {
const webhookData = pipeline.data as Prisma.JsonObject;
export async function handleWebhook(pipeline, event: ApiEvent) {
if (pipeline.data.hasOwnProperty("endpointUrl") && pipeline.data.hasOwnProperty("secret")) {
if (event.type === "pageSubmission" && pipeline.events.includes("PAGE_SUBMISSION")) {
const webhookData = pipeline.data;
const body = { time: Math.floor(Date.now() / 1000), event };
fetch(webhookData.endpointUrl.toString(), {
method: "POST",

View File

@@ -1,8 +1,7 @@
export const webhook = {
typeId: "WEBHOOK",
title: "Webhook",
description:
"Notify an external endpoint when events happen in your form (e.g. a new submission).",
description: "Notify an external endpoint when events happen in your form (e.g. a new submission).",
};
export * from "./SettingsComponent";

View File

@@ -11,60 +11,48 @@ interface Props {
smallerText?: boolean;
}
const AnalyticsCard: React.FC<Props> = ({
value,
label,
toolTipText,
trend,
smallerText,
}) => {
const AnalyticsCard: React.FC<Props> = ({ value, label, toolTipText, trend, smallerText }) => {
return (
<div className="bg-white rounded-md shadow-md">
<div className="rounded-md bg-white shadow-md">
<div key={label} className="px-4 py-5 sm:p-6">
<dt className="inline-flex text-base font-normal text-gray-900 has-tooltip">
<dt className="has-tooltip inline-flex text-base font-normal text-gray-900">
{label}{" "}
{toolTipText && (
<QuestionMarkCircleIcon className="w-4 h-4 ml-1 text-red hover:text-ui-gray-dark" />
<QuestionMarkCircleIcon className="text-red hover:text-ui-gray-dark ml-1 h-4 w-4" />
)}
{toolTipText && (
<span className="flex p-1 px-4 -mt-6 -ml-8 text-xs text-center text-white bg-gray-600 rounded shadow-lg grow tooltip">
<span className="tooltip -mt-6 -ml-8 flex grow rounded bg-gray-600 p-1 px-4 text-center text-xs text-white shadow-lg">
{toolTipText}
</span>
)}
</dt>
<dd className="flex items-baseline justify-between mt-1 md:block lg:flex">
<dd className="mt-1 flex items-baseline justify-between md:block lg:flex">
<div
className={classNames(
smallerText ? "text-lg" : "text-xl",
"flex items-baseline text-xl font-semibold text-gray-800"
)}
>
)}>
{value}
</div>
{trend && (
<div
className={classNames(
trend >= 0
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800",
"inline-flex items-baseline px-2.5 py-0.5 rounded-full text-sm font-medium md:mt-2 lg:mt-0"
)}
>
trend >= 0 ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800",
"inline-flex items-baseline rounded-full px-2.5 py-0.5 text-sm font-medium md:mt-2 lg:mt-0"
)}>
{trend >= 0 ? (
<ArrowUpIcon
className="-ml-1 mr-0.5 flex-shrink-0 self-center h-5 w-5 text-green-500"
className="-ml-1 mr-0.5 h-5 w-5 flex-shrink-0 self-center text-green-500"
aria-hidden="true"
/>
) : (
<ArrowDownIcon
className="-ml-1 mr-0.5 flex-shrink-0 self-center h-5 w-5 text-red-500"
className="-ml-1 mr-0.5 h-5 w-5 flex-shrink-0 self-center text-red-500"
aria-hidden="true"
/>
)}
<span className="sr-only">
{trend >= 0 ? "Increased" : "Decreased"} by
</span>
<span className="sr-only">{trend >= 0 ? "Increased" : "Decreased"} by</span>
{trend} %
</div>
)}

View File

@@ -3,24 +3,18 @@ import { ChevronDownIcon } from "@heroicons/react/24/solid";
import { parseAsync } from "json2csv";
import { Fragment } from "react";
import { useForm } from "../../lib/forms";
import {
getSubmission,
useSubmissionSessions,
} from "../../lib/submissionSessions";
import { getSubmission, useSubmissionSessions } from "../../lib/submissionSessions";
import { Submission } from "../../lib/types";
import { slugify } from "../../lib/utils";
import Loading from "../Loading";
export default function DownloadResponses({ formId }) {
const { submissionSessions, isLoadingSubmissionSessions } =
useSubmissionSessions(formId);
const { submissionSessions, isLoadingSubmissionSessions } = useSubmissionSessions(formId);
const { form, isLoadingForm } = useForm(formId);
const download = async (format: "csv" | "excel") => {
// build dict of answers in copy of answerSessions
const submissions: Submission[] = submissionSessions.map((s) =>
getSubmission(s, form.schema)
);
const submissions: Submission[] = submissionSessions.map((s) => getSubmission(s, form.schema));
// build data fields for csv/excel file
const data = [];
for (const submission of submissions) {
@@ -72,10 +66,7 @@ export default function DownloadResponses({ formId }) {
const url = window.URL.createObjectURL(new Blob([blob]));
const link = document.createElement("a");
link.href = url;
link.setAttribute(
"download",
`${slugify(form.name)}.${fileTypes[format].fileExtension}`
);
link.setAttribute("download", `${slugify(form.name)}.${fileTypes[format].fileExtension}`);
document.body.appendChild(link);
link.click();
link.parentNode.removeChild(link);
@@ -91,12 +82,9 @@ export default function DownloadResponses({ formId }) {
return (
<Menu as="div" className="relative z-10 inline-block w-full text-left">
<div>
<Menu.Button className="inline-flex justify-center w-full px-4 py-2 text-sm font-medium text-white bg-gray-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
<Menu.Button className="inline-flex w-full justify-center bg-gray-400 px-4 py-2 text-sm font-medium text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
Download
<ChevronDownIcon
className="w-5 h-5 ml-2 -mr-1 text-white hover:text-gray-100"
aria-hidden="true"
/>
<ChevronDownIcon className="ml-2 -mr-1 h-5 w-5 text-white hover:text-gray-100" aria-hidden="true" />
</Menu.Button>
</div>
<Transition
@@ -106,9 +94,8 @@ export default function DownloadResponses({ formId }) {
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 w-56 mt-2 origin-top-right bg-white divide-y divide-gray-100 rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
leaveTo="transform opacity-0 scale-95">
<Menu.Items className="absolute right-0 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="px-1 py-1 ">
<Menu.Item>
{({ active }) => (
@@ -116,8 +103,7 @@ export default function DownloadResponses({ formId }) {
onClick={() => download("csv")}
className={`${
active ? "bg-red-500 text-white" : "text-gray-900"
} group flex rounded-md items-center w-full px-2 py-2 text-sm`}
>
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}>
Download as CSV
</button>
)}

View File

@@ -1,16 +1,12 @@
import Image from "next/image";
import { useMemo } from "react";
import {
getSubmissionAnalytics,
useSubmissionSessions,
} from "../../lib/submissionSessions";
import { getSubmissionAnalytics, useSubmissionSessions } from "../../lib/submissionSessions";
import { timeSince } from "../../lib/utils";
import Loading from "../Loading";
import AnalyticsCard from "./AnalyticsCard";
export default function ResultsAnalytics({ formId }) {
const { submissionSessions, isLoadingSubmissionSessions } =
useSubmissionSessions(formId);
const { submissionSessions, isLoadingSubmissionSessions } = useSubmissionSessions(formId);
const analytics = useMemo(() => {
if (!isLoadingSubmissionSessions) {
@@ -31,9 +27,7 @@ export default function ResultsAnalytics({ formId }) {
{
id: "lastSubmission",
name: "Last Submission",
stat: analytics.lastSubmissionAt
? timeSince(analytics.lastSubmissionAt)
: "--",
stat: analytics.lastSubmissionAt ? timeSince(analytics.lastSubmissionAt) : "--",
smallerText: true,
toolTipText: undefined,
},
@@ -47,9 +41,9 @@ export default function ResultsAnalytics({ formId }) {
return (
<div className="my-8">
<h2 className="text-xl font-bold text-ui-gray-dark">Analytics</h2>
<h2 className="text-ui-gray-dark text-xl font-bold">Analytics</h2>
<div>
<dl className="grid grid-cols-1 gap-5 mt-8 sm:grid-cols-2">
<dl className="mt-8 grid grid-cols-1 gap-5 sm:grid-cols-2">
{stats.map((item) => (
<AnalyticsCard
key={item.id}
@@ -63,15 +57,13 @@ export default function ResultsAnalytics({ formId }) {
</dl>
</div>
<div className="flex items-end">
<h2 className="mt-16 text-xl font-bold text-ui-gray-dark">
Optimize Form
</h2>
<div className="px-3 py-2 ml-2 text-xs text-green-800 rounded-sm bg-green-50">
<h2 className="text-ui-gray-dark mt-16 text-xl font-bold">Optimize Form</h2>
<div className="ml-2 rounded-sm bg-green-50 px-3 py-2 text-xs text-green-800">
<p>coming soon</p>
</div>
</div>
<div className="grid grid-cols-2 gap-10 mt-8">
<div className="p-5 bg-white rounded-md shadow-md">
<div className="mt-8 grid grid-cols-2 gap-10">
<div className="rounded-md bg-white p-5 shadow-md">
<Image
src="/../../img/drop-offs-v1.svg"
alt="drop-off"
@@ -80,7 +72,7 @@ export default function ResultsAnalytics({ formId }) {
height={273}
/>
</div>
<div className="p-5 bg-white rounded-md shadow-md">
<div className="rounded-md bg-white p-5 shadow-md">
<Image
src="/../../img/a-b-test-v1.svg"
alt="drop-off"

View File

@@ -21,22 +21,15 @@ type ResultsResponseProps = {
};
export default function ResultsResponses({ formId }: ResultsResponseProps) {
const {
submissionSessions,
isLoadingSubmissionSessions,
mutateSubmissionSessions,
} = useSubmissionSessions(formId);
const [activeSubmissionSession, setActiveSubmissionSession] =
useState<SubmissionSession | null>(null);
const { submissionSessions, isLoadingSubmissionSessions, mutateSubmissionSessions } =
useSubmissionSessions(formId);
const [activeSubmissionSession, setActiveSubmissionSession] = useState<SubmissionSession | null>(null);
const handleDelete = async (submissionSession: SubmissionSession) => {
try {
await fetch(
`/api/forms/${formId}/submissionSessions/${submissionSession.id}`,
{
method: "DELETE",
}
);
await fetch(`/api/forms/${formId}/submissionSessions/${submissionSession.id}`, {
method: "DELETE",
});
await mutateSubmissionSessions();
setActiveSubmissionSession(null);
@@ -57,28 +50,25 @@ export default function ResultsResponses({ formId }: ResultsResponseProps) {
}
return (
<div className="flex flex-col flex-1 w-full h-full mx-auto overflow-visible max-w-screen">
<div className="relative z-0 flex flex-1 h-full overflow-visible">
<main className="relative z-0 flex-1 mb-32 overflow-y-auto focus:outline-none xl:order-last">
<div className="max-w-screen mx-auto flex h-full w-full flex-1 flex-col overflow-visible">
<div className="relative z-0 flex h-full flex-1 overflow-visible">
<main className="relative z-0 mb-32 flex-1 overflow-y-auto focus:outline-none xl:order-last">
<div className="overflow-visible sm:rounded-lg">
{!activeSubmissionSession ? (
<button
type="button"
className="relative block p-12 mx-auto mt-8 text-center border-2 border-gray-300 border-dashed rounded-lg w-96 hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<span className="block mt-2 text-sm font-medium text-gray-500">
className="relative mx-auto mt-8 block w-96 rounded-lg border-2 border-dashed border-gray-300 p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
<span className="mt-2 block text-sm font-medium text-gray-500">
Select a response on the left to see the details here
</span>
</button>
) : (
<>
<div className="px-4 py-5 bg-white shadow sm:px-12 sm:pb-4 sm:pt-12">
<div className="bg-white px-4 py-5 shadow sm:px-12 sm:pb-4 sm:pt-12">
<div className="grid grid-cols-2 gap-8 divide-x">
<div className="flow-root">
<h1 className="mb-8 text-gray-700">
{convertDateTimeString(
activeSubmissionSession.createdAt
)}
{convertDateTimeString(activeSubmissionSession.createdAt)}
</h1>
<SubmissionDisplay
key={activeSubmissionSession.id}
@@ -89,69 +79,59 @@ export default function ResultsResponses({ formId }: ResultsResponseProps) {
<div className="hidden pl-10 md:flow-root">
<h1 className="mb-8 text-gray-700">Session Activity</h1>
<ul role="list" className="-mb-8">
{activeSubmissionSession.events.map(
(event, eventIdx) => (
<li key={event.id}>
<div className="relative pb-8">
{eventIdx !==
activeSubmissionSession.events.length - 1 ? (
{activeSubmissionSession.events.map((event, eventIdx) => (
<li key={event.id}>
<div className="relative pb-8">
{eventIdx !== activeSubmissionSession.events.length - 1 ? (
<span
className="bg-ui-gray-light absolute top-4 left-4 -ml-px h-full w-0.5"
aria-hidden="true"
/>
) : null}
<div className="relative flex space-x-3">
<div>
<span
className="absolute top-4 left-4 -ml-px h-full w-0.5 bg-ui-gray-light"
aria-hidden="true"
/>
) : null}
<div className="relative flex space-x-3">
className={classNames(
"bg-red-200",
"flex h-8 w-8 items-center justify-center rounded-full ring-8 ring-white"
)}>
<CheckIcon className="h-5 w-5 text-white" aria-hidden="true" />
</span>
</div>
<div className="flex min-w-0 flex-1 flex-wrap justify-between gap-4 pt-1.5">
<div>
<span
className={classNames(
"bg-red-200",
"h-8 w-8 rounded-full flex items-center justify-center ring-8 ring-white"
)}
>
<CheckIcon
className="w-5 h-5 text-white"
aria-hidden="true"
/>
</span>
</div>
<div className="min-w-0 flex-1 pt-1.5 flex justify-between flex-wrap gap-4">
<div>
<p className="text-sm text-gray-500">
{getEventName(event.type)}
{/* <span className="font-medium text-gray-900">
<p className="text-sm text-gray-500">
{getEventName(event.type)}
{/* <span className="font-medium text-gray-900">
{event.data.pageName || ""}
</span> */}
</p>
</div>
<div className="text-sm text-right text-gray-500 whitespace-nowrap">
<time dateTime={event.createdAt}>
{convertTimeString(event.createdAt)}
</time>
</div>
</p>
</div>
<div className="whitespace-nowrap text-right text-sm text-gray-500">
<time dateTime={event.createdAt}>
{convertTimeString(event.createdAt)}
</time>
</div>
</div>
</div>
</li>
)
)}
</div>
</li>
))}
</ul>
</div>
</div>
</div>
<div className="w-full">
<button
className="flex items-center justify-center w-full gap-2 px-4 py-2 text-sm font-medium text-white bg-gray-300 border border-transparent shadow-sm hover:bg-red-500 focus:outline-none"
className="flex w-full items-center justify-center gap-2 border border-transparent bg-gray-300 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-red-500 focus:outline-none"
onClick={() => {
if (
confirm(
"Are you sure you want to delete this submission? It will be gone forever!"
)
confirm("Are you sure you want to delete this submission? It will be gone forever!")
) {
handleDelete(activeSubmissionSession);
}
}}
>
<TrashIcon className="w-4 h-4" />
}}>
<TrashIcon className="h-4 w-4" />
Delete Submission
</button>
</div>
@@ -159,24 +139,21 @@ export default function ResultsResponses({ formId }: ResultsResponseProps) {
)}
</div>
</main>
<aside className="flex flex-col flex-1 flex-shrink-0 order-first h-full border-r border-ui-gray-light md:flex-none md:w-96">
<aside className="border-ui-gray-light order-first flex h-full flex-1 flex-shrink-0 flex-col border-r md:w-96 md:flex-none">
<DownloadResponses formId={formId} />
<div className="pt-4 pb-2">
<h2 className="px-5 text-lg font-medium text-gray-900">
Responses
</h2>
<h2 className="px-5 text-lg font-medium text-gray-900">Responses</h2>
</div>
{submissionSessions.length === 0 ? (
<p className="px-5 mt-3 text-sm text-gray-500">No responses yet</p>
<p className="mt-3 px-5 text-sm text-gray-500">No responses yet</p>
) : (
<RadioGroup
value={activeSubmissionSession}
onChange={setActiveSubmissionSession}
className="flex-1 min-h-0 mb-32 overflow-y-auto shadow-inner"
as="div"
>
className="mb-32 min-h-0 flex-1 overflow-y-auto shadow-inner"
as="div">
<div className="relative">
<ul className="relative z-0 divide-y divide-ui-gray-light">
<ul className="divide-ui-gray-light relative z-0 divide-y">
{submissionSessions.map((submissionSession) => (
<RadioGroup.Option
key={submissionSession.id}
@@ -184,26 +161,19 @@ export default function ResultsResponses({ formId }: ResultsResponseProps) {
className={({ checked }) =>
classNames(
checked ? "bg-gray-100" : "",
"relative flex items-center px-6 py-5 space-x-3 "
"relative flex items-center space-x-3 px-6 py-5 "
)
}
>
<div className="flex-1 min-w-0">
}>
<div className="min-w-0 flex-1">
<button
onClick={() =>
setActiveSubmissionSession(submissionSession)
}
className="w-full text-left focus:outline-none"
>
onClick={() => setActiveSubmissionSession(submissionSession)}
className="w-full text-left focus:outline-none">
{/* Extend touch target to entire panel */}
<span
className="absolute inset-0"
aria-hidden="true"
/>
<span className="absolute inset-0" aria-hidden="true" />
<p className="text-sm font-medium text-gray-900">
{convertDateTimeString(submissionSession.createdAt)}
</p>
<p className="text-sm text-gray-500 truncate">
<p className="truncate text-sm text-gray-500">
{submissionSession.events.length} events
</p>
</button>

View File

@@ -13,8 +13,7 @@ import TextResults from "./summary/TextResults";
import ChoiceResults from "./summary/ChoiceResults";
export default function ResultsSummary({ formId }) {
const { submissionSessions, isLoadingSubmissionSessions } =
useSubmissionSessions(formId);
const { submissionSessions, isLoadingSubmissionSessions } = useSubmissionSessions(formId);
const { form, isLoadingForm } = useForm(formId);
@@ -43,9 +42,7 @@ export default function ResultsSummary({ formId }) {
{
id: "lastSubmission",
name: "Last Submission",
stat: insights.lastSubmissionAt
? timeSince(insights.lastSubmissionAt)
: "--",
stat: insights.lastSubmissionAt ? timeSince(insights.lastSubmissionAt) : "--",
smallerText: true,
toolTipText: undefined,
},
@@ -59,10 +56,8 @@ export default function ResultsSummary({ formId }) {
return (
<>
<h2 className="mt-8 text-xl font-bold text-ui-gray-dark">
Responses Overview
</h2>
<dl className="grid grid-cols-1 gap-5 mt-8 sm:grid-cols-2">
<h2 className="text-ui-gray-dark mt-8 text-xl font-bold">Responses Overview</h2>
<dl className="mt-8 grid grid-cols-1 gap-5 sm:grid-cols-2">
{stats.map((item) => (
<AnalyticsCard
key={item.id}
@@ -81,14 +76,7 @@ export default function ResultsSummary({ formId }) {
page.type === "form" && (
<div key={page.name}>
{page.elements.map((element) =>
[
"email",
"number",
"phone",
"text",
"textarea",
"website",
].includes(element.type) ? (
["email", "number", "phone", "text", "textarea", "website"].includes(element.type) ? (
<TextResults element={element} />
) : ["checkbox", "radio"].includes(element.type) ? (
<ChoiceResults element={element} />

View File

@@ -44,22 +44,18 @@ export default function Submission({ formId, submissionSession }) {
}
return (
<div className="bg-white shadow sm:rounded-lg max-w-">
<div className="max-w- bg-white shadow sm:rounded-lg">
<div className="px-4 py-5 sm:p-6">
<div className="text-ui-gray-dark-600">
<p className="text-sm">
{convertDateTimeString(submission.createdAt)}
</p>
<p className="text-sm">{convertDateTimeString(submission.createdAt)}</p>
{submission.pages.map((page) => (
<div key={page.name}>
{page.elements?.map(
(element) =>
element.type !== "submit" && (
<div key={element.name}>
<p className="font-semibold text-red">{element.label}</p>
<p className="font-normal">
{element.value || "[not provided]"}
</p>
<p className="text-red font-semibold">{element.label}</p>
<p className="font-normal">{element.value || "[not provided]"}</p>
</div>
)
)}

View File

@@ -20,22 +20,19 @@ export default function SubmissionDisplay({ formId, submissionSession }) {
return (
<div className="flow-root">
<ul role="list" className="divide-y divide-ui-gray-light">
<ul role="list" className="divide-ui-gray-light divide-y">
{submission.pages.map((page) =>
page.elements?.map(
(element) =>
element.type !== "submit" && (
<li key={element.name} className="py-5">
<p className="text-sm font-semibold text-gray-800">
{element.label}
</p>
<p className="text-sm font-semibold text-gray-800">{element.label}</p>
<p
className={classNames(
element.value ? "text-gray-600" : "text-gray-400",
"pt-1 text-sm text-gray-600 whitespace-pre-line"
)}
>
"whitespace-pre-line pt-1 text-sm text-gray-600"
)}>
{element.value || "[not provided]"}
</p>
</li>

View File

@@ -47,30 +47,20 @@ export const elementTypes = [
export const getElementTypeIcon = (type) => {
const elementType = elementTypes.find((e) => e.type === type);
return elementType ? (
<span
className={classNames(
`text-white`,
`bg-red-500`,
"rounded-lg inline-flex p-3 ring-4 ring-white"
)}
>
<elementType.icon className="w-4 h-4" aria-hidden="true" />
<span className={classNames(`text-white`, `bg-red-500`, "inline-flex rounded-lg p-3 ring-4 ring-white")}>
<elementType.icon className="h-4 w-4" aria-hidden="true" />
</span>
) : null;
};
export default function BaseResults({ element, children }) {
return (
<div className="my-8 overflow-hidden bg-white rounded-lg shadow">
<div className="my-8 overflow-hidden rounded-lg bg-white shadow">
<div className="px-4 py-5 sm:p-6">
<div className="flex items-center">
<div className="flex-shrink-0">
{getElementTypeIcon(element.type)}
</div>
<div className="flex-shrink-0">{getElementTypeIcon(element.type)}</div>
<div className="ml-4">
<h3 className="font-medium leading-6 text-gray-900 text-md">
{element.label}
</h3>
<h3 className="text-md font-medium leading-6 text-gray-900">{element.label}</h3>
</div>
</div>
</div>

View File

@@ -39,7 +39,7 @@ export default function ChoiceResults({ element }) {
return (
<BaseResults element={element}>
<div className="flow-root px-8 my-4 mt-6 text-center">
<div className="my-4 mt-6 flow-root px-8 text-center">
<Chart type="bar" data={data} options={options} height={75} />
</div>
</BaseResults>

View File

@@ -3,8 +3,8 @@ import BaseResults from "./BaseResults";
export default function TextResults({ element }) {
return (
<BaseResults element={element}>
<div className="flow-root px-8 my-4 mt-6 overflow-y-scroll text-center h-44 max-h-64">
<ul className="-my-5 divide-y divide-ui-gray-light">
<div className="my-4 mt-6 flow-root h-44 max-h-64 overflow-y-scroll px-8 text-center">
<ul className="divide-ui-gray-light -my-5 divide-y">
{element?.summary?.map((answer) => (
<li key={answer} className="py-4">
<div className="relative focus-within:ring-2 focus-within:ring-indigo-500">

View File

@@ -1,4 +1,4 @@
import { prisma } from "./prisma";
import { prisma } from "database";
export const formHasOwnership = async (session, formId) => {
try {

View File

@@ -1,6 +1,6 @@
import { handleWebhook } from "../components/pipelines/webhook";
import { capturePosthogEvent } from "./posthog";
import { prisma } from "./prisma";
import { prisma } from "database";
import { sendTelemetry } from "./telemetry";
import { ApiEvent } from "./types";
@@ -9,20 +9,15 @@ type validationError = {
message: string;
};
export const validateEvents = (
events: ApiEvent[]
): validationError | undefined => {
export const validateEvents = (events: ApiEvent[]): validationError | undefined => {
if (!Array.isArray(events)) {
return { status: 400, message: `"events" needs to be a list` };
}
for (const event of events) {
if (
![
"createSubmissionSession",
"pageSubmission",
"submissionCompleted",
"updateSchema",
].includes(event.type)
!["createSubmissionSession", "pageSubmission", "submissionCompleted", "updateSchema"].includes(
event.type
)
) {
return {
status: 400,
@@ -66,9 +61,7 @@ export const processApiEvent = async (event: ApiEvent, formId) => {
data,
});
} else {
throw Error(
`apiEvents: unsupported event type in event ${JSON.stringify(event)}`
);
throw Error(`apiEvents: unsupported event type in event ${JSON.stringify(event)}`);
}
// handle integrations
const pipelines = await prisma.pipeline.findMany({

View File

@@ -17,9 +17,7 @@ export function requireAuthentication(gssp) {
if (!token) {
return {
redirect: {
destination: `/auth/signin?callbackUrl=${encodeURIComponent(
resolvedUrl
)}`,
destination: `/auth/signin?callbackUrl=${encodeURIComponent(resolvedUrl)}`,
statusCode: 302,
},
};

View File

@@ -24,9 +24,7 @@ export const sendEmail = async (emailData: sendEmailData) => {
// debug: true,
});
const emailDefaults = {
from: `snoopForms <${
serverRuntimeConfig.mailFrom || "noreply@snoopforms.com"
}>`,
from: `snoopForms <${serverRuntimeConfig.mailFrom || "noreply@snoopforms.com"}>`,
};
await transporter.sendMail({ ...emailDefaults, ...emailData });
};
@@ -34,10 +32,8 @@ export const sendEmail = async (emailData: sendEmailData) => {
export const sendVerificationEmail = async (user) => {
const token = createToken(user.id, user.email, {
expiresIn: "1d",
})
const verifyLink = `${
serverRuntimeConfig.nextauthUrl
}/auth/verify?token=${encodeURIComponent(token)}`;
});
const verifyLink = `${serverRuntimeConfig.nextauthUrl}/auth/verify?token=${encodeURIComponent(token)}`;
const verificationRequestLink = `${
serverRuntimeConfig.nextauthUrl
}/auth/verification-requested?email=${encodeURIComponent(user.email)}`;
@@ -57,10 +53,10 @@ export const sendVerificationEmail = async (user) => {
export const sendForgotPasswordEmail = async (user) => {
const token = createToken(user.id, user.email, {
expiresIn: "1d",
})
const verifyLink = `${
serverRuntimeConfig.nextauthUrl
}/auth/reset-password?token=${encodeURIComponent(token)}`;
});
const verifyLink = `${serverRuntimeConfig.nextauthUrl}/auth/reset-password?token=${encodeURIComponent(
token
)}`;
await sendEmail({
to: user.email,
subject: "Reset your snoopForms password",

View File

@@ -56,15 +56,7 @@ export const getFormElementFieldSetter = (
elementId: string
) => {
return (input, field, parentField = "") =>
setFormElementField(
form,
mutateForm,
pageId,
elementId,
input,
field,
parentField
);
setFormElementField(form, mutateForm, pageId, elementId, input, field, parentField);
};
export const setFormElementField = (
@@ -77,17 +69,12 @@ export const setFormElementField = (
parentField: string = ""
) => {
const updatedForm = JSON.parse(JSON.stringify(form));
const elementIdx = getFormPage(updatedForm, pageId).elements.findIndex(
(e) => e.id === elementId
);
const elementIdx = getFormPage(updatedForm, pageId).elements.findIndex((e) => e.id === elementId);
if (typeof elementIdx === "undefined") {
throw Error(
`setFormElementField: unable to find element with id ${elementId}`
);
throw Error(`setFormElementField: unable to find element with id ${elementId}`);
}
if (parentField !== "") {
getFormPage(updatedForm, pageId).elements[elementIdx][parentField][field] =
input;
getFormPage(updatedForm, pageId).elements[elementIdx][parentField][field] = input;
} else {
getFormPage(updatedForm, pageId).elements[elementIdx][field] = input;
}

27
apps/web/lib/jwt.ts Normal file
View File

@@ -0,0 +1,27 @@
import jwt from "jsonwebtoken";
import getConfig from "next/config";
import { prisma } from "database";
const { serverRuntimeConfig } = getConfig();
export function createToken(userId, userEmail, options = {}) {
return jwt.sign({ id: userId }, serverRuntimeConfig.nextauthSecret + userEmail, options);
}
export async function verifyToken(token, userEmail = "") {
if (!userEmail) {
const { id } = jwt.decode(token);
const foundUser = await prisma.user.findUnique({
where: { id },
});
if (!foundUser) {
return null;
}
userEmail = foundUser.email;
}
return jwt.verify(token, serverRuntimeConfig.nextauthSecret + userEmail);
}

View File

@@ -1,7 +1,4 @@
import {
DocumentMagnifyingGlassIcon,
CommandLineIcon,
} from "@heroicons/react/24/outline";
import { DocumentMagnifyingGlassIcon, CommandLineIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
import { FaReact } from "react-icons/fa";

View File

@@ -1,8 +1,4 @@
import {
ChartBarIcon,
InboxIcon,
ArrowTrendingUpIcon,
} from "@heroicons/react/24/outline";
import { ChartBarIcon, InboxIcon, ArrowTrendingUpIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
export const useFormResultsSecondNavigation = (formId) => {

View File

@@ -2,10 +2,7 @@ import useSWR from "swr";
import { fetcher } from "./utils";
export const useNoCodeForm = (formId) => {
const { data, error, mutate } = useSWR(
`/api/forms/${formId}/nocodeform`,
fetcher
);
const { data, error, mutate } = useSWR(`/api/forms/${formId}/nocodeform`, fetcher);
return {
noCodeForm: data,
@@ -16,10 +13,7 @@ export const useNoCodeForm = (formId) => {
};
export const useNoCodeFormPublic = (formId) => {
const { data, error, mutate } = useSWR(
`/api/public/forms/${formId}/nocodeform`,
fetcher
);
const { data, error, mutate } = useSWR(`/api/public/forms/${formId}/nocodeform`, fetcher);
return {
noCodeForm: data,
@@ -39,9 +33,7 @@ export const createNoCodeForm = async (formId) => {
return await res.json();
} catch (error) {
console.error(error);
throw Error(
`createNoCodeForm: unable to create noCodeForm: ${error.message}`
);
throw Error(`createNoCodeForm: unable to create noCodeForm: ${error.message}`);
}
};

View File

@@ -2,10 +2,7 @@ import useSWR from "swr";
import { fetcher } from "./utils";
export const usePipeline = (formId: string, pipelineId: string) => {
const { data, error, mutate } = useSWR(
() => `/api/forms/${formId}/pipelines/${pipelineId}`,
fetcher
);
const { data, error, mutate } = useSWR(() => `/api/forms/${formId}/pipelines/${pipelineId}`, fetcher);
return {
pipeline: data,
@@ -16,10 +13,7 @@ export const usePipeline = (formId: string, pipelineId: string) => {
};
export const usePipelines = (formId: string) => {
const { data, error, mutate } = useSWR(
() => `/api/forms/${formId}/pipelines`,
fetcher
);
const { data, error, mutate } = useSWR(() => `/api/forms/${formId}/pipelines`, fetcher);
return {
pipelines: data,

View File

@@ -7,11 +7,7 @@ const enabled =
serverRuntimeConfig.posthogApiKey &&
serverRuntimeConfig.posthogApiHost;
export const capturePosthogEvent = async (
userId,
eventName,
properties = {}
) => {
export const capturePosthogEvent = async (userId, eventName, properties = {}) => {
if (!enabled) {
return;
}

View File

@@ -3,10 +3,7 @@ import { Schema, SubmissionSession, SubmissionSummary } from "./types";
import { fetcher } from "./utils";
export const useSubmissionSessions = (formId: string) => {
const { data, error, mutate } = useSWR(
() => `/api/forms/${formId}/submissionSessions`,
fetcher
);
const { data, error, mutate } = useSWR(() => `/api/forms/${formId}/submissionSessions`, fetcher);
return {
submissionSessions: data,
@@ -32,9 +29,7 @@ export const getSubmission = (submissionSession, schema) => {
const submissionPage = {
name: page.name,
type: page.type,
elements: page.elements
? JSON.parse(JSON.stringify(page.elements))
: [],
elements: page.elements ? JSON.parse(JSON.stringify(page.elements)) : [],
};
// search for elements in schema pages of type "form" and fill their value into the submission
if (page.type === "form") {
@@ -44,12 +39,8 @@ export const getSubmission = (submissionSession, schema) => {
if (typeof pageSubmission !== "undefined") {
for (const [elementIdx, element] of page.elements.entries()) {
if (element.type !== "submit") {
if (
"submission" in pageSubmission.data &&
element.name in pageSubmission.data?.submission
) {
submissionPage.elements[elementIdx].value =
pageSubmission.data.submission[element.name];
if ("submission" in pageSubmission.data && element.name in pageSubmission.data?.submission) {
submissionPage.elements[elementIdx].value = pageSubmission.data.submission[element.name];
}
}
}
@@ -61,22 +52,17 @@ export const getSubmission = (submissionSession, schema) => {
return submission;
};
export const getSubmissionAnalytics = (
submissionSessions: SubmissionSession[]
) => {
export const getSubmissionAnalytics = (submissionSessions: SubmissionSession[]) => {
let totalSubmissions = 0;
let lastSubmissionAt = null;
for (const submissionSession of submissionSessions) {
// collect unique users
if (submissionSession.events.length > 0) {
totalSubmissions += 1;
const lastSubmission =
submissionSession.events[submissionSession.events.length - 1];
const lastSubmission = submissionSession.events[submissionSession.events.length - 1];
if (!lastSubmissionAt) {
lastSubmissionAt = lastSubmission.createdAt;
} else if (
Date.parse(lastSubmission.createdAt) > Date.parse(lastSubmissionAt)
) {
} else if (Date.parse(lastSubmission.createdAt) > Date.parse(lastSubmissionAt)) {
lastSubmissionAt = lastSubmission.createdAt;
}
}
@@ -87,36 +73,20 @@ export const getSubmissionAnalytics = (
};
};
export const getSubmissionSummary = (
submissionSessions: SubmissionSession[],
schema: Schema
) => {
export const getSubmissionSummary = (submissionSessions: SubmissionSession[], schema: Schema) => {
if (!schema) return;
const summary: SubmissionSummary = JSON.parse(JSON.stringify(schema));
// iterate through SubmissionSessions and add values to summary
for (const submissionSession of submissionSessions) {
for (const submissionEvent of submissionSession.events) {
if (submissionEvent.type === "pageSubmission") {
const summaryPage = summary.pages.find(
(p) => p.name === submissionEvent.data.pageName
);
const summaryPage = summary.pages.find((p) => p.name === submissionEvent.data.pageName);
if (summaryPage.type === "form" && submissionEvent.data.submission) {
for (const [elementName, elementValue] of Object.entries(
submissionEvent.data.submission
)) {
const elementInSummary = summaryPage.elements.find(
(e) => e.name === elementName
);
for (const [elementName, elementValue] of Object.entries(submissionEvent.data.submission)) {
const elementInSummary = summaryPage.elements.find((e) => e.name === elementName);
if (typeof elementInSummary !== "undefined") {
if (
[
"email",
"number",
"phone",
"text",
"textarea",
"website",
].includes(elementInSummary.type)
["email", "number", "phone", "text", "textarea", "website"].includes(elementInSummary.type)
) {
if (!("summary" in elementInSummary)) {
elementInSummary.summary = [];
@@ -125,9 +95,7 @@ export const getSubmissionSummary = (
} else if (elementInSummary.type === "checkbox") {
// checkbox values are a list of values
for (const value of elementValue) {
const optionInSummary = elementInSummary.options.find(
(o) => o.value === value
);
const optionInSummary = elementInSummary.options.find((o) => o.value === value);
if (typeof optionInSummary !== "undefined") {
if (!("summary" in optionInSummary)) {
optionInSummary.summary = 0;
@@ -136,9 +104,7 @@ export const getSubmissionSummary = (
}
}
} else if (elementInSummary.type === "radio") {
const optionInSummary = elementInSummary.options.find(
(o) => o.value === elementValue
);
const optionInSummary = elementInSummary.options.find((o) => o.value === elementValue);
if (typeof optionInSummary !== "undefined") {
if (!("summary" in optionInSummary)) {
optionInSummary.summary = 0;

View File

@@ -115,10 +115,7 @@ export type updateSchemaEvent = {
data: Schema;
};
export type ApiEvent =
| pageSubmissionEvent
| submissionCompletedEvent
| updateSchemaEvent;
export type ApiEvent = pageSubmissionEvent | submissionCompletedEvent | updateSchemaEvent;
export type WebhookEvent = Event & { formId: string; timestamp: string };

View File

@@ -69,7 +69,7 @@ export const resetPassword = async (token, password) => {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
token,
hashedPassword
hashedPassword,
}),
});
if (res.status !== 200) {

View File

@@ -27,10 +27,7 @@ export const shuffle = (array) => {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
[array[currentIndex], array[randomIndex]] = [
array[randomIndex],
array[currentIndex],
];
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
}
return array;
@@ -120,8 +117,7 @@ export const timeSince = (dateString: string) => {
export const generateId = (length) => {
let result = "";
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));

View File

@@ -3,6 +3,10 @@ var path = require("path");
const nextConfig = {
reactStrictMode: false,
output: "standalone",
experimental: {
outputFileTracingRoot: path.join(__dirname, "../../"),
},
serverRuntimeConfig: {
// Will only be available on the server side
nextauthSecret: process.env.NEXTAUTH_SECRET,

61
apps/web/package.json Normal file
View File

@@ -0,0 +1,61 @@
{
"name": "web",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"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",
"bcryptjs": "^2.4.3",
"chart.js": "^3.9.1",
"crypto": "^1.0.1",
"date-fns": "^2.29.3",
"editorjs-drag-drop": "^1.1.7",
"editorjs-undo": "^2.0.9",
"highlight.js": "^11.6.0",
"json2csv": "^5.0.7",
"jsonwebtoken": "^8.5.1",
"next": "12.3.1",
"next-auth": "^4.13.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-loader-spinner": "^5.3.4",
"react-toastify": "^9.0.8",
"sanitize-html": "^2.7.2",
"sharp": "^0.31.1",
"swr": "^1.3.0",
"ui": "workspace:*",
"uuid": "^9.0.0"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.3",
"@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",
"autoprefixer": "^10.4.12",
"database": "workspace:*",
"eslint": "8.25.0",
"eslint-config-custom": "workspace:*",
"postcss": "^8.4.17",
"tailwind-config": "workspace:*",
"tailwindcss": "^3.1.8",
"ts-node": "^10.9.1",
"tsconfig": "workspace:*",
"typescript": "4.8.4"
}
}

30
apps/web/pages/404.tsx Normal file
View File

@@ -0,0 +1,30 @@
import Image from "next/image";
import BaseLayoutUnauthorized from "../components/layout/BaseLayoutUnauthorized";
import Link from "next/link";
export default function Error404Page() {
return (
<BaseLayoutUnauthorized title="Page not found">
<div className="bg-ui-gray-light flex min-h-screen">
<div className="mx-auto flex flex-1 flex-col justify-center px-4 py-12 sm:px-6 lg:flex-none lg:px-20 xl:px-24">
<div className="mx-auto w-full max-w-sm p-8 lg:w-96">
<div>
<Image src="/img/snoopforms-logo.svg" alt="snoopForms logo" width={500} height={89} />
</div>
<div className="mt-8">
<h1 className="leading-2 mb-4 text-center font-bold">This page does not exist!</h1>
<p className="text-center">
Sorry, the page you were looking for could not be found. Please make sure the URL is correct
or{" "}
<span className="underline">
<Link href="/">go back to the homepage</Link>
</span>
.
</p>
</div>
</div>
</div>
</div>
</BaseLayoutUnauthorized>
);
}

View File

@@ -1,15 +1,12 @@
import "highlight.js/styles/tokyo-night-dark.css";
import { SessionProvider } from "next-auth/react";
import { AppProps } from "next/app";
import { ToastContainer } from "react-toastify";
import "../styles/editorjs.css";
import "ui/styles.css";
import "../styles/globals.css";
import "../styles/toastify.css";
function SnoopApp({
Component,
pageProps: { session, ...pageProps },
}: AppProps) {
function SnoopApp({ Component, pageProps: { session, ...pageProps } }) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />

View File

@@ -0,0 +1,20 @@
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html className="scroll-smooth">
<Head>
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png" />
<link rel="manifest" href="/favicons/site.webmanifest" />
<link rel="mask-icon" href="/favicons/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="theme-color" content="#0D0010" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}

View File

@@ -2,7 +2,7 @@ import { NextApiRequest, NextApiResponse } from "next";
import getConfig from "next/config";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { prisma } from "../../../lib/prisma";
import { prisma } from "database";
import { verifyPassword } from "../../../lib/auth";
import { verifyToken } from "../../../lib/jwt";
@@ -54,10 +54,7 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
throw new Error("Incorrect password");
}
const isValid = await verifyPassword(
credentials.password,
user.password
);
const isValid = await verifyPassword(credentials.password, user.password);
if (!isValid) {
throw new Error("Incorrect password");
@@ -89,7 +86,7 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
async authorize(credentials, _req) {
let user;
try {
const { id } = await verifyToken(credentials?.token)
const { id } = await verifyToken(credentials?.token);
user = await prisma.user.findUnique({
where: {
id: id,
@@ -126,14 +123,12 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
}),
],
callbacks: {
async signIn({ user }) {
async signIn({ user }: any) {
if (user.emailVerified || publicRuntimeConfig.emailVerificationDisabled) {
return true;
} else {
// Return false to display a default error message or you can return a URL to redirect to
return `/auth/verification-requested?email=${encodeURIComponent(
user.email
)}`;
return `/auth/verification-requested?email=${encodeURIComponent(user.email)}`;
}
},
},

View File

@@ -3,10 +3,7 @@ import NextCors from "nextjs-cors";
import { processApiEvent, validateEvents } from "../../../../lib/apiEvents";
///api/submissionSession
export default async function handle(
req: NextApiRequest,
res: NextApiResponse
) {
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
await NextCors(req, res, {
// Options
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"],
@@ -34,8 +31,6 @@ export default async function handle(
}
// Unknown HTTP Method
else {
throw new Error(
`The HTTP ${req.method} method is not supported by this route.`
);
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}

View File

@@ -1,12 +1,9 @@
import type { NextApiResponse, NextApiRequest } from "next";
import { getSession } from "next-auth/react";
import { formHasOwnership } from "../../../../lib/api";
import { prisma } from "../../../../lib/prisma";
import { prisma } from "database";
export default async function handle(
req: NextApiRequest,
res: NextApiResponse
) {
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const session = await getSession({ req: req });
if (!session) {
@@ -17,9 +14,7 @@ export default async function handle(
const ownership = await formHasOwnership(session, formId);
if (!ownership) {
return res
.status(401)
.json({ message: "You are not authorized to access this form" });
return res.status(401).json({ message: "You are not authorized to access this form" });
}
// GET /api/forms/:id
@@ -52,8 +47,6 @@ export default async function handle(
}
// Unknown HTTP Method
else {
throw new Error(
`The HTTP ${req.method} method is not supported by this route.`
);
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}

View File

@@ -1,12 +1,9 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "next-auth/react";
import { formHasOwnership } from "../../../../../lib/api";
import { prisma } from "../../../../../lib/prisma";
import { prisma } from "database";
export default async function handle(
req: NextApiRequest,
res: NextApiResponse
) {
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const session = await getSession({ req: req });
if (!session) {
@@ -17,9 +14,7 @@ export default async function handle(
const ownership = await formHasOwnership(session, formId);
if (!ownership) {
return res
.status(401)
.json({ message: "You are not authorized to access this noCodeForm" });
return res.status(401).json({ message: "You are not authorized to access this noCodeForm" });
}
// GET /api/forms/:id/nocodeform
@@ -63,8 +58,6 @@ export default async function handle(
}
// Unknown HTTP Method
else {
throw new Error(
`The HTTP ${req.method} method is not supported by this route.`
);
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}

View File

@@ -2,12 +2,9 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "next-auth/react";
import NextCors from "nextjs-cors";
import { formHasOwnership } from "../../../../../../lib/api";
import { prisma } from "../../../../../../lib/prisma";
import { prisma } from "database";
export default async function handle(
req: NextApiRequest,
res: NextApiResponse
) {
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
await NextCors(req, res, {
// Options
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"],
@@ -65,8 +62,6 @@ export default async function handle(
}
// Unknown HTTP Method
else {
throw new Error(
`The HTTP ${req.method} method is not supported by this route.`
);
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}

View File

@@ -2,12 +2,9 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "next-auth/react";
import NextCors from "nextjs-cors";
import { formHasOwnership } from "../../../../../lib/api";
import { prisma } from "../../../../../lib/prisma";
import { prisma } from "database";
export default async function handle(
req: NextApiRequest,
res: NextApiResponse
) {
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
const formId = req.query.id.toString();
await NextCors(req, res, {
@@ -25,9 +22,7 @@ export default async function handle(
// check if user is form owner
const ownership = await formHasOwnership(session, formId);
if (!ownership) {
return res
.status(401)
.json({ message: "You are not authorized to change this noCodeForm" });
return res.status(401).json({ message: "You are not authorized to change this noCodeForm" });
}
// GET /api/forms/[formId]/pipelines
@@ -58,8 +53,6 @@ export default async function handle(
}
// Unknown HTTP Method
else {
throw new Error(
`The HTTP ${req.method} method is not supported by this route.`
);
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}

View File

@@ -2,12 +2,9 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "next-auth/react";
import NextCors from "nextjs-cors";
import { formHasOwnership } from "../../../../../../lib/api";
import { prisma } from "../../../../../../lib/prisma";
import { prisma } from "database";
export default async function handle(
req: NextApiRequest,
res: NextApiResponse
) {
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
await NextCors(req, res, {
// Options
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"],
@@ -43,8 +40,6 @@ export default async function handle(
}
// Unknown HTTP Method
else {
throw new Error(
`The HTTP ${req.method} method is not supported by this route.`
);
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}

View File

@@ -2,12 +2,9 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "next-auth/react";
import NextCors from "nextjs-cors";
import { formHasOwnership } from "../../../../../lib/api";
import { prisma } from "../../../../../lib/prisma";
import { prisma } from "database";
export default async function handle(
req: NextApiRequest,
res: NextApiResponse
) {
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
await NextCors(req, res, {
// Options
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"],
@@ -28,8 +25,7 @@ export default async function handle(
const ownership = await formHasOwnership(session, formId);
if (!ownership) {
return res.status(401).json({
message:
"You are not authorized to access this form and their submissions",
message: "You are not authorized to access this form and their submissions",
});
}
@@ -62,8 +58,6 @@ export default async function handle(
}
// Unknown HTTP Method
else {
throw new Error(
`The HTTP ${req.method} method is not supported by this route.`
);
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}

View File

@@ -1,13 +1,10 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "../../../lib/prisma";
import { prisma } from "database";
import { getSession } from "next-auth/react";
import { generateId } from "../../../lib/utils";
import { capturePosthogEvent } from "../../../lib/posthog";
export default async function handle(
req: NextApiRequest,
res: NextApiResponse
) {
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const session = await getSession({ req: req });
if (!session) {
@@ -65,9 +62,7 @@ export default async function handle(
// Unknown HTTP Method
else {
throw new Error(
`The HTTP ${req.method} method is not supported by this route.`
);
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}

View File

@@ -1,10 +1,7 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "../../../../../../lib/prisma";
import { prisma } from "database";
export default async function handle(
req: NextApiRequest,
res: NextApiResponse
) {
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
const formId = req.query.id.toString();
// GET /api/forms/:id/nocodeform

View File

@@ -1,12 +1,9 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "../../../../lib/prisma";
import { prisma } from "database";
import { capturePosthogEvent } from "../../../../lib/posthog";
import { sendForgotPasswordEmail } from "../../../../lib/email";
export default async function handle(
req: NextApiRequest,
res: NextApiResponse
) {
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// POST /api/public/users/forgot-password
// Sends a reset password email to the user
// Required fields in body: email
@@ -39,8 +36,6 @@ export default async function handle(
// Unknown HTTP Method
else {
throw new Error(
`The HTTP ${req.method} method is not supported by this route.`
);
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}

Some files were not shown because too many files have changed in this diff Show More