diff --git a/.dockerignore b/.dockerignore
deleted file mode 100644
index 7de2e19..0000000
--- a/.dockerignore
+++ /dev/null
@@ -1,32 +0,0 @@
-# flyctl launch added from .gitignore
-**/pkg/github/tmp
-**/pkg/parser/tmp
-**/postgres-data
-**/.DS_Store
-
-# flyctl launch added from ui/.gitignore
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-ui/node_modules
-ui/.pnp
-ui/**/.pnp.js
-
-# testing
-ui/coverage
-
-# next.js
-ui/.next
-ui/out
-
-# production
-ui/build
-
-# misc
-ui/**/.DS_Store
-
-# debug
-ui/**/npm-debug.log*
-ui/**/yarn-debug.log*
-ui/**/yarn-error.log*
-fly.toml
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 8d54635..a625e36 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -8,7 +8,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v5
with:
- go-version: 1.22
+ go-version: 1.23
- name: Check out code into the Go module directory
uses: actions/checkout@v4
diff --git a/.gitignore b/.gitignore
index 4e06414..8f734db 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@ ui/npm-debug.log*
ui/yarn-debug.log*
ui/yarn-error.log*
.DS_Store
+.env
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 9274396..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,75 +0,0 @@
-# Build API
-FROM golang:1.22-alpine AS api_builder
-
-RUN apk add build-base
-
-WORKDIR /api
-
-COPY --from=api go.mod .
-COPY --from=api go.sum .
-
-RUN go mod download
-
-COPY --from=api ./cmd ./cmd
-COPY --from=api ./pkg ./pkg
-COPY --from=api ./migrations ./migrations
-COPY --from=api ./surveys ./surveys-examples
-RUN CGO_ENABLED=1 GOOS=linux go build -o api -tags enablecgo cmd/console-api/api.go
-
-
-# Build UI
-FROM node:20-alpine AS ui_base
-
-FROM ui_base AS deps
-RUN apk add --no-cache libc6-compat
-WORKDIR /app
-
-COPY --from=ui package.json package-lock.json ./
-RUN npm ci
-
-FROM ui_base AS ui_builder
-WORKDIR /app
-COPY --from=deps /app/node_modules ./node_modules
-COPY --from=ui . .
-
-ENV NODE_ENV=production
-
-RUN npm run build
-
-
-# Final image
-FROM alpine:latest AS runner
-
-RUN apk --no-cache add ca-certificates tzdata nodejs
-
-WORKDIR /app
-ENV NODE_ENV=production
-
-COPY --from=ui_builder /app/public ./public
-
-RUN mkdir .next
-RUN chown 1000:1000 .next
-
-COPY --from=ui_builder --chown=1000:1000 /app/.next/standalone ./
-COPY --from=ui_builder --chown=1000:1000 /app/.next/static ./.next/static
-
-WORKDIR /api
-
-COPY --from=api_builder --chown=1000:1000 /api/api ./api
-COPY --from=api_builder --chown=1000:1000 /api/migrations ./migrations
-COPY --from=api_builder --chown=1000:1000 /api/surveys-examples ./surveys-examples
-
-RUN mkdir /data
-RUN chown 1000:1000 /data
-
-COPY start.sh /start.sh
-RUN chmod +x /start.sh
-
-USER 1000:1000
-RUN mkdir /data/surveys
-RUN mkdir /data/db
-
-EXPOSE 3000
-EXPOSE 8080
-
-CMD ["sh", "/start.sh"]
diff --git a/README.md b/README.md
index 46e45d3..4521bc6 100644
--- a/README.md
+++ b/README.md
@@ -28,12 +28,13 @@ This approach offers a number of advantages, including:
- [x] Default theme
- [x] Custom themes support
- [x] Personalized options: intro, outro, etc.
-- [x] Cookie/IP deduplication
+- [x] Cookie/IP duplicate response protection
- [x] Admin user authentication
- [x] Different database options: SQLite and Postgres
- [x] Continue where you left off
- [x] Advanced validation rules
- [x] Detect survey changes in real time
+- [x] Export responses in UI or via API
- [ ] Advanced question types
- [ ] Pipe answers into the following questions
@@ -249,6 +250,17 @@ Presents a question where users can only answer "yes" or "no".
label: Is Berlin the capital of Germany?
```
+## Responses
+
+Responses can be shown in the UI and exported as a JSON. Alternatively you can use REST API to get survey resposnes:
+
+```bash
+curl -XGET \
+http://localhost:9900/app/surveys/{SURVEY_ID}/sessions?limit=100&offset=0&sort_by=created_at&order=desc
+```
+
+Where `{SURVEY_ID}` id the UUID of a given survey.
+
## Screenshots
@@ -256,7 +268,7 @@ Presents a question where users can only answer "yes" or "no".
-## Installation: Docker
+## Installation & Deployment
```
docker-compose up -d --build
@@ -264,22 +276,14 @@ docker-compose up -d --build
And you should be able to access the UI on http://localhost:3000 (default basic auth: `user:pass`).
-## Deployment
+You can deploy individual services to any cloud provider or self host them.
-You can deploy individual services to any cloud provider:
-
-- Go backend. It's packaged as a Docker container and can be deployed to any cloud provider.
-- Next.js frontend. It's also packaged as a Docker container, but also can be deployed to Vercel or Netlify.
+- Go backend. It's packaged as a Docker container and can be deployed anywhere.
+- Next.js frontend. It's also packaged as a Docker container, but also can be deployed to Vercel for example.
- [Optional] Postgres database. You can use managed Postgres services or deploy it yourself.
The demo service (links above) is deployed to Fly.io (Go, SQLite) and Vercel (Next.js) and are under the free tiers.
-There is also a combined version of Go and Next.js in the same Docker container. You can run it with:
-
-```
-docker-compose -f compose-combined.yml up -d --build
-```
-
### Environment Variables
API:
@@ -291,14 +295,14 @@ API:
UI:
-- `CONSOLE_API_ADDR_INTERNAL` - Internal address of the Go backend, e.g. `http://api:8080` (could be the same as `CONSOLE_API_ADDR` if UI amd API are not on the same network).
-- `CONSOLE_API_ADDR` - Public address of the Go backend.
+- `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)
## Tech Stack
-- Backend: Go, Postgres, SQLite
+- Backend: Go, (Postgres or SQLite)
- UI: Next.js, Tailwind CSS
## Create new SQLite/Postgres migration
diff --git a/api/Dockerfile b/api/Dockerfile
index 7ed06c5..f32ca4b 100644
--- a/api/Dockerfile
+++ b/api/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.22-alpine AS builder
+FROM golang:1.23-alpine AS builder
RUN apk add build-base
diff --git a/api/go.mod b/api/go.mod
index f0b951b..cb61550 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -1,6 +1,6 @@
module github.com/plutov/formulosity/api
-go 1.22
+go 1.23
require (
github.com/fsnotify/fsnotify v1.7.0
diff --git a/compose-combined.yml b/compose-combined.yml
deleted file mode 100644
index dd0fe56..0000000
--- a/compose-combined.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-services:
- formulosity:
- restart: unless-stopped
- image: formulosity:latest
- build:
- context: .
- additional_contexts:
- api: ./api
- ui: ./ui
- ports:
- - "3000:3000"
- - "8080:8080"
- init: true
- environment:
- - LOG_LEVEL=debug
- - DATABASE_TYPE=sqlite # postgres|sqlite
- - DATABASE_URL=/data/sqlite3/formulosity.db
- - SURVEYS_DIR=/data/surveys
- - CONSOLE_API_ADDR_INTERNAL=http://127.0.0.1:8080
- - CONSOLE_API_ADDR=http://127.0.0.1:8080
- - IRON_SESSION_SECRET=e75af92dffba8065f2730472f45f2046941fe35f361739d31992f42d88d6bf6c
- - HTTP_BASIC_AUTH=user:pass
- volumes:
- - ./api/surveys:/data/surveys
- - ./api/sqlite3:/data/sqlite3
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 87ec88e..828559e 100644
--- a/ui/src/app/app/surveys/[survey_uuid]/responses/page.tsx
+++ b/ui/src/app/app/surveys/[survey_uuid]/responses/page.tsx
@@ -33,7 +33,8 @@ export default async function ResponsesPage({
const surveySessionsResp = await getSurveySessions(
currentSurvey.uuid,
- `limit=${SurveySessionsLimit}&offset=0&sort_by=created_at&order=desc`
+ `limit=${SurveySessionsLimit}&offset=0&sort_by=created_at&order=desc`,
+ ''
)
if (surveySessionsResp.error) {
errMsg = 'Unable to fetch survey sessions'
@@ -53,9 +54,11 @@ 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 1723e74..448e819 100644
--- a/ui/src/app/layout.tsx
+++ b/ui/src/app/layout.tsx
@@ -2,7 +2,6 @@ import 'styles/global.css'
import { ReactNode } from 'react'
import { Metadata } from 'next'
import { siteConfig } from 'lib/siteConfig'
-import { dm_sans, inter } from 'lib/fonts'
export const metadata: Metadata = {
title: {
@@ -31,13 +30,9 @@ type LayoutProps = { children?: ReactNode }
export default async function RootLayout({ children }: LayoutProps) {
return (
-
+
{children}
+ {children}
)
}
diff --git a/ui/src/components/app/AppLayout.tsx b/ui/src/components/app/AppLayout.tsx
index 564aaf5..42fb75e 100644
--- a/ui/src/components/app/AppLayout.tsx
+++ b/ui/src/components/app/AppLayout.tsx
@@ -1,11 +1,10 @@
'use client'
import { ReactNode, useState } from 'react'
-import { usePathname } from 'next/navigation'
import { Badge, Navbar, Sidebar } from 'flowbite-react'
import { LogoIcon } from 'components/ui/LogoIcon'
import { siteConfig } from 'lib/siteConfig'
-import { HiViewGrid, HiMenu, HiOutlineBookOpen } from 'react-icons/hi'
+import { HiMenu, HiOutlineBookOpen } from 'react-icons/hi'
import 'styles/app.css'
type AppLayoutProps = {
@@ -14,25 +13,16 @@ type AppLayoutProps = {
export default function AppLayout({ children }: AppLayoutProps) {
const [isSidebarOpen, setIsSidebarOpen] = useState(false)
- const pathname = usePathname()
-
- const navigation = [
- {
- name: 'Surveys',
- href: `/app`,
- icon: HiViewGrid,
- },
- ]
return (
-
- {navigation.map((item) => (
-
- {item.name}
-
- ))}
-
@@ -80,7 +55,7 @@ export default function AppLayout({ children }: AppLayoutProps) {
Beta
-
+
{siteConfig.name} is currently in beta.
diff --git a/ui/src/components/app/SurveyResponsesPage.tsx b/ui/src/components/app/SurveyResponsesPage.tsx
index 67edb16..cef9569 100644
--- a/ui/src/components/app/SurveyResponsesPage.tsx
+++ b/ui/src/components/app/SurveyResponsesPage.tsx
@@ -23,10 +23,12 @@ import moment from 'moment'
type SurveyResponsesPageProps = {
currentSurvey: Survey
+ apiURL: string
}
export function SurveyResponsesPage({
currentSurvey,
+ apiURL,
}: SurveyResponsesPageProps) {
currentSurvey = currentSurvey as Survey
@@ -57,7 +59,8 @@ export function SurveyResponsesPage({
const offset = (page - 1) * limit
const surveySessionsResp = await getSurveySessions(
currentSurvey.uuid,
- `limit=${limit}&offset=${offset}&sort_by=${sortBy}&order=${order}`
+ `limit=${limit}&offset=${offset}&sort_by=${sortBy}&order=${order}`,
+ apiURL
)
if (surveySessionsResp.error) {
@@ -85,13 +88,14 @@ export function SurveyResponsesPage({