diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1e9b43fa..d43f6a0d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -7,8 +7,8 @@ on: branches: - main jobs: - lint: - name: Lint Go + lint-test-go: + name: Lint and Test Go runs-on: ubuntu-latest steps: - name: Install Go @@ -24,18 +24,6 @@ jobs: with: working-directory: ./api - test: - name: Test Go - runs-on: ubuntu-latest - steps: - - name: Install Go - uses: actions/setup-go@v5 - with: - go-version: 1.24 - - - name: Check out code - uses: actions/checkout@v4 - - name: Run Tests working-directory: ./api run: go test -v -bench=. -race ./... @@ -45,7 +33,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node_version: [20] + node_version: [22] steps: - name: Check out code diff --git a/README.md b/README.md index f93f1b97..d3e5430b 100644 --- a/README.md +++ b/README.md @@ -266,30 +266,17 @@ http://localhost:9900/app/surveys/{SURVEY_ID}/sessions?limit=100&offset=0&sort_b Where `{SURVEY_ID}` id the UUID of a given survey. -## Screenshots - -

- - -

- ## Installation & Deployment -You can build and run both API and UI with Docker Compose: +### API and Postgres with Docker Compose + +You can build and run both API and Postgres with Docker Compose: ``` docker-compose up -d --build ``` -And you should be able to access the UI on [localhost:3000](http://localhost:3000) (default basic auth: `user:pass`). - -You can deploy individual services to any cloud provider or self host them. - -- Go backend. -- Next.js frontend. -- Postgres database. - -### Environment Variables +Environment variables: API: @@ -297,22 +284,25 @@ API: - `SURVEYS_DIR` - Directory with surveys, e.g. `/root/surveys`. It's suggested to use mounted volume for this directory. - `UPLOADS_DIR` - Directory for uploading files from the survey forms. -UI: - -- `CONSOLE_API_ADDR` - Public address of the Go backend. Need to be accessible from the browser. -- `CONSOLE_API_ADDR_INTERNAL` - Internal address of the Go backend, e.g. `http://api:8080` (could be the same as `CONSOLE_API_ADDR`). -- `IRON_SESSION_SECRET` - Secret for session encryption -- `HTTP_BASIC_AUTH` - Format: `user:pass` for basic auth (optional) - ### Run UI with npm -It's also possible to run UI using `npm`: - ``` +cd ui npm install npm run dev ``` +### Run API locally + +Assuming you have Postgres running locally (`docker-compose up -d postgres`), you can run the API with: + +``` +cd api +export DATABASE_URL="postgres://user:pass@localhost:5432/formulosity?sslmode=disable" +export SURVEYS_DIR="./surveys" +go run main.go +``` + ## Tech Stack - Backend: Go, Postgres diff --git a/compose.yaml b/compose.yaml index a1f3fa66..3346fd38 100644 --- a/compose.yaml +++ b/compose.yaml @@ -14,19 +14,6 @@ services: volumes: - ./api/surveys:/root/surveys - ./api/uploads:/root/uploads - ui: - restart: always - build: - context: ./ui - ports: - - "3000:3000" - environment: - - CONSOLE_API_ADDR_INTERNAL=http://api:8080 - - CONSOLE_API_ADDR=http://localhost:9900 - - IRON_SESSION_SECRET=e75af92dffba8065f2730472f45f2046941fe35f361739d31992f42d88d6bf6c - - HTTP_BASIC_AUTH=user:pass - depends_on: - - api postgres: image: postgres:16.0-alpine restart: always diff --git a/screenshots/app.png b/screenshots/app.png deleted file mode 100644 index 263f27c4..00000000 Binary files a/screenshots/app.png and /dev/null differ diff --git a/screenshots/survey.png b/screenshots/survey.png deleted file mode 100644 index efc2a734..00000000 Binary files a/screenshots/survey.png and /dev/null differ diff --git a/ui/.env.example b/ui/.env.example index e71db2e3..512a3b6d 100644 --- a/ui/.env.example +++ b/ui/.env.example @@ -1,5 +1,4 @@ -CONSOLE_API_ADDR=http://localhost:9900 -CONSOLE_API_ADDR_INTERNAL=http://localhost:9900 +NEXT_PUBLIC_API_ADDR=http://localhost:9900 IRON_SESSION_SECRET=not_very_secret_replace_me HTTP_BASIC_AUTH=user:pass diff --git a/ui/Dockerfile b/ui/Dockerfile deleted file mode 100644 index 0ea602c7..00000000 --- a/ui/Dockerfile +++ /dev/null @@ -1,41 +0,0 @@ -FROM node:20-alpine AS base - -FROM base AS deps -RUN apk add --no-cache libc6-compat -WORKDIR /app - -COPY package.json package-lock.json ./ -RUN npm ci - -FROM base AS builder -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY . . - -ENV NODE_ENV=production - -RUN npm run build - -FROM base AS runner -WORKDIR /app - -ENV NODE_ENV=production - -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs - -COPY --from=builder /app/public ./public - -RUN mkdir .next -RUN chown nextjs:nodejs .next - -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static - -USER nextjs - -EXPOSE 3000 - -ENV PORT=3000 - -CMD ["node", "server.js"] diff --git a/ui/package-lock.json b/ui/package-lock.json index f00b0372..5be99ef3 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -9,7 +9,6 @@ "flowbite": "^1.8.1", "flowbite-react": "^0.6.4", "formulosity-ui": "file:", - "iron-session": "^6.3.1", "moment": "^2.29.4", "next": "^14.2.15", "react": "18.2.0", @@ -459,42 +458,6 @@ "node": ">= 8" } }, - "node_modules/@peculiar/asn1-schema": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.6.tgz", - "integrity": "sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA==", - "dependencies": { - "asn1js": "^3.0.5", - "pvtsutils": "^1.3.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@peculiar/json-schema": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", - "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@peculiar/webcrypto": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.4.3.tgz", - "integrity": "sha512-VtaY4spKTdN5LjJ04im/d/joXuvLbQdgy5Z4DXF4MFZhQ+MTrejbNMkfZBp1Bs3O5+bFqnJgyGdPuZQflvIa5A==", - "dependencies": { - "@peculiar/asn1-schema": "^2.3.6", - "@peculiar/json-schema": "^1.1.12", - "pvtsutils": "^1.3.2", - "tslib": "^2.5.0", - "webcrypto-core": "^1.7.7" - }, - "engines": { - "node": ">=10.12.0" - } - }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -536,84 +499,6 @@ "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" } }, - "node_modules/@types/accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.3", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz", - "integrity": "sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.36", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", - "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/content-disposition": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.6.tgz", - "integrity": "sha512-GmShTb4qA9+HMPPaV2+Up8tJafgi38geFi7vL4qAM7k8BwjoelgHZqEUKJZLvughUw22h6vD/wvwN4IUCaWpDA==" - }, - "node_modules/@types/cookie": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.2.tgz", - "integrity": "sha512-DBpRoJGKJZn7RY92dPrgoMew8xCWc2P71beqsjyhEI/Ds9mOyVmBwtekyfhpwFIVt1WrxTonFifiOZ62V8CnNA==" - }, - "node_modules/@types/cookies": { - "version": "0.7.8", - "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.8.tgz", - "integrity": "sha512-y6KhF1GtsLERUpqOV+qZJrjUGzc0GE6UTa0b5Z/LZ7Nm2mKSdCXmS6Kdnl7fctPNnMSouHjxqEWI12/YqQfk5w==", - "dependencies": { - "@types/connect": "*", - "@types/express": "*", - "@types/keygrip": "*", - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.18", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.18.tgz", - "integrity": "sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.37", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.37.tgz", - "integrity": "sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-assert": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.3.tgz", - "integrity": "sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA==" - }, - "node_modules/@types/http-errors": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz", - "integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==" - }, "node_modules/@types/json-schema": { "version": "7.0.13", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", @@ -626,43 +511,11 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, - "node_modules/@types/keygrip": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.3.tgz", - "integrity": "sha512-tfzBBb7OV2PbUfKbG6zRE5UbmtdLVCKT/XT364Z9ny6pXNbd9GnIB6aFYpq2A5lZ6mq9bhXgK6h5MFGNwhMmuQ==" - }, - "node_modules/@types/koa": { - "version": "2.13.9", - "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.13.9.tgz", - "integrity": "sha512-tPX3cN1dGrMn+sjCDEiQqXH2AqlPoPd594S/8zxwUm/ZbPsQXKqHPUypr2gjCPhHUc+nDJLduhh5lXI/1olnGQ==", - "dependencies": { - "@types/accepts": "*", - "@types/content-disposition": "*", - "@types/cookies": "*", - "@types/http-assert": "*", - "@types/http-errors": "*", - "@types/keygrip": "*", - "@types/koa-compose": "*", - "@types/node": "*" - } - }, - "node_modules/@types/koa-compose": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.6.tgz", - "integrity": "sha512-PHiciWxH3NRyAaxUdEDE1NIZNfvhgtPlsdkjRPazHC6weqt90Jr0uLhIQs+SDwC8HQ/jnA7UQP6xOqGFB7ugWw==", - "dependencies": { - "@types/koa": "*" - } - }, - "node_modules/@types/mime": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", - "integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==" - }, "node_modules/@types/node": { "version": "18.18.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.0.tgz", - "integrity": "sha512-3xA4X31gHT1F1l38ATDIL9GpRLdwVhnEFC8Uikv5ZLlXATwrCYyPq7ZWHxzxc3J/30SUiwiYT+bQe0/XvKlWbw==" + "integrity": "sha512-3xA4X31gHT1F1l38ATDIL9GpRLdwVhnEFC8Uikv5ZLlXATwrCYyPq7ZWHxzxc3J/30SUiwiYT+bQe0/XvKlWbw==", + "dev": true }, "node_modules/@types/prop-types": { "version": "15.7.7", @@ -670,16 +523,6 @@ "integrity": "sha512-FbtmBWCcSa2J4zL781Zf1p5YUBXQomPEcep9QZCfRfQgTxz3pJWiDFLebohZ9fFntX5ibzOkSsrJ0TEew8cAog==", "dev": true }, - "node_modules/@types/qs": { - "version": "6.9.8", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz", - "integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==" - }, - "node_modules/@types/range-parser": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz", - "integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==" - }, "node_modules/@types/react": { "version": "18.2.23", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.23.tgz", @@ -703,25 +546,6 @@ "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", "dev": true }, - "node_modules/@types/send": { - "version": "0.17.2", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.2.tgz", - "integrity": "sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.3.tgz", - "integrity": "sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==", - "dependencies": { - "@types/http-errors": "*", - "@types/mime": "*", - "@types/node": "*" - } - }, "node_modules/@types/sortablejs": { "version": "1.15.8", "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.8.tgz", @@ -1156,19 +980,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asn1js": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", - "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", - "dependencies": { - "pvtsutils": "^1.3.2", - "pvutils": "^1.1.3", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -1256,25 +1067,6 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1336,29 +1128,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -1513,14 +1282,6 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, - "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2715,25 +2476,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -2796,55 +2538,6 @@ "node": ">= 0.4" } }, - "node_modules/iron-session": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/iron-session/-/iron-session-6.3.1.tgz", - "integrity": "sha512-3UJ7y2vk/WomAtEySmPgM6qtYF1cZ3tXuWX5GsVX4PJXAcs5y/sV9HuSfpjKS6HkTL/OhZcTDWJNLZ7w+Erx3A==", - "dependencies": { - "@peculiar/webcrypto": "^1.4.0", - "@types/cookie": "^0.5.1", - "@types/express": "^4.17.13", - "@types/koa": "^2.13.5", - "@types/node": "^17.0.41", - "cookie": "^0.5.0", - "iron-webcrypto": "^0.2.5" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "express": ">=4", - "koa": ">=2", - "next": ">=10" - }, - "peerDependenciesMeta": { - "express": { - "optional": true - }, - "koa": { - "optional": true - }, - "next": { - "optional": true - } - } - }, - "node_modules/iron-session/node_modules/@types/node": { - "version": "17.0.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", - "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==" - }, - "node_modules/iron-webcrypto": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-0.2.8.tgz", - "integrity": "sha512-YPdCvjFMOBjXaYuDj5tiHst5CEk6Xw84Jo8Y2+jzhMceclAnb3+vNPP/CTtb5fO2ZEuXEaO4N+w62Vfko757KA==", - "dependencies": { - "buffer": "^6" - }, - "funding": { - "url": "https://github.com/sponsors/brc-dd" - } - }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -4017,22 +3710,6 @@ "node": ">=6" } }, - "node_modules/pvtsutils": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.5.tgz", - "integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==", - "dependencies": { - "tslib": "^2.6.1" - } - }, - "node_modules/pvutils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", - "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4897,18 +4574,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/webcrypto-core": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.7.7.tgz", - "integrity": "sha512-7FjigXNsBfopEj+5DV2nhNpfic2vumtjjgPmeDKk45z+MJwXKKfhPB7118Pfzrmh4jqOMST6Ch37iPAHoImg5g==", - "dependencies": { - "@peculiar/asn1-schema": "^2.3.6", - "@peculiar/json-schema": "^1.1.12", - "asn1js": "^3.0.1", - "pvtsutils": "^1.3.2", - "tslib": "^2.4.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/ui/package.json b/ui/package.json index 7451ab27..1333faaf 100644 --- a/ui/package.json +++ b/ui/package.json @@ -15,7 +15,6 @@ "flowbite": "^1.8.1", "flowbite-react": "^0.6.4", "formulosity-ui": "file:", - "iron-session": "^6.3.1", "moment": "^2.29.4", "next": "^14.2.15", "react": "18.2.0", diff --git a/ui/src/app/app/page.tsx b/ui/src/app/app/page.tsx index f64a244b..6fb7761a 100644 --- a/ui/src/app/app/page.tsx +++ b/ui/src/app/app/page.tsx @@ -1,23 +1,37 @@ -import { Metadata } from 'next' +'use client' + +import { useEffect, useState } from 'react' import AppLayout from 'components/app/AppLayout' import { SurveysPage } from 'components/app/SurveysPage' import { ErrCode } from 'components/ui/ErrCode' import { getSurveys } from 'lib/api' +import { Survey } from 'lib/types' -export const metadata: Metadata = { - title: 'Formulosity', -} +export default function AppPage() { + const [surveys, setSurveys] = useState([]) + const [errMsg, setErrMsg] = useState('') + const [loading, setLoading] = useState(true) -export default async function AppPage() { - let errMsg = '' + useEffect(() => { + const fetchSurveys = async () => { + const surveysResp = await getSurveys() + if (surveysResp.error) { + setErrMsg('Failed to fetch surveys') + } else { + setSurveys(surveysResp.data.data) + } + setLoading(false) + } + fetchSurveys() + }, []) - let surveys = [] - const surveysResp = await getSurveys() - if (surveysResp.error) { - errMsg = 'Failed to fetch surveys' - } else { - surveys = surveysResp.data.data + if (loading) { + return ( + +
Loading...
+
+ ) } if (errMsg) { @@ -28,11 +42,9 @@ export default async function AppPage() { ) } - const apiURL = process.env.CONSOLE_API_ADDR || '' - return ( - + ) } diff --git a/ui/src/app/app/surveys/[survey_uuid]/responses/page.tsx b/ui/src/app/app/surveys/[survey_uuid]/responses/page.tsx index 828559ea..9374662b 100644 --- a/ui/src/app/app/surveys/[survey_uuid]/responses/page.tsx +++ b/ui/src/app/app/surveys/[survey_uuid]/responses/page.tsx @@ -1,4 +1,7 @@ -import { Metadata } from 'next' +'use client' + +import { useEffect, useState } from 'react' +import { useParams } from 'next/navigation' import AppLayout from 'components/app/AppLayout' import { ErrCode } from 'components/ui/ErrCode' @@ -6,44 +9,62 @@ import { getSurveys, getSurveySessions } from 'lib/api' import { Survey, SurveySessionsLimit } from 'lib/types' import { SurveyResponsesPage } from 'components/app/SurveyResponsesPage' -export const metadata: Metadata = { - title: 'Survey Responses', -} +export default function ResponsesPage() { + const params = useParams() + const [currentSurvey, setCurrentSurvey] = useState( + undefined + ) + const [errMsg, setErrMsg] = useState('') + const [loading, setLoading] = useState(true) -export default async function ResponsesPage({ - params, -}: { - params: { survey_uuid: string } -}) { - let errMsg = '' + useEffect(() => { + const fetchData = async () => { + if (!params.survey_uuid) return - let currentSurvey = undefined - const surveysResp = await getSurveys() - if (surveysResp.error) { - errMsg = 'Unable to fetch surveys' - } else { - const surveys = surveysResp.data.data - const survey = surveys.find( - (survey: Survey) => survey.uuid === params.survey_uuid - ) - if (!survey) { - errMsg = 'Survey not found' - } else { - currentSurvey = survey + const surveysResp = await getSurveys() + if (surveysResp.error) { + setErrMsg('Unable to fetch surveys') + setLoading(false) + return + } + + const surveys = surveysResp.data.data + const survey = surveys.find( + (survey: Survey) => survey.uuid === params.survey_uuid + ) + + if (!survey) { + setErrMsg('Survey not found') + setLoading(false) + return + } const surveySessionsResp = await getSurveySessions( - currentSurvey.uuid, - `limit=${SurveySessionsLimit}&offset=0&sort_by=created_at&order=desc`, - '' + survey.uuid, + `limit=${SurveySessionsLimit}&offset=0&sort_by=created_at&order=desc` ) + if (surveySessionsResp.error) { - errMsg = 'Unable to fetch survey sessions' + setErrMsg('Unable to fetch survey sessions') } else { - currentSurvey = surveySessionsResp.data.data.survey - currentSurvey.sessions = surveySessionsResp.data.data.sessions - currentSurvey.pages_count = surveySessionsResp.data.data.pages_count + const updatedSurvey = surveySessionsResp.data.data.survey + updatedSurvey.sessions = surveySessionsResp.data.data.sessions + updatedSurvey.pages_count = surveySessionsResp.data.data.pages_count + setCurrentSurvey(updatedSurvey) } + + setLoading(false) } + + fetchData() + }, [params.survey_uuid]) + + if (loading) { + return ( + +
Loading...
+
+ ) } if (errMsg) { @@ -54,11 +75,9 @@ export default async function ResponsesPage({ ) } - const apiURL = process.env.CONSOLE_API_ADDR || '' - return ( - + ) } diff --git a/ui/src/app/layout.tsx b/ui/src/app/layout.tsx index 448e8195..6e7e5464 100644 --- a/ui/src/app/layout.tsx +++ b/ui/src/app/layout.tsx @@ -1,34 +1,11 @@ +'use client' + import 'styles/global.css' import { ReactNode } from 'react' -import { Metadata } from 'next' -import { siteConfig } from 'lib/siteConfig' - -export const metadata: Metadata = { - title: { - default: siteConfig.name, - template: `%s | ${siteConfig.name}`, - }, - description: siteConfig.description, - keywords: [], - alternates: { - canonical: '/', - }, - openGraph: { - type: 'website', - locale: 'en_US', - title: siteConfig.name, - description: siteConfig.description, - siteName: siteConfig.name, - }, - icons: { - icon: '/favicon.ico', - }, - manifest: '/manifest.webmanifest', -} type LayoutProps = { children?: ReactNode } -export default async function RootLayout({ children }: LayoutProps) { +export default function RootLayout({ children }: LayoutProps) { return ( diff --git a/ui/src/app/survey/[url_slug]/page.tsx b/ui/src/app/survey/[url_slug]/page.tsx index cf7b3d38..37ae63e3 100644 --- a/ui/src/app/survey/[url_slug]/page.tsx +++ b/ui/src/app/survey/[url_slug]/page.tsx @@ -1,69 +1,59 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useParams } from 'next/navigation' import { getSurvey } from 'lib/api' import { Survey } from 'lib/types' import SurveyLayout from 'components/app/survey/SurveyLayout' import SurveyNotFound from 'components/app/survey/SurveyNotFound' import SurveyForm from 'components/app/survey/SurveyForm' -import { headers } from 'next/headers' -export async function generateMetadata({ - params, -}: { - params: { url_slug: string } -}) { - const headersList = headers() - const surveyResp = await getSurvey( - headersList.get('host') as string, - params.url_slug - ) - if ( - surveyResp.error || - !surveyResp.data.data || - !surveyResp.data.data.config - ) { - return { - title: 'Survey not found', +export default function SurveyPage() { + const params = useParams() + const [survey, setSurvey] = useState(null) + const [loading, setLoading] = useState(true) + const [notFound, setNotFound] = useState(false) + + useEffect(() => { + const fetchSurvey = async () => { + if (!params.url_slug) return + + const surveyResp = await getSurvey(params.url_slug as string) + + if ( + surveyResp.error || + !surveyResp.data.data || + !surveyResp.data.data.config + ) { + setNotFound(true) + } else { + setSurvey(surveyResp.data.data as Survey) + } + setLoading(false) } - } - const survey = surveyResp.data.data as Survey + fetchSurvey() + }, [params.url_slug]) - return { - title: survey.config.title, + if (loading) { + return ( + +
Loading...
+
+ ) } -} -export default async function SurveyPage({ - params, -}: { - params: { url_slug: string } -}) { - const headersList = headers() - const surveyResp = await getSurvey( - headersList.get('host') as string, - params.url_slug - ) - const apiURL = process.env.CONSOLE_API_ADDR || '' - if ( - surveyResp.error || - !surveyResp.data.data || - !surveyResp.data.data.config - ) { + if (notFound || !survey) { return ( - + ) } - const survey = surveyResp.data.data as Survey - return ( - - + + ) } diff --git a/ui/src/components/app/SurveyResponsesPage.tsx b/ui/src/components/app/SurveyResponsesPage.tsx index 8f4efc5a..8697b555 100644 --- a/ui/src/components/app/SurveyResponsesPage.tsx +++ b/ui/src/components/app/SurveyResponsesPage.tsx @@ -24,12 +24,10 @@ import moment from 'moment' type SurveyResponsesPageProps = { currentSurvey: Survey - apiURL: string } export function SurveyResponsesPage({ currentSurvey, - apiURL, }: SurveyResponsesPageProps) { currentSurvey = currentSurvey as Survey @@ -52,8 +50,7 @@ export function SurveyResponsesPage({ const downloadFile = async (path: string) => { await download( currentSurvey.uuid, - path.substring(path.lastIndexOf('/') + 1), - apiURL + path.substring(path.lastIndexOf('/') + 1) ) } @@ -68,8 +65,7 @@ export function SurveyResponsesPage({ const offset = (page - 1) * limit const surveySessionsResp = await getSurveySessions( currentSurvey.uuid, - `limit=${limit}&offset=${offset}&sort_by=${sortBy}&order=${order}`, - apiURL + `limit=${limit}&offset=${offset}&sort_by=${sortBy}&order=${order}` ) if (surveySessionsResp.error) { @@ -84,8 +80,7 @@ export function SurveyResponsesPage({ const deleteSessionResp = await deleteSurveySession( currentSurvey.uuid, - session.uuid, - apiURL + session.uuid ) if (deleteSessionResp.error) { @@ -119,8 +114,7 @@ export function SurveyResponsesPage({ const allSessionsResp = await getSurveySessions( currentSurvey.uuid, - `limit=1000000&offset=0&sort_by=created_at&order=desc`, - apiURL + `limit=1000000&offset=0&sort_by=created_at&order=desc` ) setDownloading(false) diff --git a/ui/src/components/app/SurveyRow.tsx b/ui/src/components/app/SurveyRow.tsx index c978ed64..42a62f7d 100644 --- a/ui/src/components/app/SurveyRow.tsx +++ b/ui/src/components/app/SurveyRow.tsx @@ -1,3 +1,5 @@ +'use client' + import { ErrCode } from 'components/ui/ErrCode' import { Alert, Badge, Button, Table } from 'flowbite-react' import { updateSurvey } from 'lib/api' @@ -14,21 +16,16 @@ import { type SurveyCardProps = { survey: Survey - apiURL: string } -export function SurveyRow({ survey, apiURL }: SurveyCardProps) { +export function SurveyRow({ survey }: SurveyCardProps) { const [errorMsg, setErrorMsg] = useState('') const [showErrorLog, setShowErrorLog] = useState(false) async function updateSurveyStatus(surveyUUID: string, status: string) { - const res = await updateSurvey( - surveyUUID, - { - delivery_status: status, - }, - apiURL - ) + const res = await updateSurvey(surveyUUID, { + delivery_status: status, + }) if (res.error) { setErrorMsg(res.error) @@ -91,7 +88,7 @@ export function SurveyRow({ survey, apiURL }: SurveyCardProps) { {(isLaunched || canSartSurvey) && ( )} diff --git a/ui/src/components/app/SurveysPage.tsx b/ui/src/components/app/SurveysPage.tsx index 5e47c537..e9cb3cf7 100644 --- a/ui/src/components/app/SurveysPage.tsx +++ b/ui/src/components/app/SurveysPage.tsx @@ -6,10 +6,9 @@ import { SurveyRow } from './SurveyRow' type SurveysPageProps = { surveys: Array - apiURL: string } -export function SurveysPage({ surveys, apiURL }: SurveysPageProps) { +export function SurveysPage({ surveys }: SurveysPageProps) { return (
@@ -26,13 +25,7 @@ export function SurveysPage({ surveys, apiURL }: SurveysPageProps) { {surveys.map((survey) => { - return ( - - ) + return })} diff --git a/ui/src/components/app/survey/SurveyForm.tsx b/ui/src/components/app/survey/SurveyForm.tsx index 31fe8256..16c3e04c 100644 --- a/ui/src/components/app/survey/SurveyForm.tsx +++ b/ui/src/components/app/survey/SurveyForm.tsx @@ -8,10 +8,9 @@ import SurveyQuestions from 'components/app/survey/SurveyQuestions' type SurveyFormProps = { survey: Survey - apiURL: string } -export default function SurveyForm({ survey, apiURL }: SurveyFormProps) { +export default function SurveyForm({ survey }: SurveyFormProps) { const [surveySession, setSurveySession] = useState( undefined ) @@ -30,12 +29,7 @@ export default function SurveyForm({ survey, apiURL }: SurveyFormProps) { return } - const sessionRes = await getSurveySession( - window.location.hostname, - survey.url_slug, - lsValue, - apiURL - ) + const sessionRes = await getSurveySession(survey.url_slug, lsValue) if (sessionRes.error || !sessionRes.data.data) { localStorage.removeItem(`survey_session_id:${survey.url_slug}`) setIsNewSession(true) @@ -48,7 +42,7 @@ export default function SurveyForm({ survey, apiURL }: SurveyFormProps) { setIsLoading(false) } })() - }, [survey, apiURL]) + }, [survey]) if (isLoading) { return ( @@ -76,14 +70,10 @@ export default function SurveyForm({ survey, apiURL }: SurveyFormProps) { } if (isNewSession) { - return + return } return ( - + ) } diff --git a/ui/src/components/app/survey/SurveyIntro.tsx b/ui/src/components/app/survey/SurveyIntro.tsx index 706ac3a7..7adfc562 100644 --- a/ui/src/components/app/survey/SurveyIntro.tsx +++ b/ui/src/components/app/survey/SurveyIntro.tsx @@ -9,10 +9,9 @@ import { createSurveySession } from 'lib/api' type SurveyIntroProps = { survey: Survey - apiURL: string } -export default function SurveyIntro({ survey, apiURL }: SurveyIntroProps) { +export default function SurveyIntro({ survey }: SurveyIntroProps) { const [errMessage, seterrMessage] = useState(undefined) const [surveySession, setSurveySession] = useState( undefined @@ -24,7 +23,6 @@ export default function SurveyIntro({ survey, apiURL }: SurveyIntroProps) { ) } @@ -42,11 +40,7 @@ export default function SurveyIntro({ survey, apiURL }: SurveyIntroProps) {