├── .dockerignore ├── .env ├── .gitignore ├── Caddyfile ├── Caddyfile.demohosting ├── Dockerfile ├── Experiment ├── .env ├── compose.metrics.yml ├── compose.yml ├── compose.zitadel.yml └── vector.toml ├── LICENSE ├── README.md ├── build.Dockerfile ├── build.dockerignore ├── compose.demohosting.yml ├── compose.multiservice.yml ├── compose.replicas.yml ├── compose.reverseproxy.yml ├── compose.yml ├── docs ├── docs │ ├── .vitepress │ │ ├── config.mts │ │ └── theme │ │ │ ├── custom.css │ │ │ └── index.ts │ ├── guide │ │ ├── configurations.md │ │ ├── create-board.md │ │ ├── dashboard.md │ │ ├── development.md │ │ ├── getting-started.md │ │ └── self-hosting.md │ ├── index.md │ └── public │ │ ├── apple-touch-icon.png │ │ ├── createboard.png │ │ ├── createboard_turnstile.png │ │ ├── dashboard_add_cards.png │ │ ├── dashboard_focus_panel.png │ │ ├── dashboard_guest.png │ │ ├── dashboard_move.png │ │ ├── dashboard_owner.png │ │ ├── favicon.ico │ │ ├── logo.png │ │ ├── logo_large_dark.png │ │ ├── logo_large_light.png │ │ ├── robots.txt │ │ └── videos │ │ ├── add-update-message.mp4 │ │ ├── create-board.mp4 │ │ └── start-stop-timer.mp4 ├── package-lock.json └── package.json ├── homepage ├── 404.html ├── apple-touch-icon.png ├── assets │ ├── app.rh1oyAvj.js │ ├── chunks │ │ ├── framework.CTVYQtO4.js │ │ └── theme.Z7SuLSAi.js │ ├── guide_configurations.md.CT2ZnaGS.js │ ├── guide_configurations.md.CT2ZnaGS.lean.js │ ├── guide_create-board.md.Dg_d45NA.js │ ├── guide_create-board.md.Dg_d45NA.lean.js │ ├── guide_dashboard.md.DNu-gDWb.js │ ├── guide_dashboard.md.DNu-gDWb.lean.js │ ├── guide_dashboard.md.giEGfGkw.js │ ├── guide_dashboard.md.giEGfGkw.lean.js │ ├── guide_dashboard.md.mJgkaj93.js │ ├── guide_dashboard.md.mJgkaj93.lean.js │ ├── guide_development.md.BY4ATYwc.js │ ├── guide_development.md.BY4ATYwc.lean.js │ ├── guide_development.md.Ds4A4Cqt.js │ ├── guide_development.md.Ds4A4Cqt.lean.js │ ├── guide_getting-started.md.CP0vWESL.js │ ├── guide_getting-started.md.CP0vWESL.lean.js │ ├── guide_getting-started.md.Clt8uJRh.js │ ├── guide_getting-started.md.Clt8uJRh.lean.js │ ├── guide_self-hosting.md.D4ovObpY.js │ ├── guide_self-hosting.md.D4ovObpY.lean.js │ ├── index.md.6AvOtPGe.js │ ├── index.md.6AvOtPGe.lean.js │ ├── index.md.CU58q19j.js │ ├── index.md.CU58q19j.lean.js │ ├── index.md.DJqEP-JG.js │ ├── index.md.DJqEP-JG.lean.js │ ├── inter-italic-cyrillic-ext.r48I6akx.woff2 │ ├── inter-italic-cyrillic.By2_1cv3.woff2 │ ├── inter-italic-greek-ext.1u6EdAuj.woff2 │ ├── inter-italic-greek.DJ8dCoTZ.woff2 │ ├── inter-italic-latin-ext.CN1xVJS-.woff2 │ ├── inter-italic-latin.C2AdPX0b.woff2 │ ├── inter-italic-vietnamese.BSbpV94h.woff2 │ ├── inter-roman-cyrillic-ext.BBPuwvHQ.woff2 │ ├── inter-roman-cyrillic.C5lxZ8CY.woff2 │ ├── inter-roman-greek-ext.CqjqNYQ-.woff2 │ ├── inter-roman-greek.BBVDIX6e.woff2 │ ├── inter-roman-latin-ext.4ZJIpNVo.woff2 │ ├── inter-roman-latin.Di8DUHzh.woff2 │ ├── inter-roman-vietnamese.BjW4sHH5.woff2 │ └── style.DLuJsz9X.css ├── createboard.png ├── createboard_turnstile.png ├── dashboard_add_cards.png ├── dashboard_focus_panel.png ├── dashboard_guest.png ├── dashboard_move.png ├── dashboard_owner.png ├── favicon.ico ├── guide │ ├── configurations.html │ ├── create-board.html │ ├── dashboard.html │ ├── development.html │ ├── getting-started.html │ └── self-hosting.html ├── hashmap.json ├── index.html ├── logo.png ├── logo_large_dark.png ├── logo_large_light.png ├── robots.txt ├── sitemap.xml ├── videos │ ├── add-update-message.mp4 │ ├── create-board.mp4 │ └── start-stop-timer.mp4 └── vp-icons.css ├── homepage_deprecated ├── images │ ├── addmessage.png │ ├── createboard.png │ ├── dashboard.png │ ├── owneractions.png │ └── share.png ├── index.html └── output.css ├── onlybackend.Dockerfile ├── onlybackend.dockerignore ├── redis ├── redis.conf └── users.acl └── src ├── board.go ├── client.go ├── config.toml ├── event.go ├── eventresponses.go ├── eventtypes.go ├── frontend ├── .env ├── .env.development ├── .eslintrc.js ├── .gitignore ├── .vscode │ └── extensions.json ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── src │ ├── App.vue │ ├── api │ │ └── index.ts │ ├── assets │ │ └── vue.svg │ ├── components │ │ ├── Avatar.vue │ │ ├── Card.vue │ │ ├── Category.vue │ │ ├── CountdownTimer.vue │ │ ├── CreateBoard.vue │ │ ├── DarkModeToggle.vue │ │ ├── Dashboard.vue │ │ ├── HelloWorld.vue │ │ ├── Join.vue │ │ ├── LanguageSelector.vue │ │ ├── NewAnonymousCard.vue │ │ ├── NewCard.vue │ │ ├── TimerPanel.vue │ │ └── TurnstileWidget.vue │ ├── composables │ │ ├── useLanguage.ts │ │ └── useSanitize.ts │ ├── i18n │ │ ├── de.ts │ │ ├── en.ts │ │ ├── es.ts │ │ ├── fr-CA.ts │ │ ├── fr.ts │ │ ├── index.ts │ │ ├── it.ts │ │ ├── ja.ts │ │ ├── ko.ts │ │ ├── nl.ts │ │ ├── pt-BR.ts │ │ ├── pt.ts │ │ ├── ru.ts │ │ ├── uk.ts │ │ └── zh-CN.ts │ ├── index.css │ ├── main.ts │ ├── models │ │ ├── BoardColumn.ts │ │ ├── CategoryChangeMessage.ts │ │ ├── DraftMessage.ts │ │ ├── LikeMessage.ts │ │ ├── OnlineUser.ts │ │ └── Requests.ts │ ├── router.ts │ ├── style.css │ ├── types │ │ └── turnstile.d.ts │ ├── utils │ │ └── index.ts │ └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── go.mod ├── go.sum ├── helpers.go ├── helpers_test.go ├── hub.go ├── input.css ├── main.go ├── message.go ├── redis.go └── user.go /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REDIS_CONNSTR=redis://redis:6379/0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | node_modules 24 | 25 | # VitePress output directory 26 | docs/docs/.vitepress/cache/ 27 | docs/docs/.vitepress/dist/ -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | docs.localhost { 2 | # Permanent redirects for changed URLs 3 | redir /configurations /guide/configurations 301 4 | redir /create-board /guide/create-board 301 5 | redir /dashboard /guide/dashboard 301 6 | redir /development /guide/development 301 7 | redir /getting-started /guide/getting-started 301 8 | redir /self-hosting /guide/self-hosting 301 9 | # Handle trailing slash for guide 10 | redir /guide/ /guide/getting-started 301 11 | redir /guide /guide/getting-started 301 12 | 13 | encode gzip 14 | root * /var/www/homepage 15 | try_files {path}.html {path}/ {path} /404.html 16 | file_server 17 | } 18 | 19 | localhost { 20 | encode gzip 21 | reverse_proxy app:8080 22 | } 23 | 24 | # Uncomment below block and comment above block when used with comnpose.multiservice.yml 25 | # localhost { 26 | # encode gzip 27 | # reverse_proxy { 28 | # to app:8080 app01:8080 29 | 30 | # lb_policy round_robin 31 | # lb_retries 2 32 | # } 33 | # } -------------------------------------------------------------------------------- /Caddyfile.demohosting: -------------------------------------------------------------------------------- 1 | { 2 | email vijeesh82@gmail.com 3 | } 4 | 5 | quickretro.app { 6 | # Permanent redirects for changed URLs 7 | redir /configurations /guide/configurations 301 8 | redir /create-board /guide/create-board 301 9 | redir /dashboard /guide/dashboard 301 10 | redir /development /guide/development 301 11 | redir /getting-started /guide/getting-started 301 12 | redir /self-hosting /guide/self-hosting 301 13 | # Handle trailing slash for guide 14 | redir /guide/ /guide/getting-started 301 15 | redir /guide /guide/getting-started 301 16 | 17 | encode gzip 18 | root * /var/www/homepage 19 | # try_files {path} /404.html 20 | try_files {path}.html {path}/ {path} /404.html 21 | file_server 22 | } 23 | 24 | # quickretro.app { 25 | # encode gzip 26 | # root * /var/www/homepage 27 | # file_server 28 | # route /images/* { 29 | # uri strip_prefix /images 30 | # root * /var/www/homepage/images 31 | # file_server 32 | # } 33 | # } 34 | 35 | demo.quickretro.app { 36 | encode gzip 37 | reverse_proxy app:8080 38 | } 39 | 40 | # secretmsg.us { 41 | # encode gzip 42 | # reverse_proxy secretnoteapp:8085 43 | # } 44 | 45 | # quickretro.app { 46 | # encode gzip 47 | # reverse_proxy app:8080 48 | # } 49 | 50 | # localhost { 51 | # encode gzip 52 | # reverse_proxy app:8080 53 | # } 54 | 55 | # Uncomment below block and comment above block when used with comnpose.multiservice.yml 56 | # localhost { 57 | # encode gzip 58 | # reverse_proxy { 59 | # to app:8080 app01:8080 60 | 61 | # lb_policy round_robin 62 | # lb_retries 2 63 | # } 64 | # } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22.14.0-alpine AS frontend-builder 2 | WORKDIR /app 3 | # node_modules directory is excluded with .dockerignore 4 | COPY src/frontend/ . 5 | RUN npm install 6 | RUN npm run build-dev 7 | 8 | FROM golang:1.24.2-alpine AS backend-builder 9 | WORKDIR /app 10 | COPY src/go.mod src/go.sum ./ 11 | RUN go mod download 12 | # COPY src/ . 13 | COPY src/*.go . 14 | # COPY src/frontend/dist frontend/dist 15 | COPY src/config.toml . 16 | COPY --from=frontend-builder /app/dist frontend/dist 17 | RUN CGO_ENABLED=0 GOOS=linux go build -o retroapp . 18 | 19 | FROM alpine:latest AS certificates 20 | RUN apk --no-cache add ca-certificates 21 | 22 | FROM scratch 23 | WORKDIR /app 24 | COPY --from=certificates /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 25 | COPY --from=backend-builder /app/retroapp . 26 | COPY --from=backend-builder /app/config.toml . 27 | # COPY src/public ./public 28 | EXPOSE 8080 29 | CMD ["./retroapp"] -------------------------------------------------------------------------------- /Experiment/.env: -------------------------------------------------------------------------------- 1 | # Password for the 'elastic' user (at least 6 characters) 2 | ELASTIC_PASSWORD=changeme 3 | 4 | # Password for the 'kibana_system' user (at least 6 characters) 5 | KIBANA_PASSWORD=changeme 6 | 7 | # Version of Elastic products 8 | STACK_VERSION=8.12.2 9 | 10 | # Set the cluster name 11 | CLUSTER_NAME=docker-cluster 12 | 13 | # Set to 'basic' or 'trial' to automatically start the 30-day trial 14 | LICENSE=basic 15 | #LICENSE=trial 16 | 17 | # Port to expose Elasticsearch HTTP API to the host 18 | ES_PORT=9200 19 | #ES_PORT=127.0.0.1:9200 20 | 21 | # Port to expose Kibana to the host 22 | KIBANA_PORT=5601 23 | #KIBANA_PORT=80 24 | 25 | # Increase or decrease based on the available host memory (in bytes) 26 | MEM_LIMIT=1073741824 27 | 28 | # SAMPLE Predefined Key only to be used in POC environments 29 | ENCRYPTION_KEY=c34d38b3a14956121ff2170e5030b471551370178f43e5626eec58b04a30fae2 30 | 31 | # Project namespace (defaults to the current folder name if not set) 32 | #COMPOSE_PROJECT_NAME=myproject -------------------------------------------------------------------------------- /Experiment/compose.metrics.yml: -------------------------------------------------------------------------------- 1 | # Run docker compose. 2 | # docker compose -f compose.metrics.yml up 3 | # To stop and remove compose created items 4 | # docker compose -f compose.metrics.yml down --rmi "all" --volumes 5 | 6 | services: 7 | elasticsearch: 8 | image: elasticsearch:8.12.2 9 | environment: 10 | - discovery.type=single-node 11 | volumes: 12 | - esdata:/usr/share/elasticsearch/data 13 | ports: 14 | - "9200:9200" 15 | 16 | vector: 17 | image: timberio/vector:0.36.0-alpine 18 | volumes: 19 | - ./vector.toml:/etc/vector/vector.toml 20 | ports: 21 | - "9000:9000" 22 | depends_on: 23 | - elasticsearch 24 | command: -c /etc/vector/vector.toml 25 | # command: ["--config", "/etc/vector/vector.toml"] 26 | 27 | kibana: 28 | image: kibana:8.12.2 29 | ports: 30 | - "5601:5601" 31 | depends_on: 32 | - elasticsearch 33 | 34 | networks: 35 | metricnet: 36 | name: metricnet 37 | 38 | volumes: 39 | esdata: 40 | driver: local -------------------------------------------------------------------------------- /Experiment/compose.zitadel.yml: -------------------------------------------------------------------------------- 1 | # Run docker compose. 2 | # docker compose -f .\compose.zitadel.yml up 3 | # To stop and remove compose created items 4 | # docker compose -f .\compose.zitadel.yml down --rmi "all" --volumes 5 | 6 | services: 7 | zitadel: 8 | restart: 'always' 9 | networks: 10 | - 'zitadel' 11 | image: 'ghcr.io/zitadel/zitadel:latest' 12 | command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled' 13 | environment: 14 | - 'ZITADEL_DATABASE_POSTGRES_HOST=db' 15 | - 'ZITADEL_DATABASE_POSTGRES_PORT=5432' 16 | - 'ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel' 17 | - 'ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel' 18 | - 'ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=zitadel' 19 | - 'ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable' 20 | - 'ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=postgres' 21 | - 'ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD=postgres' 22 | - 'ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable' 23 | - 'ZITADEL_EXTERNALSECURE=false' 24 | depends_on: 25 | db: 26 | condition: 'service_healthy' 27 | ports: 28 | - '8080:8080' 29 | 30 | db: 31 | restart: 'always' 32 | image: postgres:16-alpine 33 | environment: 34 | - POSTGRES_USER=postgres 35 | - POSTGRES_PASSWORD=postgres 36 | networks: 37 | - 'zitadel' 38 | healthcheck: 39 | test: ["CMD-SHELL", "pg_isready", "-d", "db_prod"] 40 | interval: '10s' 41 | timeout: '30s' 42 | retries: 5 43 | start_period: '20s' 44 | ports: 45 | - '5432:5432' 46 | 47 | networks: 48 | zitadel: -------------------------------------------------------------------------------- /Experiment/vector.toml: -------------------------------------------------------------------------------- 1 | [sources.app_logs] 2 | type = "docker_logs" 3 | # include_labels = ["com.docker.compose.service"] 4 | include_containers = [ "app", "redis" ] 5 | 6 | # [sources.docker.containers] 7 | # include = ["*"] 8 | 9 | # [sources.docker.containers.include] 10 | # name="*" 11 | 12 | [sinks.elasticsearch] 13 | type = "elasticsearch" 14 | api_version = "v8" 15 | endpoints = [ "https://localhost:9200" ] 16 | inputs = [ "app_logs" ] 17 | # mode = "bulk" 18 | # indices.enabled = true 19 | # indices.index = "logs-%Y.%m.%d" 20 | 21 | [sinks.elasticsearch.buffer] 22 | type = "memory" 23 | max_events = 500 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Vijeesh Ravindran 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # quickretro 2 | A websocket based app for conducting a quick sprint retro. 3 | 4 | ## Live app demo 5 | https://demo.quickretro.app 6 | 7 | ## Site 8 | https://quickretro.app 9 | 10 | ### Docs 11 | https://quickretro.app/guide/getting-started 12 | 13 | ## Running the app locally 14 | Ensure Go, Nodejs and Docker are installed. 15 | 16 | ### Build the Vue frontend 17 | ```sh 18 | cd .\src\frontend\ 19 | ``` 20 | Install packages and dependencies. 21 | ```sh 22 | npm install 23 | ``` 24 | Build the frontend. 25 | This creates assets in "frontend/dist" directory. This dist directory is embedded in the backend Golang binary. 26 | ```sh 27 | npm run build-dev 28 | ``` 29 | 30 | ### To start the Golang backend server 31 | Navigate back to root directory. 32 | ```sh 33 | docker compose up 34 | ``` 35 | Visit http://localhost:8080 to open the Vue app and start creating a board. 36 | 37 | ## For Frontend Development 38 | ### Running Vue app in development mode 39 | Run the app. 40 | ```sh 41 | npm run dev 42 | ``` 43 | Visit http://localhost:5173/ to open. 44 | 45 | ## Features 46 | - No Signups or Logins - That's right! No need to signup or login. 47 | - No Board Limits - Create Boards or Invite Users without limits. 48 | - Mobile Friendly UI - Easily participate from your mobile phone. 49 | - Customize Column Names - Choose upto 5 columns with any name. 50 | - Mask/Blur messages - Avoid revealing messages of other participants. 51 | - Anonymous Messages - Post messages without revealing your name. 52 | - Download as PDF - Download messages as PDF. 53 | - Countdown Timer - Stopwatch with max 1 hour limit. 54 | - Board Lock - Lock to stop addition/updation of messages. 55 | - Dark Theme - Easily switch to use a Dark theme. 56 | - Online Presence Display - See participants present in the meeting in realtime. 57 | - Auto-Delete data - Auto-delete with configurable retention duration. 58 | 59 | ![dashboard_owner](https://github.com/user-attachments/assets/9f35a7fc-7c91-4b39-b4ef-b338a181cec8) 60 | 61 | ![dashboard_guest](https://github.com/user-attachments/assets/551886c9-d8e2-44ca-8eaa-28e2a8a16ce5) 62 | -------------------------------------------------------------------------------- /build.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22.14.0-alpine AS frontend-builder 2 | WORKDIR /app 3 | # node_modules directory is excluded with .dockerignore 4 | COPY src/frontend/ . 5 | RUN npm install 6 | RUN npm run build 7 | 8 | FROM golang:1.24.2-alpine AS backend-builder 9 | WORKDIR /app 10 | COPY src/go.mod src/go.sum ./ 11 | RUN go mod download 12 | # COPY src/ . 13 | COPY src/*.go . 14 | # COPY src/frontend/dist frontend/dist 15 | COPY src/config.toml . 16 | COPY --from=frontend-builder /app/dist frontend/dist 17 | RUN CGO_ENABLED=0 GOOS=linux go build -o retroapp . 18 | 19 | FROM alpine:latest AS certificates 20 | RUN apk --no-cache add ca-certificates 21 | 22 | FROM scratch 23 | WORKDIR /app 24 | COPY --from=certificates /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 25 | COPY --from=backend-builder /app/retroapp . 26 | COPY --from=backend-builder /app/config.toml . 27 | # COPY src/public ./public 28 | EXPOSE 8080 29 | CMD ["./retroapp"] -------------------------------------------------------------------------------- /build.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules -------------------------------------------------------------------------------- /compose.demohosting.yml: -------------------------------------------------------------------------------- 1 | # With reverse-proxy. Access only with https://localhost. 2 | 3 | # Run docker compose. 4 | # docker compose -f compose.demohosting.yml up 5 | # To stop and remove compose created items 6 | # docker compose -f compose.demohosting.yml down --rmi "all" --volumes 7 | 8 | services: 9 | redis: 10 | image: "redis:8.0.1-alpine" 11 | ############## Redis ACL ############## 12 | # volumes: 13 | # - ./redis/users.acl:/usr/local/etc/redis/users.acl 14 | # command: redis-server --aclfile /usr/local/etc/redis/users.acl 15 | # # command: ["redis-server", "--aclfile", "/usr/local/etc/redis/users.acl"] 16 | ############## Redis ACL ############## 17 | restart: always 18 | networks: 19 | - redisnet 20 | # ports: 21 | # - "6379:6379" 22 | expose: 23 | - 6379 24 | 25 | app: 26 | image: "vijeesh82/quickretro-app" 27 | restart: unless-stopped 28 | depends_on: 29 | - redis 30 | environment: 31 | # Load from .env file in same directory as the compose file. 32 | # To create file, in CLI: echo "REDIS_CONNSTR=redis://redis:6379/0" > .env 33 | # DO NOT create file from Windows. It creates Unicode text, UTF-16, little-endian text, with CRLF line terminators. 34 | - REDIS_CONNSTR=${REDIS_CONNSTR} 35 | # Default Redis (No Auth or ACL). Outside docker - redis://localhost:6379/0 36 | # - REDIS_CONNSTR=redis://redis:6379/0 37 | # Using Redis ACL with Username & Password. Outside docker - redis://app-user:mysecretpassword@localhost:6379/0 38 | # - REDIS_CONNSTR=redis://app-user:mysecretpassword@redis:6379/0 39 | - TURNSTILE_ENABLED=${TURNSTILE_ENABLED} 40 | - TURNSTILE_SITE_KEY=${TURNSTILE_SITE_KEY} 41 | - TURNSTILE_SECRET_KEY=${TURNSTILE_SECRET_KEY} 42 | networks: 43 | - redisnet 44 | - proxynet 45 | # ports: 46 | # - "8080:8080" 47 | expose: 48 | - 8080 49 | 50 | # secretnoteapp: 51 | # image: "vijeesh82/secretnote-app" 52 | # restart: unless-stopped 53 | # depends_on: 54 | # - redis 55 | # environment: 56 | # - REDIS_CONNSTR=redis://redis:6379/0 # Default Redis (No Auth or ACL). Outside docker - redis://localhost:6379/0 57 | # ############## Redis ACL ############## 58 | # # - REDIS_CONNSTR=redis://app-user:mysecretpassword@redis:6379/0 # Using ACL with Username & Password. Outside docker - redis://app-user:mysecretpassword@localhost:6379/0 59 | # ############## Redis ACL ############## 60 | # networks: 61 | # - redisnet 62 | # - proxynet 63 | # # ports: 64 | # # - "8085:8085" 65 | # expose: 66 | # - 8085 67 | 68 | caddy: 69 | image: caddy:2.10.0-alpine 70 | restart: unless-stopped 71 | ports: 72 | - "80:80" 73 | - "443:443" 74 | - "443:443/udp" 75 | depends_on: 76 | - app 77 | networks: 78 | - proxynet 79 | volumes: 80 | - ./Caddyfile.demohosting:/etc/caddy/Caddyfile 81 | - ./homepage:/var/www/homepage 82 | - ./site:/srv 83 | - caddy_data:/data 84 | - caddy_config:/config 85 | 86 | volumes: 87 | caddy_data: 88 | caddy_config: 89 | 90 | networks: 91 | redisnet: 92 | name: redisnet 93 | proxynet: 94 | name: proxynet 95 | # external: true -------------------------------------------------------------------------------- /compose.multiservice.yml: -------------------------------------------------------------------------------- 1 | # Example with running multiple services of same image, load balanced using Caddy reverse-proxy. 2 | 3 | # Update Caddyfile with instructions given in it. 4 | 5 | # Run following command to build the image before running docker compose. 6 | # docker build -f build.Dockerfile -t quickretro-app . 7 | # Run docker compose. 8 | # docker compose -f compose.multiservice.yml up 9 | # To stop and remove compose created items 10 | # docker compose -f compose.multiservice.yml down --rmi "all" --volumes 11 | 12 | x-app-defaults: &app-defaults 13 | image: quickretro-app 14 | restart: unless-stopped 15 | depends_on: 16 | - redis 17 | environment: 18 | # Load from .env file in same directory as the compose file. 19 | # To create file, in CLI: echo "REDIS_CONNSTR=redis://redis:6379/0" > .env 20 | # DO NOT create file from Windows. It creates Unicode text, UTF-16, little-endian text, with CRLF line terminators. 21 | - REDIS_CONNSTR=${REDIS_CONNSTR} 22 | # Default Redis (No Auth or ACL). Outside docker - redis://localhost:6379/0 23 | # - REDIS_CONNSTR=redis://redis:6379/0 24 | # Using Redis ACL with Username & Password. Outside docker - redis://app-user:mysecretpassword@localhost:6379/0 25 | # - REDIS_CONNSTR=redis://app-user:mysecretpassword@redis:6379/0 26 | - TURNSTILE_ENABLED=${TURNSTILE_ENABLED} 27 | - TURNSTILE_SITE_KEY=${TURNSTILE_SITE_KEY} 28 | - TURNSTILE_SECRET_KEY=${TURNSTILE_SECRET_KEY} 29 | networks: 30 | - redisnet 31 | - proxynet 32 | expose: 33 | - 8080 34 | 35 | services: 36 | redis: 37 | image: "redis:8.0.1-alpine" 38 | ############## Redis ACL ############## 39 | # volumes: 40 | # - ./redis/users.acl:/usr/local/etc/redis/users.acl 41 | # command: redis-server --aclfile /usr/local/etc/redis/users.acl 42 | # # command: ["redis-server", "--aclfile", "/usr/local/etc/redis/users.acl"] 43 | ############## Redis ACL ############## 44 | restart: always 45 | networks: 46 | - redisnet 47 | expose: 48 | - 6379 49 | 50 | app: 51 | <<: *app-defaults 52 | 53 | app01: 54 | <<: *app-defaults 55 | 56 | caddy: 57 | image: caddy:2.10.0-alpine 58 | restart: unless-stopped 59 | ports: 60 | - "80:80" 61 | - "443:443" 62 | - "443:443/udp" 63 | depends_on: 64 | - app 65 | networks: 66 | - proxynet 67 | volumes: 68 | - ./Caddyfile:/etc/caddy/Caddyfile 69 | - ./site:/srv 70 | - caddy_data:/data 71 | - caddy_config:/config 72 | 73 | volumes: 74 | caddy_data: 75 | caddy_config: 76 | 77 | networks: 78 | redisnet: 79 | name: redisnet 80 | proxynet: 81 | name: proxynet -------------------------------------------------------------------------------- /compose.replicas.yml: -------------------------------------------------------------------------------- 1 | # Example with running multiple services of same image with replicas, load balanced by Docker internally. 2 | 3 | # Run docker compose. 4 | # docker compose -f compose.replicas.yml up 5 | # To stop and remove compose created items 6 | # docker compose -f compose.replicas.yml down --rmi "all" --volumes 7 | 8 | services: 9 | redis: 10 | image: "redis:8.0.1-alpine" 11 | ############## Redis ACL ############## 12 | # volumes: 13 | # - ./redis/users.acl:/usr/local/etc/redis/users.acl 14 | # command: redis-server --aclfile /usr/local/etc/redis/users.acl 15 | # # command: ["redis-server", "--aclfile", "/usr/local/etc/redis/users.acl"] 16 | ############## Redis ACL ############## 17 | restart: always 18 | networks: 19 | - redisnet 20 | # ports: 21 | # - "6379:6379" 22 | expose: 23 | - 6379 24 | 25 | app: 26 | build: 27 | context: . 28 | dockerfile: build.Dockerfile 29 | restart: unless-stopped 30 | deploy: 31 | mode: replicated 32 | replicas: 2 33 | restart_policy: 34 | condition: on-failure 35 | max_attempts: 3 36 | depends_on: 37 | - redis 38 | environment: 39 | # Load from .env file in same directory as the compose file. 40 | # To create file, in CLI: echo "REDIS_CONNSTR=redis://redis:6379/0" > .env 41 | # DO NOT create file from Windows. It creates Unicode text, UTF-16, little-endian text, with CRLF line terminators. 42 | - REDIS_CONNSTR=${REDIS_CONNSTR} 43 | # Default Redis (No Auth or ACL). Outside docker - redis://localhost:6379/0 44 | # - REDIS_CONNSTR=redis://redis:6379/0 45 | # Using Redis ACL with Username & Password. Outside docker - redis://app-user:mysecretpassword@localhost:6379/0 46 | # - REDIS_CONNSTR=redis://app-user:mysecretpassword@redis:6379/0 47 | - TURNSTILE_ENABLED=${TURNSTILE_ENABLED} 48 | - TURNSTILE_SITE_KEY=${TURNSTILE_SITE_KEY} 49 | - TURNSTILE_SECRET_KEY=${TURNSTILE_SECRET_KEY} 50 | networks: 51 | - redisnet 52 | - proxynet 53 | expose: 54 | - 8080 55 | 56 | caddy: 57 | image: caddy:2.10.0-alpine 58 | restart: unless-stopped 59 | ports: 60 | - "80:80" 61 | - "443:443" 62 | - "443:443/udp" 63 | depends_on: 64 | - app 65 | networks: 66 | - proxynet 67 | volumes: 68 | - ./Caddyfile:/etc/caddy/Caddyfile 69 | - ./site:/srv 70 | - caddy_data:/data 71 | - caddy_config:/config 72 | 73 | volumes: 74 | caddy_data: 75 | caddy_config: 76 | 77 | networks: 78 | redisnet: 79 | name: redisnet 80 | proxynet: 81 | name: proxynet -------------------------------------------------------------------------------- /compose.reverseproxy.yml: -------------------------------------------------------------------------------- 1 | # With reverse-proxy. Access only with https://localhost. 2 | 3 | # Run docker compose. 4 | # docker compose -f compose.reverseproxy.yml up 5 | # To stop and remove compose created items 6 | # docker compose -f compose.reverseproxy.yml down --rmi "all" --volumes 7 | 8 | services: 9 | redis: 10 | image: "redis:8.0.1-alpine" 11 | ############## Redis ACL ############## 12 | # volumes: 13 | # - ./redis/users.acl:/usr/local/etc/redis/users.acl 14 | # command: redis-server --aclfile /usr/local/etc/redis/users.acl 15 | # # command: ["redis-server", "--aclfile", "/usr/local/etc/redis/users.acl"] 16 | ############## Redis ACL ############## 17 | restart: always 18 | networks: 19 | - redisnet 20 | # ports: 21 | # - "6379:6379" 22 | expose: 23 | - 6379 24 | 25 | app: 26 | build: 27 | context: . 28 | dockerfile: build.Dockerfile 29 | restart: unless-stopped 30 | depends_on: 31 | - redis 32 | environment: 33 | # Load from .env file in same directory as the compose file. 34 | # To create file, in CLI: echo "REDIS_CONNSTR=redis://redis:6379/0" > .env 35 | # DO NOT create file from Windows. It creates Unicode text, UTF-16, little-endian text, with CRLF line terminators. 36 | - REDIS_CONNSTR=${REDIS_CONNSTR} 37 | # Default Redis (No Auth or ACL). Outside docker - redis://localhost:6379/0 38 | # - REDIS_CONNSTR=redis://redis:6379/0 39 | # Using Redis ACL with Username & Password. Outside docker - redis://app-user:mysecretpassword@localhost:6379/0 40 | # - REDIS_CONNSTR=redis://app-user:mysecretpassword@redis:6379/0 41 | - TURNSTILE_ENABLED=${TURNSTILE_ENABLED} 42 | - TURNSTILE_SITE_KEY=${TURNSTILE_SITE_KEY} 43 | - TURNSTILE_SECRET_KEY=${TURNSTILE_SECRET_KEY} 44 | networks: 45 | - redisnet 46 | - proxynet 47 | # ports: 48 | # - "8080:8080" 49 | expose: 50 | - 8080 51 | 52 | caddy: 53 | image: caddy:2.10.0-alpine 54 | restart: unless-stopped 55 | ports: 56 | - "80:80" 57 | - "443:443" 58 | - "443:443/udp" 59 | depends_on: 60 | - app 61 | networks: 62 | - proxynet 63 | volumes: 64 | - ./Caddyfile:/etc/caddy/Caddyfile 65 | - ./homepage:/var/www/homepage 66 | - ./site:/srv 67 | - caddy_data:/data 68 | - caddy_config:/config 69 | 70 | volumes: 71 | caddy_data: 72 | caddy_config: 73 | 74 | networks: 75 | redisnet: 76 | name: redisnet 77 | proxynet: 78 | name: proxynet 79 | # external: true -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | # For local development only. Ports are exposed to host machine. No reverse-proxy. 2 | 3 | # Run docker compose. 4 | # docker compose up 5 | # To stop and remove compose created items 6 | # docker compose down --rmi "all" --volumes 7 | 8 | services: 9 | redis: 10 | image: "redis:8.0.1-alpine" 11 | ############## Redis ACL ############## 12 | # volumes: 13 | # - ./redis/users.acl:/usr/local/etc/redis/users.acl 14 | # command: redis-server --aclfile /usr/local/etc/redis/users.acl 15 | # # command: ["redis-server", "--aclfile", "/usr/local/etc/redis/users.acl"] 16 | ############## Redis ACL ############## 17 | restart: always 18 | networks: 19 | - redisnet 20 | ports: 21 | - "6379:6379" 22 | 23 | app: 24 | build: 25 | context: . 26 | # dockerfile: Dockerfile 27 | dockerfile: onlybackend.Dockerfile 28 | restart: unless-stopped 29 | depends_on: 30 | - redis 31 | environment: 32 | # Load from .env file in same directory as the compose file. 33 | # To create file, in CLI: echo "REDIS_CONNSTR=redis://redis:6379/0" > .env 34 | # DO NOT create file from Windows. It creates Unicode text, UTF-16, little-endian text, with CRLF line terminators. 35 | - REDIS_CONNSTR=${REDIS_CONNSTR} 36 | # Default Redis (No Auth or ACL). Outside docker - redis://localhost:6379/0 37 | # - REDIS_CONNSTR=redis://redis:6379/0 38 | # Using Redis ACL with Username & Password. Outside docker - redis://app-user:mysecretpassword@localhost:6379/0 39 | # - REDIS_CONNSTR=redis://app-user:mysecretpassword@redis:6379/0 40 | - TURNSTILE_ENABLED=${TURNSTILE_ENABLED} 41 | - TURNSTILE_SITE_KEY=${TURNSTILE_SITE_KEY} 42 | - TURNSTILE_SECRET_KEY=${TURNSTILE_SECRET_KEY} 43 | networks: 44 | - redisnet 45 | ports: 46 | - "8080:8080" 47 | 48 | networks: 49 | redisnet: 50 | name: redisnet -------------------------------------------------------------------------------- /docs/docs/.vitepress/theme/custom.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* --vp-home-hero-name-color: blue; */ 3 | --vp-c-brand-1: #0EA5E9; 4 | --vp-c-brand-2: #0369A1; 5 | --vp-c-brand-3: #0EA5E9; 6 | } 7 | 8 | .shadow-img { 9 | box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2); 10 | border-radius: 8px; 11 | /* Optional for rounded corners */ 12 | } 13 | 14 | .display-icon { 15 | width: 1.5rem; 16 | height: 1.5rem; 17 | display: inline-block; 18 | vertical-align: middle; 19 | } 20 | 21 | .video-play { 22 | border-radius: 8px; 23 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 24 | } 25 | 26 | /* :root { 27 | --vp-home-hero-name-color: transparent; 28 | --vp-home-hero-name-background: -webkit-linear-gradient(120deg, 29 | #bd34fe 30%, 30 | #41d1ff); 31 | --vp-home-hero-image-background-image: linear-gradient(-45deg, 32 | #bd34fe 50%, 33 | #47caff 50%); 34 | --vp-home-hero-image-filter: blur(44px); 35 | } */ -------------------------------------------------------------------------------- /docs/docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme' 2 | import './custom.css' 3 | 4 | export default DefaultTheme -------------------------------------------------------------------------------- /docs/docs/guide/configurations.md: -------------------------------------------------------------------------------- 1 | # Configurations 2 | The application's default behaviour can be altered with configuration settings. This document provides a quick overview about it. 3 | 4 | ## Auto-Delete Duration 5 | By default, data is deleted within 2 hours in Redis. This can be updated by making the below changes.\ 6 | In the src/config.toml file, update the value for auto_delete_duration 7 | 8 | ```toml{5} 9 | [data] 10 | # Format: 11 | # Units: s=seconds, m=minutes, h=hours, d=days 12 | # Examples: "50s" for 50 seconds, "5m" for 5 minutes, "2h" for 2 hours, "7d" for 7 days 13 | auto_delete_duration = "2h" 14 | ``` 15 | 16 | ## Websocket Max Message Size 17 | QuickRetro uses Websockets for communication. This configuration setting controls the max allowed size in bytes for all data sent through the websocket. 18 | 19 | In the src/config.toml file, update the value for max_message_size_bytes 20 | ```toml{4} 21 | [websocket] 22 | # Maximum message size (in bytes) allowed from peer for the websocket connection 23 | # For the front-end validation, keep the same value in (src/frontend/.env [VITE_MAX_WEBSOCKET_MESSAGE_SIZE_BYTES]) 24 | max_message_size_bytes = 1024 25 | ``` 26 | 27 | This setting is defined separately for the backend and frontend. For the frontend, this is defined in src/frontend/.env.\ 28 | Update the value for VITE_MAX_WEBSOCKET_MESSAGE_SIZE_BYTES 29 | ```ini{6} 30 | VITE_WS_PROTOCOL=wss 31 | VITE_SHOW_CONSOLE_LOGS=false 32 | # Triggers message size validation. 33 | # It is recommended to keep the same value as what's allowed in backend server (defined in src/config.toml [websocket].max_message_size_bytes). 34 | # To avoid message size validation, comment out below line. However, this will break the server websocket connection when the limit is breached. 35 | VITE_MAX_WEBSOCKET_MESSAGE_SIZE_BYTES=1024 36 | ``` 37 | ::: danger IMPORTANT 38 | Ensure the config values are same for both frontend and backend 39 | ::: 40 | 41 | ::: tip 42 | VITE_MAX_WEBSOCKET_MESSAGE_SIZE_BYTES also causes UI validation to run everytime a User type's or paste's text.\ 43 | Commenting it out will stop the validation from being run everytime.\ 44 | It is not recommended to comment out this config, unless its causing issues for users. 45 | ::: 46 | 47 | ## Allowed Origins 48 | Update the allowed_origins config setting in src/config.toml to add some degree of protection to the websocket connection.\ 49 | You will typically update this setting when [self-hosting](self-hosting). 50 | ```toml{7-14} 51 | [server] 52 | # When self-hosting, add your domain to allowed_origins list. 53 | # For e.g. if you are hosting your site at https://example.com, allowed_origins will look like - 54 | # allowed_origins = [ 55 | # "https://example.com" 56 | # ] 57 | allowed_origins = [ 58 | "http://localhost:8080", 59 | "https://localhost:8080", 60 | "http://localhost:5173", 61 | "https://localhost", 62 | "https://quickretro.app", 63 | "https://demo.quickretro.app" 64 | ] 65 | ``` 66 | 67 | ## Connecting to Redis 68 | The Go app always attempts to connect to Redis when its starts. It errors out if connecting to Redis fails. 69 | The app looks for an ENV variable named REDIS_CONNSTR for the connection details. 70 | 71 | The Redis ACL username and password can be passed as part of the url to REDIS_CONNSTR. 72 | 73 | ## Enable Cloudflare Turnstile 74 | Turnstile is a smart CAPTCHA alternative from Cloudflare used to prevent bots. It is disabled by default for the Create board page. 75 | 76 | To enable it, set the TURNSTILE_ENABLED, TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY environment variables. 77 | 78 | ```ini{2-4} 79 | REDIS_CONNSTR= 80 | TURNSTILE_ENABLED=true 81 | TURNSTILE_SITE_KEY= 82 | TURNSTILE_SECRET_KEY= 83 | ``` 84 | 85 | ::: tip 86 | You need to register with Cloudflare to get TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY. Visit [Cloudflare](https://www.cloudflare.com/en-in/application-services/products/turnstile/) for more details. 87 | ::: 88 | -------------------------------------------------------------------------------- /docs/docs/guide/create-board.md: -------------------------------------------------------------------------------- 1 | # Create Board 2 | 3 | The first thing you do is create/setup a board.\ 4 | Enter a name for the Board and an optional Team name. 5 | 6 | ::: info NOTE 7 | The board creator is also the board owner and can perform multiple actions not available to others.\ 8 | We'll soon see it in [Dashboard](dashboard) section. 9 | ::: 10 | 11 | ## Configuring Board Columns 12 | ::: tip 13 | Since the introduction of multi-language support with , default column names can be automatically translated to other 14 | languages.\ 15 | ***Custom column names are not automatically translated.***\ 16 | It is recommended to use the defaults, if any of your team members use the app in a different language. 17 | ::: 18 | 19 | A max of 5 columns are allowed. The first 3 columns are always enabled by default.\ 20 | You can choose which columns you want and name them accordingly. 21 | 22 | Create Board 23 | 24 | Click the coloured dot (***present towards left of each column name***) to enable/disable a column.\ 25 | Click the column name text to type any custom name. 26 | 27 | When a Board is created, the user is taken to the [Dashboard](dashboard). 28 | 29 | ## Quick video 30 | 31 | 35 | 36 | ## Cloudflare Turnstile Integration 37 | 38 | Available from 39 | 40 | Cloudflare Turnstile 41 | 42 | Cloudflare Turnstile is a CAPTCHA alternative provided by Cloudflare. The integration can be enabled/disabled in a configurable way. It is disabled by default. 43 | 44 | Details to enable it provided in [Configurations](configurations#enable-cloudflare-turnstile) 45 | 46 | -------------------------------------------------------------------------------- /docs/docs/guide/development.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | # Development Guide 5 | This guide is intended to help you get started with running the application locally, and making changes to it. 6 | 7 | ## Prerequisites 8 | - Go or higher 9 | - Node.js version or higher 10 | - Docker 11 | - Redis is used as the datastore and for pubsub 12 | - A text editor, preferably VS Code, and a CLI 13 | ::: info 14 | The Go app runs as a single binary with the frontend embedded inside it 15 | ::: 16 | 17 | ## Running locally 18 | The easiest way to run locally is by using Docker. 19 | 20 | ### Build Vue frontend 21 | The Vue frontend must be built first 22 | ```sh 23 | cd ./src/frontend/ 24 | npm install 25 | npm run build-dev 26 | ``` 27 | This installs the packages, dependencies, and creates assets in frontend/dist directory. This directory is embedded in the backend Golang binary when it is built. 28 | 29 | ### Run with Docker 30 | Navigate back to root directory and run - 31 | ```sh 32 | docker compose up 33 | ``` 34 | This builds and starts a docker container for the app, and another container with Redis.\ 35 | The app starts at http://localhost:8080 36 | 37 | ## Setting up for Development 38 | Ensure you have Redis running. 39 | ::: tip 40 | From the previous docker step, you can keep the Redis container running and stop the other containers 41 | ::: 42 | ### Running Go backend app 43 | To run the Go app directly (outside the container) - 44 | Open a terminal and from the root directory 45 | ```sh 46 | cd ./src/ 47 | go run . 48 | ``` 49 | This starts the Go server. You are ready to make changes to the Go app now. 50 | 51 | ::: tip 52 | Go must be installed for this step. 53 | ::: 54 | 55 | ### Running Vue frontend app 56 | Open another terminal and from the root directoy - 57 | ```sh 58 | cd ./src/frontend/ 59 | npm run dev 60 | ``` 61 | This starts the Vue app at http://localhost:5173\ 62 | Feel free to make changes to the app. -------------------------------------------------------------------------------- /docs/docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | This guide gives a quick and easy functional walkthrough of QuickRetro app.\ 4 | To start, visit the site and type in a name to join as guest. There is no signup/login process. 5 | 6 | ### Latest version 7 | 8 | ## Try the Demo 9 | Try out the [live demo](https://demo.quickretro.app). It is recommended to self-host. 10 | 11 | ::: info NOTE 12 | The name you enter initially is saved in your browser cache. It will be auto-filled the next time you visit. You can change it if needed. 13 | ::: 14 | ::: warning 15 | All data in [demo](https://demo.quickretro.app) site is deleted in 2 hours. 16 | ::: 17 | 18 | ## Supported Languages 19 | English\ 20 | 简体中文 (zh-CN) \ 21 | Español\ 22 | Deutsch\ 23 | Français\ 24 | Português (Brasil)\ 25 | Русский (ru) \ 26 | 日本語 (ja) \ 27 | Português\ 28 | Nederlands\ 29 | 한국어 (ko) \ 30 | Українська (uk) \ 31 | Italiano\ 32 | Français (Canada) -------------------------------------------------------------------------------- /docs/docs/guide/self-hosting.md: -------------------------------------------------------------------------------- 1 | # Self-Hosting 2 | 3 | Although the [demo app](https://demo.quickretro.app) has all the features and can be used as-is, it runs on low resources. The data is auto-deleted within 2 hours. It is recommended to self-host the app for better flexibility. 4 | 5 | ## Update Allowed-Origins 6 | As defined in [Configurations](configurations#allowed-origins), update the config setting with your site origin. 7 | 8 | ## Secure Redis Instance 9 | It is recommended to secure your Redis instance, preferably with ACL enabled. Check out the redis directory, and sample docker compose files compose.yml, compose.reverseproxy.yml, compose.demohosting.yml etc in [github repository](https://github.com/vijeeshr/quickretro) for more details. 10 | 11 | ## Passing ENV variables with Compose 12 | Environment variables are passed using .env file which is present in the same directory as compose*.yml files.\ 13 | Example: Create an env file with your values - 14 | ```sh 15 | echo "REDIS_CONNSTR=redis://redis:6379/0" > .env 16 | # echo "MY_VAR1=false" >> .env 17 | # echo "MY_VAR2=true" >> .env 18 | ``` 19 | ::: info 20 | To securely pass ENV vars, feel free to use an approach which suits you best. 21 | ::: 22 | ::: warning NOTE 23 | DO NOT create the file directly from Windows CMD if you intend to run the app in Linux. It creates Unicode text, UTF-16, little-endian text, with CRLF line terminators. This causes problems for Docker Compose to read the env file. 24 | 25 | On Windows, you can create the file in UTF-8 using Git Terminal. 26 | ::: 27 | 28 | ## Sample Compose files 29 | Check out the sample docker compose files compose.yml, compose.reverseproxy.yml, compose.demohosting.yml etc in [github repository](https://github.com/vijeeshr/quickretro) for more details. 30 | -------------------------------------------------------------------------------- /docs/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | title: "QuickRetro - Free and Open-Source Sprint Retrospective Meeting App" 6 | 7 | hero: 8 | name: "QuickRetro" 9 | text: "Sprint Retrospective Meeting App for Remote Agile Teams" 10 | tagline: Free, Open-Source & Self-hosted 11 | actions: 12 | - theme: brand 13 | text: Live Demo 14 | link: https://demo.quickretro.app 15 | - theme: alt 16 | text: Getting Started 17 | link: /guide/getting-started 18 | image: 19 | light: /logo_large_light.png 20 | dark: /logo_large_dark.png 21 | # src: /logo.png 22 | alt: QuickRetro 23 | 24 | features: 25 | - title: No Signups 26 | details: That's right! No need to signup or login 27 | - title: No Board Limits 28 | details: Create Boards or Invite Users without limits 29 | - title: Mobile Friendly UI 30 | details: Easily participate from your mobile phone 31 | - title: Customize Column Names 32 | details: Choose upto 5 columns with any name 33 | - title: Mask/Blur messages 34 | details: Avoid revealing messages of other participants 35 | - title: Anonymous Messages 36 | details: Post messages without revealing your name 37 | - title: Download as PDF 38 | details: Download messages as PDF 39 | - title: Countdown Timer 40 | details: Stopwatch with max 1 hour limit 41 | - title: Board Lock 42 | details: Lock to stop addition/updation of messages 43 | - title: Dark Theme 44 | details: Easily switch to use a Dark theme 45 | - title: Focussed View 46 | details: Highlight cards just for a User at a time 47 | - title: Smart CAPTCHA Integration 48 | details: Built-in integration with Cloudflare Turnstile 49 | - title: Online Presence Display 50 | details: See participants present in the meeting 51 | - title: Auto-Delete data 52 | details: Auto-delete data with configurable retention duration 53 | --- -------------------------------------------------------------------------------- /docs/docs/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/docs/docs/public/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/docs/public/createboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/docs/docs/public/createboard.png -------------------------------------------------------------------------------- /docs/docs/public/createboard_turnstile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/docs/docs/public/createboard_turnstile.png -------------------------------------------------------------------------------- /docs/docs/public/dashboard_add_cards.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/docs/docs/public/dashboard_add_cards.png -------------------------------------------------------------------------------- /docs/docs/public/dashboard_focus_panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/docs/docs/public/dashboard_focus_panel.png -------------------------------------------------------------------------------- /docs/docs/public/dashboard_guest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/docs/docs/public/dashboard_guest.png -------------------------------------------------------------------------------- /docs/docs/public/dashboard_move.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/docs/docs/public/dashboard_move.png -------------------------------------------------------------------------------- /docs/docs/public/dashboard_owner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/docs/docs/public/dashboard_owner.png -------------------------------------------------------------------------------- /docs/docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/docs/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/docs/docs/public/logo.png -------------------------------------------------------------------------------- /docs/docs/public/logo_large_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/docs/docs/public/logo_large_dark.png -------------------------------------------------------------------------------- /docs/docs/public/logo_large_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/docs/docs/public/logo_large_light.png -------------------------------------------------------------------------------- /docs/docs/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | 4 | Sitemap: https://quickretro.app/sitemap.xml -------------------------------------------------------------------------------- /docs/docs/public/videos/add-update-message.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/docs/docs/public/videos/add-update-message.mp4 -------------------------------------------------------------------------------- /docs/docs/public/videos/create-board.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/docs/docs/public/videos/create-board.mp4 -------------------------------------------------------------------------------- /docs/docs/public/videos/start-stop-timer.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/docs/docs/public/videos/start-stop-timer.mp4 -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "vitepress": "^1.6.3" 4 | }, 5 | "scripts": { 6 | "docs:dev": "vitepress dev docs", 7 | "docs:build": "vitepress build docs", 8 | "docs:preview": "vitepress preview docs" 9 | } 10 | } -------------------------------------------------------------------------------- /homepage/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 404 | QuickRetro 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 42 | 43 | 44 | 45 | 46 | 47 |
48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /homepage/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/apple-touch-icon.png -------------------------------------------------------------------------------- /homepage/assets/app.rh1oyAvj.js: -------------------------------------------------------------------------------- 1 | import{t as p}from"./chunks/theme.Z7SuLSAi.js";import{R as s,a0 as i,a1 as u,a2 as c,a3 as l,a4 as f,a5 as d,a6 as m,a7 as h,a8 as g,a9 as A,d as v,u as y,v as C,s as P,aa as b,ab as w,ac as R,ad as E}from"./chunks/framework.CTVYQtO4.js";function r(e){if(e.extends){const a=r(e.extends);return{...a,...e,async enhanceApp(t){a.enhanceApp&&await a.enhanceApp(t),e.enhanceApp&&await e.enhanceApp(t)}}}return e}const n=r(p),S=v({name:"VitePressApp",setup(){const{site:e,lang:a,dir:t}=y();return C(()=>{P(()=>{document.documentElement.lang=a.value,document.documentElement.dir=t.value})}),e.value.router.prefetchLinks&&b(),w(),R(),n.setup&&n.setup(),()=>E(n.Layout)}});async function T(){globalThis.__VITEPRESS__=!0;const e=_(),a=D();a.provide(u,e);const t=c(e.route);return a.provide(l,t),a.component("Content",f),a.component("ClientOnly",d),Object.defineProperties(a.config.globalProperties,{$frontmatter:{get(){return t.frontmatter.value}},$params:{get(){return t.page.value.params}}}),n.enhanceApp&&await n.enhanceApp({app:a,router:e,siteData:m}),{app:a,router:e,data:t}}function D(){return A(S)}function _(){let e=s;return h(a=>{let t=g(a),o=null;return t&&(e&&(t=t.replace(/\.js$/,".lean.js")),o=import(t)),s&&(e=!1),o},n.NotFound)}s&&T().then(({app:e,router:a,data:t})=>{a.go().then(()=>{i(a.route,t.site),e.mount("#app")})});export{T as createApp}; 2 | -------------------------------------------------------------------------------- /homepage/assets/guide_configurations.md.CT2ZnaGS.lean.js: -------------------------------------------------------------------------------- 1 | import{_ as i,c as e,o as a,ae as t}from"./chunks/framework.CTVYQtO4.js";const k=JSON.parse('{"title":"Configurations","description":"","frontmatter":{},"headers":[],"relativePath":"guide/configurations.md","filePath":"guide/configurations.md","lastUpdated":1743150417000}'),n={name:"guide/configurations.md"};function l(o,s,h,p,d,r){return a(),e("div",null,s[0]||(s[0]=[t("",24)]))}const g=i(n,[["render",l]]);export{k as __pageData,g as default}; 2 | -------------------------------------------------------------------------------- /homepage/assets/guide_create-board.md.Dg_d45NA.js: -------------------------------------------------------------------------------- 1 | import{v as i,C as s,c as d,o as u,ae as n,j as a,a as t,G as l}from"./chunks/framework.CTVYQtO4.js";const m="/createboard.png",c="/videos/create-board.mp4",p="/createboard_turnstile.png",b={class:"tip custom-block"},v=JSON.parse('{"title":"Create Board","description":"","frontmatter":{},"headers":[],"relativePath":"guide/create-board.md","filePath":"guide/create-board.md","lastUpdated":1743150417000}'),f={name:"guide/create-board.md"},C=Object.assign(f,{setup(g){return i(()=>{const o=document.getElementById("createBoardVideo");o&&(o.playbackRate=2.5)}),(o,e)=>{const r=s("Badge");return u(),d("div",null,[e[8]||(e[8]=n('

Create Board

The first thing you do is create/setup a board.
Enter a name for the Board and an optional Team name.

NOTE

The board creator is also the board owner and can perform multiple actions not available to others.
We'll soon see it in Dashboard section.

Configuring Board Columns

',4)),a("div",b,[e[6]||(e[6]=a("p",{class:"custom-block-title"},"TIP",-1)),a("p",null,[e[0]||(e[0]=t("Since the introduction of multi-language support with ")),l(r,{type:"tip",text:"v1.3.0"}),e[1]||(e[1]=t(", default column names can be automatically translated to other languages.")),e[2]||(e[2]=a("br",null,null,-1)),e[3]||(e[3]=a("em",null,[a("strong",null,"Custom column names are not automatically translated.")],-1)),e[4]||(e[4]=a("br",null,null,-1)),e[5]||(e[5]=t(" It is recommended to use the defaults, if any of your team members use the app in a different language."))])]),e[9]||(e[9]=n('

A max of 5 columns are allowed. The first 3 columns are always enabled by default.
You can choose which columns you want and name them accordingly.

Create Board

Click the coloured dot (present towards left of each column name) to enable/disable a column.
Click the column name text to type any custom name.

When a Board is created, the user is taken to the Dashboard.

Quick video

Cloudflare Turnstile Integration

',7)),a("p",null,[e[7]||(e[7]=t("Available from ")),l(r,{type:"tip",text:"v1.4.0"})]),e[10]||(e[10]=a("img",{src:p,class:"shadow-img",alt:"Cloudflare Turnstile",width:"360",loading:"lazy"},null,-1)),e[11]||(e[11]=a("p",null,"Cloudflare Turnstile is a CAPTCHA alternative provided by Cloudflare. The integration can be enabled/disabled in a configurable way. It is disabled by default.",-1)),e[12]||(e[12]=a("p",null,[t("Details to enable it provided in "),a("a",{href:"./configurations#enable-cloudflare-turnstile"},"Configurations")],-1))])}}});export{v as __pageData,C as default}; 2 | -------------------------------------------------------------------------------- /homepage/assets/guide_create-board.md.Dg_d45NA.lean.js: -------------------------------------------------------------------------------- 1 | import{v as i,C as s,c as d,o as u,ae as n,j as a,a as t,G as l}from"./chunks/framework.CTVYQtO4.js";const m="/createboard.png",c="/videos/create-board.mp4",p="/createboard_turnstile.png",b={class:"tip custom-block"},v=JSON.parse('{"title":"Create Board","description":"","frontmatter":{},"headers":[],"relativePath":"guide/create-board.md","filePath":"guide/create-board.md","lastUpdated":1743150417000}'),f={name:"guide/create-board.md"},C=Object.assign(f,{setup(g){return i(()=>{const o=document.getElementById("createBoardVideo");o&&(o.playbackRate=2.5)}),(o,e)=>{const r=s("Badge");return u(),d("div",null,[e[8]||(e[8]=n("",4)),a("div",b,[e[6]||(e[6]=a("p",{class:"custom-block-title"},"TIP",-1)),a("p",null,[e[0]||(e[0]=t("Since the introduction of multi-language support with ")),l(r,{type:"tip",text:"v1.3.0"}),e[1]||(e[1]=t(", default column names can be automatically translated to other languages.")),e[2]||(e[2]=a("br",null,null,-1)),e[3]||(e[3]=a("em",null,[a("strong",null,"Custom column names are not automatically translated.")],-1)),e[4]||(e[4]=a("br",null,null,-1)),e[5]||(e[5]=t(" It is recommended to use the defaults, if any of your team members use the app in a different language."))])]),e[9]||(e[9]=n("",7)),a("p",null,[e[7]||(e[7]=t("Available from ")),l(r,{type:"tip",text:"v1.4.0"})]),e[10]||(e[10]=a("img",{src:p,class:"shadow-img",alt:"Cloudflare Turnstile",width:"360",loading:"lazy"},null,-1)),e[11]||(e[11]=a("p",null,"Cloudflare Turnstile is a CAPTCHA alternative provided by Cloudflare. The integration can be enabled/disabled in a configurable way. It is disabled by default.",-1)),e[12]||(e[12]=a("p",null,[t("Details to enable it provided in "),a("a",{href:"./configurations#enable-cloudflare-turnstile"},"Configurations")],-1))])}}});export{v as __pageData,C as default}; 2 | -------------------------------------------------------------------------------- /homepage/assets/guide_dashboard.md.DNu-gDWb.lean.js: -------------------------------------------------------------------------------- 1 | import{_ as p,C as u,c as l,o as i,j as o,ae as r,a,G as t}from"./chunks/framework.CTVYQtO4.js";const h="/dashboard_owner.png",m="/dashboard_guest.png",c="/dashboard_add_cards.png",g="/videos/add-update-message.mp4",v="/dashboard_move.png",k="/videos/start-stop-timer.mp4",b="/dashboard_focus_panel.png",q=JSON.parse('{"title":"Dashboard","description":"","frontmatter":{},"headers":[],"relativePath":"guide/dashboard.md","filePath":"guide/dashboard.md","lastUpdated":1748342669000}'),w={name:"guide/dashboard.md"},f={class:"danger custom-block"},T={id:"quick-video-for-versions-prior-to",tabindex:"-1"};function y(n,e,x,M,A,P){const s=u("Badge");return i(),l("div",null,[e[23]||(e[23]=o("h1",{id:"dashboard",tabindex:"-1"},[a("Dashboard "),o("a",{class:"header-anchor",href:"#dashboard","aria-label":'Permalink to "Dashboard"'},"​")],-1)),e[24]||(e[24]=o("p",null,"This guide gives a quick overview of the dashboard and all its features.",-1)),e[25]||(e[25]=o("p",null,[a("The left side-bar has all the action controls. The "),o("em",null,[o("strong",null,"board creator a.k.a owner")]),a(" has more controls than a guest user.")],-1)),e[26]||(e[26]=o("img",{src:h,class:"shadow-img",alt:"Dashboard",width:"640",loading:"lazy"},null,-1)),e[27]||(e[27]=o("p",null,"The left side-bar for a guest user has fewer controls.",-1)),e[28]||(e[28]=o("img",{src:m,class:"shadow-img",alt:"Dashboard",width:"640",loading:"lazy"},null,-1)),o("p",null,[e[1]||(e[1]=a("The right-sidebar shows a real-time display of all participants who are currently in the meeting.")),e[2]||(e[2]=o("br",null,null,-1)),e[3]||(e[3]=a(" From ")),t(s,{type:"tip",text:"v1.2.0"}),e[4]||(e[4]=a(" onwards, each participant's message count is also displayed."))]),e[29]||(e[29]=r("",6)),o("div",f,[e[11]||(e[11]=o("p",{class:"custom-block-title"},"BEHAVIOR CHANGE",-1)),o("p",null,[e[5]||(e[5]=a("From ")),t(s,{type:"danger",text:"v1.2.0"}),e[6]||(e[6]=a(", each column has dedicated buttons to create messages. Columns names no longer act as buttons.")),e[7]||(e[7]=o("br",null,null,-1)),e[8]||(e[8]=a(" To create a message, click the ")),e[9]||(e[9]=o("strong",null,"+",-1)),e[10]||(e[10]=a(" button."))])]),e[30]||(e[30]=o("img",{src:c,class:"shadow-img",alt:"Dashboard Add Cards",width:"312",loading:"lazy"},null,-1)),e[31]||(e[31]=o("p",null,[a("For all older versions, users can add messages by clicking on the Column name ("),o("em",null,"Yup! They are buttons"),a(")."),o("br"),a(" Type in text and press Enter "),o("em",null,"or"),a(" click anywhere else on the page. The message is instantly sent to all.")],-1)),e[32]||(e[32]=o("p",null,[a("To Update a message, click on the text and the card becomes updateable."),o("br"),a(" Press Enter "),o("em",null,"or"),a(" click anywhere else on the page. The update is instantly sent to all.")],-1)),o("h3",T,[e[12]||(e[12]=a("Quick video - For versions prior to ")),t(s,{type:"tip",text:"v1.2.0"}),e[13]||(e[13]=a()),e[14]||(e[14]=o("a",{class:"header-anchor",href:"#quick-video-for-versions-prior-to","aria-label":'Permalink to "Quick video - For versions prior to "'},"​",-1))]),e[33]||(e[33]=o("video",{class:"video-play",controls:"",width:"640"},[o("source",{src:g,type:"video/webm"}),a(" Your browser does not support the video tag. ")],-1)),e[34]||(e[34]=o("h2",{id:"anonymous-message",tabindex:"-1"},[a("Anonymous message "),o("a",{class:"header-anchor",href:"#anonymous-message","aria-label":'Permalink to "Anonymous message"'},"​")],-1)),o("p",null,[e[15]||(e[15]=a("Available from ")),t(s,{type:"tip",text:"v1.2.0"})]),e[35]||(e[35]=r("",7)),o("p",null,[e[16]||(e[16]=a("Available from ")),t(s,{type:"tip",text:"v1.2.0"})]),e[36]||(e[36]=r("",22)),o("p",null,[e[17]||(e[17]=a("Available from ")),t(s,{type:"tip",text:"v1.1.0"})]),e[37]||(e[37]=r("",2)),o("p",null,[e[18]||(e[18]=a("Available from ")),t(s,{type:"tip",text:"v1.3.0"})]),e[38]||(e[38]=r("",3)),o("p",null,[e[19]||(e[19]=a("Available from ")),t(s,{type:"tip",text:"^v1.3.0"})]),o("p",null,[e[21]||(e[21]=a("Use the ")),(i(),l("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",onClick:e[0]||(e[0]=(...d)=>n.openLanguageDialog&&n.openLanguageDialog(...d)),class:"display-icon"},e[20]||(e[20]=[o("circle",{cx:"12",cy:"12",r:"10"},null,-1),o("path",{d:"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"},null,-1),o("path",{d:"M2 12h20"},null,-1)]))),e[22]||(e[22]=a(" button to change the current language."))])])}const B=p(w,[["render",y]]);export{q as __pageData,B as default}; 2 | -------------------------------------------------------------------------------- /homepage/assets/guide_dashboard.md.giEGfGkw.lean.js: -------------------------------------------------------------------------------- 1 | import{_ as p,C as u,c as l,o as i,j as o,ae as r,a,G as t}from"./chunks/framework.CTVYQtO4.js";const h="/dashboard_owner.png",m="/dashboard_guest.png",c="/dashboard_add_cards.png",g="/videos/add-update-message.mp4",v="/dashboard_move.png",k="/videos/start-stop-timer.mp4",b="/dashboard_focus_panel.png",q=JSON.parse('{"title":"Dashboard","description":"","frontmatter":{},"headers":[],"relativePath":"guide/dashboard.md","filePath":"guide/dashboard.md","lastUpdated":1743157788000}'),w={name:"guide/dashboard.md"},f={class:"danger custom-block"},T={id:"quick-video-for-versions-prior-to",tabindex:"-1"};function y(n,e,x,M,A,P){const s=u("Badge");return i(),l("div",null,[e[23]||(e[23]=o("h1",{id:"dashboard",tabindex:"-1"},[a("Dashboard "),o("a",{class:"header-anchor",href:"#dashboard","aria-label":'Permalink to "Dashboard"'},"​")],-1)),e[24]||(e[24]=o("p",null,"This guide gives a quick overview of the dashboard and all its features.",-1)),e[25]||(e[25]=o("p",null,[a("The left side-bar has all the action controls. The "),o("em",null,[o("strong",null,"board creator a.k.a owner")]),a(" has more controls than a guest user.")],-1)),e[26]||(e[26]=o("img",{src:h,class:"shadow-img",alt:"Dashboard",width:"640",loading:"lazy"},null,-1)),e[27]||(e[27]=o("p",null,"The left side-bar for a guest user has fewer controls.",-1)),e[28]||(e[28]=o("img",{src:m,class:"shadow-img",alt:"Dashboard",width:"640",loading:"lazy"},null,-1)),o("p",null,[e[1]||(e[1]=a("The right-sidebar shows a real-time display of all participants who are currently in the meeting.")),e[2]||(e[2]=o("br",null,null,-1)),e[3]||(e[3]=a(" From ")),t(s,{type:"tip",text:"v1.2.0"}),e[4]||(e[4]=a(" onwards, each participant's message count is also displayed."))]),e[29]||(e[29]=r("",6)),o("div",f,[e[11]||(e[11]=o("p",{class:"custom-block-title"},"BEHAVIOR CHANGE",-1)),o("p",null,[e[5]||(e[5]=a("From ")),t(s,{type:"danger",text:"v1.2.0"}),e[6]||(e[6]=a(", each column has dedicated buttons to create messages. Columns names no longer act as buttons.")),e[7]||(e[7]=o("br",null,null,-1)),e[8]||(e[8]=a(" To create a message, click the ")),e[9]||(e[9]=o("strong",null,"+",-1)),e[10]||(e[10]=a(" button."))])]),e[30]||(e[30]=o("img",{src:c,class:"shadow-img",alt:"Dashboard Add Cards",width:"312",loading:"lazy"},null,-1)),e[31]||(e[31]=o("p",null,[a("For all older versions, users can add messages by clicking on the Column name ("),o("em",null,"Yup! They are buttons"),a(")."),o("br"),a(" Type in text and press Enter "),o("em",null,"or"),a(" click anywhere else on the page. The message is instantly sent to all.")],-1)),e[32]||(e[32]=o("p",null,[a("To Update a message, click on the text and the card becomes updateable."),o("br"),a(" Press Enter "),o("em",null,"or"),a(" click anywhere else on the page. The update is instantly sent to all.")],-1)),o("h3",T,[e[12]||(e[12]=a("Quick video - For versions prior to ")),t(s,{type:"tip",text:"v1.2.0"}),e[13]||(e[13]=a()),e[14]||(e[14]=o("a",{class:"header-anchor",href:"#quick-video-for-versions-prior-to","aria-label":'Permalink to "Quick video - For versions prior to "'},"​",-1))]),e[33]||(e[33]=o("video",{class:"video-play",controls:"",width:"640"},[o("source",{src:g,type:"video/webm"}),a(" Your browser does not support the video tag. ")],-1)),e[34]||(e[34]=o("h2",{id:"anonymous-message",tabindex:"-1"},[a("Anonymous message "),o("a",{class:"header-anchor",href:"#anonymous-message","aria-label":'Permalink to "Anonymous message"'},"​")],-1)),o("p",null,[e[15]||(e[15]=a("Available from ")),t(s,{type:"tip",text:"v1.2.0"})]),e[35]||(e[35]=r("",7)),o("p",null,[e[16]||(e[16]=a("Available from ")),t(s,{type:"tip",text:"v1.2.0"})]),e[36]||(e[36]=r("",22)),o("p",null,[e[17]||(e[17]=a("Available from ")),t(s,{type:"tip",text:"v1.1.0"})]),e[37]||(e[37]=r("",2)),o("p",null,[e[18]||(e[18]=a("Available from ")),t(s,{type:"tip",text:"v1.3.0"})]),e[38]||(e[38]=r("",3)),o("p",null,[e[19]||(e[19]=a("Available from ")),t(s,{type:"tip",text:"^v1.3.0"})]),o("p",null,[e[21]||(e[21]=a("Use the ")),(i(),l("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",onClick:e[0]||(e[0]=(...d)=>n.openLanguageDialog&&n.openLanguageDialog(...d)),class:"display-icon"},e[20]||(e[20]=[o("circle",{cx:"12",cy:"12",r:"10"},null,-1),o("path",{d:"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"},null,-1),o("path",{d:"M2 12h20"},null,-1)]))),e[22]||(e[22]=a(" button to change the current language."))])])}const B=p(w,[["render",y]]);export{q as __pageData,B as default}; 2 | -------------------------------------------------------------------------------- /homepage/assets/guide_dashboard.md.mJgkaj93.lean.js: -------------------------------------------------------------------------------- 1 | import{_ as p,C as u,c as l,o as i,j as o,ae as r,a,G as t}from"./chunks/framework.CTVYQtO4.js";const h="/dashboard_owner.png",m="/dashboard_guest.png",c="/dashboard_add_cards.png",g="/videos/add-update-message.mp4",v="/dashboard_move.png",k="/videos/start-stop-timer.mp4",b="/dashboard_focus_panel.png",q=JSON.parse('{"title":"Dashboard","description":"","frontmatter":{},"headers":[],"relativePath":"guide/dashboard.md","filePath":"guide/dashboard.md","lastUpdated":1743150417000}'),w={name:"guide/dashboard.md"},f={class:"danger custom-block"},T={id:"quick-video-for-versions-prior-to",tabindex:"-1"};function y(n,e,x,M,A,P){const s=u("Badge");return i(),l("div",null,[e[23]||(e[23]=o("h1",{id:"dashboard",tabindex:"-1"},[a("Dashboard "),o("a",{class:"header-anchor",href:"#dashboard","aria-label":'Permalink to "Dashboard"'},"​")],-1)),e[24]||(e[24]=o("p",null,"This guide gives a quick overview of the dashboard and all its features.",-1)),e[25]||(e[25]=o("p",null,[a("The left side-bar has all the action controls. The "),o("em",null,[o("strong",null,"board creator a.k.a owner")]),a(" has more controls than a guest user.")],-1)),e[26]||(e[26]=o("img",{src:h,class:"shadow-img",alt:"Dashboard",width:"640",loading:"lazy"},null,-1)),e[27]||(e[27]=o("p",null,"The left side-bar for a guest user has fewer controls.",-1)),e[28]||(e[28]=o("img",{src:m,class:"shadow-img",alt:"Dashboard",width:"640",loading:"lazy"},null,-1)),o("p",null,[e[1]||(e[1]=a("The right-sidebar shows a real-time display of all participants who are currently in the meeting.")),e[2]||(e[2]=o("br",null,null,-1)),e[3]||(e[3]=a(" From ")),t(s,{type:"tip",text:"v1.2.0"}),e[4]||(e[4]=a(" onwards, each participant's message count is also displayed."))]),e[29]||(e[29]=r("",6)),o("div",f,[e[11]||(e[11]=o("p",{class:"custom-block-title"},"BEHAVIOR CHANGE",-1)),o("p",null,[e[5]||(e[5]=a("From ")),t(s,{type:"danger",text:"v1.2.0"}),e[6]||(e[6]=a(", each column has dedicated buttons to create messages. Columns names no longer act as buttons.")),e[7]||(e[7]=o("br",null,null,-1)),e[8]||(e[8]=a(" To create a message, click the ")),e[9]||(e[9]=o("strong",null,"+",-1)),e[10]||(e[10]=a(" button."))])]),e[30]||(e[30]=o("img",{src:c,class:"shadow-img",alt:"Dashboard Add Cards",width:"312",loading:"lazy"},null,-1)),e[31]||(e[31]=o("p",null,[a("For all older versions, users can add messages by clicking on the Column name ("),o("em",null,"Yup! They are buttons"),a(")."),o("br"),a(" Type in text and press Enter "),o("em",null,"or"),a(" click anywhere else on the page. The message is instantly sent to all.")],-1)),e[32]||(e[32]=o("p",null,[a("To Update a message, click on the text and the card becomes updateable."),o("br"),a(" Press Enter "),o("em",null,"or"),a(" click anywhere else on the page. The update is instantly sent to all.")],-1)),o("h3",T,[e[12]||(e[12]=a("Quick video - For versions prior to ")),t(s,{type:"tip",text:"v1.2.0"}),e[13]||(e[13]=a()),e[14]||(e[14]=o("a",{class:"header-anchor",href:"#quick-video-for-versions-prior-to","aria-label":'Permalink to "Quick video - For versions prior to "'},"​",-1))]),e[33]||(e[33]=o("video",{class:"video-play",controls:"",width:"640"},[o("source",{src:g,type:"video/webm"}),a(" Your browser does not support the video tag. ")],-1)),e[34]||(e[34]=o("h2",{id:"anonymous-message",tabindex:"-1"},[a("Anonymous message "),o("a",{class:"header-anchor",href:"#anonymous-message","aria-label":'Permalink to "Anonymous message"'},"​")],-1)),o("p",null,[e[15]||(e[15]=a("Available from ")),t(s,{type:"tip",text:"v1.2.0"})]),e[35]||(e[35]=r("",7)),o("p",null,[e[16]||(e[16]=a("Available from ")),t(s,{type:"tip",text:"v1.2.0"})]),e[36]||(e[36]=r("",22)),o("p",null,[e[17]||(e[17]=a("Available from ")),t(s,{type:"tip",text:"v1.1.0"})]),e[37]||(e[37]=r("",2)),o("p",null,[e[18]||(e[18]=a("Available from ")),t(s,{type:"tip",text:"v1.3.0"})]),e[38]||(e[38]=r("",3)),o("p",null,[e[19]||(e[19]=a("Available from ")),t(s,{type:"tip",text:"^v1.3.0"})]),o("p",null,[e[21]||(e[21]=a("Use the ")),(i(),l("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",onClick:e[0]||(e[0]=(...d)=>n.openLanguageDialog&&n.openLanguageDialog(...d)),class:"display-icon"},e[20]||(e[20]=[o("circle",{cx:"12",cy:"12",r:"10"},null,-1),o("path",{d:"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"},null,-1),o("path",{d:"M2 12h20"},null,-1)]))),e[22]||(e[22]=a(" button to change the current language."))])])}const B=p(w,[["render",y]]);export{q as __pageData,B as default}; 2 | -------------------------------------------------------------------------------- /homepage/assets/guide_development.md.BY4ATYwc.lean.js: -------------------------------------------------------------------------------- 1 | import{_ as n,C as l,c as o,o as r,j as a,ae as p,a as s,G as t}from"./chunks/framework.CTVYQtO4.js";const F=JSON.parse('{"title":"Development Guide","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"guide/development.md","filePath":"guide/development.md","lastUpdated":1743150417000}'),d={name:"guide/development.md"};function h(u,e,k,c,g,b){const i=l("Badge");return r(),o("div",null,[e[7]||(e[7]=a("h1",{id:"development-guide",tabindex:"-1"},[s("Development Guide "),a("a",{class:"header-anchor",href:"#development-guide","aria-label":'Permalink to "Development Guide"'},"​")],-1)),e[8]||(e[8]=a("p",null,"This guide is intended to help you get started with running the application locally, and making changes to it.",-1)),e[9]||(e[9]=a("h2",{id:"prerequisites",tabindex:"-1"},[s("Prerequisites "),a("a",{class:"header-anchor",href:"#prerequisites","aria-label":'Permalink to "Prerequisites"'},"​")],-1)),a("ul",null,[a("li",null,[e[0]||(e[0]=s("Go ")),t(i,{type:"tip",text:"1.22.0"}),e[1]||(e[1]=s(" or higher"))]),a("li",null,[e[2]||(e[2]=s("Node.js version ")),t(i,{type:"tip",text:"20.10.0"}),e[3]||(e[3]=s(" or higher"))]),e[4]||(e[4]=a("li",null,"Docker",-1)),e[5]||(e[5]=a("li",null,"Redis is used as the datastore and for pubsub",-1)),e[6]||(e[6]=a("li",null,"A text editor, preferably VS Code, and a CLI",-1))]),e[10]||(e[10]=p("",23))])}const v=n(d,[["render",h]]);export{F as __pageData,v as default}; 2 | -------------------------------------------------------------------------------- /homepage/assets/guide_development.md.Ds4A4Cqt.lean.js: -------------------------------------------------------------------------------- 1 | import{_ as n,C as l,c as o,o as r,j as a,ae as p,a as s,G as t}from"./chunks/framework.CTVYQtO4.js";const F=JSON.parse('{"title":"Development Guide","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"guide/development.md","filePath":"guide/development.md","lastUpdated":1743150417000}'),d={name:"guide/development.md"};function h(u,e,k,c,g,b){const i=l("Badge");return r(),o("div",null,[e[7]||(e[7]=a("h1",{id:"development-guide",tabindex:"-1"},[s("Development Guide "),a("a",{class:"header-anchor",href:"#development-guide","aria-label":'Permalink to "Development Guide"'},"​")],-1)),e[8]||(e[8]=a("p",null,"This guide is intended to help you get started with running the application locally, and making changes to it.",-1)),e[9]||(e[9]=a("h2",{id:"prerequisites",tabindex:"-1"},[s("Prerequisites "),a("a",{class:"header-anchor",href:"#prerequisites","aria-label":'Permalink to "Prerequisites"'},"​")],-1)),a("ul",null,[a("li",null,[e[0]||(e[0]=s("Go ")),t(i,{type:"tip",text:"1.24.2"}),e[1]||(e[1]=s(" or higher"))]),a("li",null,[e[2]||(e[2]=s("Node.js version ")),t(i,{type:"tip",text:"20.10.0"}),e[3]||(e[3]=s(" or higher"))]),e[4]||(e[4]=a("li",null,"Docker",-1)),e[5]||(e[5]=a("li",null,"Redis is used as the datastore and for pubsub",-1)),e[6]||(e[6]=a("li",null,"A text editor, preferably VS Code, and a CLI",-1))]),e[10]||(e[10]=p("",23))])}const v=n(d,[["render",h]]);export{F as __pageData,v as default}; 2 | -------------------------------------------------------------------------------- /homepage/assets/guide_getting-started.md.CP0vWESL.js: -------------------------------------------------------------------------------- 1 | import{_ as r,C as i,c as s,o as a,j as n,ae as d,a as l,G as o}from"./chunks/framework.CTVYQtO4.js";const v=JSON.parse('{"title":"Getting Started","description":"","frontmatter":{},"headers":[],"relativePath":"guide/getting-started.md","filePath":"guide/getting-started.md","lastUpdated":1743150417000}'),u={name:"guide/getting-started.md"},p={id:"latest-version",tabindex:"-1"};function g(m,t,b,f,y,P){const e=i("Badge");return a(),s("div",null,[t[30]||(t[30]=n("h1",{id:"getting-started",tabindex:"-1"},[l("Getting Started "),n("a",{class:"header-anchor",href:"#getting-started","aria-label":'Permalink to "Getting Started"'},"​")],-1)),t[31]||(t[31]=n("p",null,[l("This guide gives a quick and easy functional walkthrough of QuickRetro app."),n("br"),l(" To start, visit the site and type in a name to join as guest. There is no signup/login process.")],-1)),n("h3",p,[t[0]||(t[0]=l("Latest version ")),o(e,{type:"tip",text:"v1.5.1"}),t[1]||(t[1]=l()),t[2]||(t[2]=n("a",{class:"header-anchor",href:"#latest-version","aria-label":'Permalink to "Latest version "'},"​",-1))]),t[32]||(t[32]=d('

Try the Demo

Try out the live demo. It is recommended to self-host.

NOTE

The name you enter initially is saved in your browser cache. It will be auto-filled the next time you visit. You can change it if needed.

WARNING

All data in demo site is deleted in 2 hours.

Supported Languages

',5)),n("p",null,[t[3]||(t[3]=l("English")),t[4]||(t[4]=n("br",null,null,-1)),t[5]||(t[5]=l(" 简体中文 (zh-CN) ")),o(e,{type:"info",text:"No PDF download. Only Print option."}),t[6]||(t[6]=n("br",null,null,-1)),t[7]||(t[7]=l(" Español")),t[8]||(t[8]=n("br",null,null,-1)),t[9]||(t[9]=l(" Deutsch")),t[10]||(t[10]=n("br",null,null,-1)),t[11]||(t[11]=l(" Français")),t[12]||(t[12]=n("br",null,null,-1)),t[13]||(t[13]=l(" Português (Brasil)")),t[14]||(t[14]=n("br",null,null,-1)),t[15]||(t[15]=l(" Русский (ru) ")),o(e,{type:"info",text:"No PDF download. Only Print option."}),t[16]||(t[16]=n("br",null,null,-1)),t[17]||(t[17]=l(" 日本語 (ja) ")),o(e,{type:"info",text:"No PDF download. Only Print option."}),t[18]||(t[18]=n("br",null,null,-1)),t[19]||(t[19]=l(" Português")),t[20]||(t[20]=n("br",null,null,-1)),t[21]||(t[21]=l(" Nederlands")),t[22]||(t[22]=n("br",null,null,-1)),t[23]||(t[23]=l(" 한국어 (ko) ")),o(e,{type:"info",text:"No PDF download. Only Print option."}),t[24]||(t[24]=n("br",null,null,-1)),t[25]||(t[25]=l(" Українська (uk) ")),o(e,{type:"info",text:"No PDF download. Only Print option."}),t[26]||(t[26]=n("br",null,null,-1)),t[27]||(t[27]=l(" Italiano")),t[28]||(t[28]=n("br",null,null,-1)),t[29]||(t[29]=l(" Français (Canada)"))])])}const x=r(u,[["render",g]]);export{v as __pageData,x as default}; 2 | -------------------------------------------------------------------------------- /homepage/assets/guide_getting-started.md.CP0vWESL.lean.js: -------------------------------------------------------------------------------- 1 | import{_ as r,C as i,c as s,o as a,j as n,ae as d,a as l,G as o}from"./chunks/framework.CTVYQtO4.js";const v=JSON.parse('{"title":"Getting Started","description":"","frontmatter":{},"headers":[],"relativePath":"guide/getting-started.md","filePath":"guide/getting-started.md","lastUpdated":1743150417000}'),u={name:"guide/getting-started.md"},p={id:"latest-version",tabindex:"-1"};function g(m,t,b,f,y,P){const e=i("Badge");return a(),s("div",null,[t[30]||(t[30]=n("h1",{id:"getting-started",tabindex:"-1"},[l("Getting Started "),n("a",{class:"header-anchor",href:"#getting-started","aria-label":'Permalink to "Getting Started"'},"​")],-1)),t[31]||(t[31]=n("p",null,[l("This guide gives a quick and easy functional walkthrough of QuickRetro app."),n("br"),l(" To start, visit the site and type in a name to join as guest. There is no signup/login process.")],-1)),n("h3",p,[t[0]||(t[0]=l("Latest version ")),o(e,{type:"tip",text:"v1.5.1"}),t[1]||(t[1]=l()),t[2]||(t[2]=n("a",{class:"header-anchor",href:"#latest-version","aria-label":'Permalink to "Latest version "'},"​",-1))]),t[32]||(t[32]=d("",5)),n("p",null,[t[3]||(t[3]=l("English")),t[4]||(t[4]=n("br",null,null,-1)),t[5]||(t[5]=l(" 简体中文 (zh-CN) ")),o(e,{type:"info",text:"No PDF download. Only Print option."}),t[6]||(t[6]=n("br",null,null,-1)),t[7]||(t[7]=l(" Español")),t[8]||(t[8]=n("br",null,null,-1)),t[9]||(t[9]=l(" Deutsch")),t[10]||(t[10]=n("br",null,null,-1)),t[11]||(t[11]=l(" Français")),t[12]||(t[12]=n("br",null,null,-1)),t[13]||(t[13]=l(" Português (Brasil)")),t[14]||(t[14]=n("br",null,null,-1)),t[15]||(t[15]=l(" Русский (ru) ")),o(e,{type:"info",text:"No PDF download. Only Print option."}),t[16]||(t[16]=n("br",null,null,-1)),t[17]||(t[17]=l(" 日本語 (ja) ")),o(e,{type:"info",text:"No PDF download. Only Print option."}),t[18]||(t[18]=n("br",null,null,-1)),t[19]||(t[19]=l(" Português")),t[20]||(t[20]=n("br",null,null,-1)),t[21]||(t[21]=l(" Nederlands")),t[22]||(t[22]=n("br",null,null,-1)),t[23]||(t[23]=l(" 한국어 (ko) ")),o(e,{type:"info",text:"No PDF download. Only Print option."}),t[24]||(t[24]=n("br",null,null,-1)),t[25]||(t[25]=l(" Українська (uk) ")),o(e,{type:"info",text:"No PDF download. Only Print option."}),t[26]||(t[26]=n("br",null,null,-1)),t[27]||(t[27]=l(" Italiano")),t[28]||(t[28]=n("br",null,null,-1)),t[29]||(t[29]=l(" Français (Canada)"))])])}const x=r(u,[["render",g]]);export{v as __pageData,x as default}; 2 | -------------------------------------------------------------------------------- /homepage/assets/guide_getting-started.md.Clt8uJRh.js: -------------------------------------------------------------------------------- 1 | import{_ as r,C as i,c as s,o as a,j as n,ae as d,a as l,G as o}from"./chunks/framework.CTVYQtO4.js";const v=JSON.parse('{"title":"Getting Started","description":"","frontmatter":{},"headers":[],"relativePath":"guide/getting-started.md","filePath":"guide/getting-started.md","lastUpdated":1743150417000}'),u={name:"guide/getting-started.md"},p={id:"latest-version",tabindex:"-1"};function g(m,t,b,f,y,P){const e=i("Badge");return a(),s("div",null,[t[30]||(t[30]=n("h1",{id:"getting-started",tabindex:"-1"},[l("Getting Started "),n("a",{class:"header-anchor",href:"#getting-started","aria-label":'Permalink to "Getting Started"'},"​")],-1)),t[31]||(t[31]=n("p",null,[l("This guide gives a quick and easy functional walkthrough of QuickRetro app."),n("br"),l(" To start, visit the site and type in a name to join as guest. There is no signup/login process.")],-1)),n("h3",p,[t[0]||(t[0]=l("Latest version ")),o(e,{type:"tip",text:"v1.5.0"}),t[1]||(t[1]=l()),t[2]||(t[2]=n("a",{class:"header-anchor",href:"#latest-version","aria-label":'Permalink to "Latest version "'},"​",-1))]),t[32]||(t[32]=d('

Try the Demo

Try out the live demo. It is recommended to self-host.

NOTE

The name you enter initially is saved in your browser cache. It will be auto-filled the next time you visit. You can change it if needed.

WARNING

All data in demo site is deleted in 2 hours.

Supported Languages

',5)),n("p",null,[t[3]||(t[3]=l("English")),t[4]||(t[4]=n("br",null,null,-1)),t[5]||(t[5]=l(" 简体中文 (zh-CN) ")),o(e,{type:"info",text:"No PDF download. Only Print option."}),t[6]||(t[6]=n("br",null,null,-1)),t[7]||(t[7]=l(" Español")),t[8]||(t[8]=n("br",null,null,-1)),t[9]||(t[9]=l(" Deutsch")),t[10]||(t[10]=n("br",null,null,-1)),t[11]||(t[11]=l(" Français")),t[12]||(t[12]=n("br",null,null,-1)),t[13]||(t[13]=l(" Português (Brasil)")),t[14]||(t[14]=n("br",null,null,-1)),t[15]||(t[15]=l(" Русский (ru) ")),o(e,{type:"info",text:"No PDF download. Only Print option."}),t[16]||(t[16]=n("br",null,null,-1)),t[17]||(t[17]=l(" 日本語 (ja) ")),o(e,{type:"info",text:"No PDF download. Only Print option."}),t[18]||(t[18]=n("br",null,null,-1)),t[19]||(t[19]=l(" Português")),t[20]||(t[20]=n("br",null,null,-1)),t[21]||(t[21]=l(" Nederlands")),t[22]||(t[22]=n("br",null,null,-1)),t[23]||(t[23]=l(" 한국어 (ko) ")),o(e,{type:"info",text:"No PDF download. Only Print option."}),t[24]||(t[24]=n("br",null,null,-1)),t[25]||(t[25]=l(" Українська (uk) ")),o(e,{type:"info",text:"No PDF download. Only Print option."}),t[26]||(t[26]=n("br",null,null,-1)),t[27]||(t[27]=l(" Italiano")),t[28]||(t[28]=n("br",null,null,-1)),t[29]||(t[29]=l(" Français (Canada)"))])])}const x=r(u,[["render",g]]);export{v as __pageData,x as default}; 2 | -------------------------------------------------------------------------------- /homepage/assets/guide_getting-started.md.Clt8uJRh.lean.js: -------------------------------------------------------------------------------- 1 | import{_ as r,C as i,c as s,o as a,j as n,ae as d,a as l,G as o}from"./chunks/framework.CTVYQtO4.js";const v=JSON.parse('{"title":"Getting Started","description":"","frontmatter":{},"headers":[],"relativePath":"guide/getting-started.md","filePath":"guide/getting-started.md","lastUpdated":1743150417000}'),u={name:"guide/getting-started.md"},p={id:"latest-version",tabindex:"-1"};function g(m,t,b,f,y,P){const e=i("Badge");return a(),s("div",null,[t[30]||(t[30]=n("h1",{id:"getting-started",tabindex:"-1"},[l("Getting Started "),n("a",{class:"header-anchor",href:"#getting-started","aria-label":'Permalink to "Getting Started"'},"​")],-1)),t[31]||(t[31]=n("p",null,[l("This guide gives a quick and easy functional walkthrough of QuickRetro app."),n("br"),l(" To start, visit the site and type in a name to join as guest. There is no signup/login process.")],-1)),n("h3",p,[t[0]||(t[0]=l("Latest version ")),o(e,{type:"tip",text:"v1.5.0"}),t[1]||(t[1]=l()),t[2]||(t[2]=n("a",{class:"header-anchor",href:"#latest-version","aria-label":'Permalink to "Latest version "'},"​",-1))]),t[32]||(t[32]=d("",5)),n("p",null,[t[3]||(t[3]=l("English")),t[4]||(t[4]=n("br",null,null,-1)),t[5]||(t[5]=l(" 简体中文 (zh-CN) ")),o(e,{type:"info",text:"No PDF download. Only Print option."}),t[6]||(t[6]=n("br",null,null,-1)),t[7]||(t[7]=l(" Español")),t[8]||(t[8]=n("br",null,null,-1)),t[9]||(t[9]=l(" Deutsch")),t[10]||(t[10]=n("br",null,null,-1)),t[11]||(t[11]=l(" Français")),t[12]||(t[12]=n("br",null,null,-1)),t[13]||(t[13]=l(" Português (Brasil)")),t[14]||(t[14]=n("br",null,null,-1)),t[15]||(t[15]=l(" Русский (ru) ")),o(e,{type:"info",text:"No PDF download. Only Print option."}),t[16]||(t[16]=n("br",null,null,-1)),t[17]||(t[17]=l(" 日本語 (ja) ")),o(e,{type:"info",text:"No PDF download. Only Print option."}),t[18]||(t[18]=n("br",null,null,-1)),t[19]||(t[19]=l(" Português")),t[20]||(t[20]=n("br",null,null,-1)),t[21]||(t[21]=l(" Nederlands")),t[22]||(t[22]=n("br",null,null,-1)),t[23]||(t[23]=l(" 한국어 (ko) ")),o(e,{type:"info",text:"No PDF download. Only Print option."}),t[24]||(t[24]=n("br",null,null,-1)),t[25]||(t[25]=l(" Українська (uk) ")),o(e,{type:"info",text:"No PDF download. Only Print option."}),t[26]||(t[26]=n("br",null,null,-1)),t[27]||(t[27]=l(" Italiano")),t[28]||(t[28]=n("br",null,null,-1)),t[29]||(t[29]=l(" Français (Canada)"))])])}const x=r(u,[["render",g]]);export{v as __pageData,x as default}; 2 | -------------------------------------------------------------------------------- /homepage/assets/guide_self-hosting.md.D4ovObpY.js: -------------------------------------------------------------------------------- 1 | import{_ as s,c as i,o as a,ae as o}from"./chunks/framework.CTVYQtO4.js";const u=JSON.parse('{"title":"Self-Hosting","description":"","frontmatter":{},"headers":[],"relativePath":"guide/self-hosting.md","filePath":"guide/self-hosting.md","lastUpdated":1743150417000}'),t={name:"guide/self-hosting.md"};function r(n,e,l,c,d,p){return a(),i("div",null,e[0]||(e[0]=[o(`

Self-Hosting

Although the demo app has all the features and can be used as-is, it runs on low resources. The data is auto-deleted within 2 hours. It is recommended to self-host the app for better flexibility.

Update Allowed-Origins

As defined in Configurations, update the config setting with your site origin.

Secure Redis Instance

It is recommended to secure your Redis instance, preferably with ACL enabled. Check out the redis directory, and sample docker compose files compose.yml, compose.reverseproxy.yml, compose.demohosting.yml etc in github repository for more details.

Passing ENV variables with Compose

Environment variables are passed using .env file which is present in the same directory as compose*.yml files.
Example: Create an env file with your values -

sh
echo "REDIS_CONNSTR=redis://redis:6379/0" > .env
2 | # echo "MY_VAR1=false" >> .env
3 | # echo "MY_VAR2=true" >> .env

INFO

To securely pass ENV vars, feel free to use an approach which suits you best.

NOTE

DO NOT create the file directly from Windows CMD if you intend to run the app in Linux. It creates Unicode text, UTF-16, little-endian text, with CRLF line terminators. This causes problems for Docker Compose to read the env file.

On Windows, you can create the file in UTF-8 using Git Terminal.

Sample Compose files

Check out the sample docker compose files compose.yml, compose.reverseproxy.yml, compose.demohosting.yml etc in github repository for more details.

`,13)]))}const m=s(t,[["render",r]]);export{u as __pageData,m as default}; 4 | -------------------------------------------------------------------------------- /homepage/assets/guide_self-hosting.md.D4ovObpY.lean.js: -------------------------------------------------------------------------------- 1 | import{_ as s,c as i,o as a,ae as o}from"./chunks/framework.CTVYQtO4.js";const u=JSON.parse('{"title":"Self-Hosting","description":"","frontmatter":{},"headers":[],"relativePath":"guide/self-hosting.md","filePath":"guide/self-hosting.md","lastUpdated":1743150417000}'),t={name:"guide/self-hosting.md"};function r(n,e,l,c,d,p){return a(),i("div",null,e[0]||(e[0]=[o("",13)]))}const m=s(t,[["render",r]]);export{u as __pageData,m as default}; 2 | -------------------------------------------------------------------------------- /homepage/assets/index.md.6AvOtPGe.js: -------------------------------------------------------------------------------- 1 | import{_ as e,c as t,o as i}from"./chunks/framework.CTVYQtO4.js";const u=JSON.parse(`{"title":"QuickRetro - Free and Open-Source Sprint Retrospective Meeting App","description":"","frontmatter":{"layout":"home","title":"QuickRetro - Free and Open-Source Sprint Retrospective Meeting App","hero":{"name":"QuickRetro","text":"Sprint Retrospective Meeting App for Remote Agile Teams","tagline":"Free, Open-Source & Self-hosted","actions":[{"theme":"brand","text":"Live Demo","link":"https://demo.quickretro.app"},{"theme":"alt","text":"Getting Started","link":"/guide/getting-started"}],"image":{"light":"/logo_large_light.png","dark":"/logo_large_dark.png","alt":"QuickRetro"}},"features":[{"title":"No Signups","details":"That's right! No need to signup or login"},{"title":"No Board Limits","details":"Create Boards or Invite Users without limits"},{"title":"Mobile Friendly UI","details":"Easily participate from your mobile phone"},{"title":"Customize Column Names","details":"Choose upto 5 columns with any name"},{"title":"Mask/Blur messages","details":"Avoid revealing messages of other participants"},{"title":"Anonymous Messages","details":"Post messages without revealing your name"},{"title":"Download as PDF","details":"Download messages as PDF"},{"title":"Countdown Timer","details":"Stopwatch with max 1 hour limit"},{"title":"Board Lock","details":"Lock to stop addition/updation of messages"},{"title":"Dark Theme","details":"Easily switch to use a Dark theme"},{"title":"Focussed View","details":"Highlight cards just for a User at a time"},{"title":"Smart CAPTCHA Integration","details":"Built-in integration with Cloudflare Turnstile"},{"title":"Online Presence Display","details":"See participants present in the meeting"},{"title":"Auto-Delete data","details":"Auto-delete data with configurable retention duration"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":1748342669000}`),a={name:"index.md"};function o(s,r,n,l,d,p){return i(),t("div")}const c=e(a,[["render",o]]);export{u as __pageData,c as default}; 2 | -------------------------------------------------------------------------------- /homepage/assets/index.md.6AvOtPGe.lean.js: -------------------------------------------------------------------------------- 1 | import{_ as e,c as t,o as i}from"./chunks/framework.CTVYQtO4.js";const u=JSON.parse(`{"title":"QuickRetro - Free and Open-Source Sprint Retrospective Meeting App","description":"","frontmatter":{"layout":"home","title":"QuickRetro - Free and Open-Source Sprint Retrospective Meeting App","hero":{"name":"QuickRetro","text":"Sprint Retrospective Meeting App for Remote Agile Teams","tagline":"Free, Open-Source & Self-hosted","actions":[{"theme":"brand","text":"Live Demo","link":"https://demo.quickretro.app"},{"theme":"alt","text":"Getting Started","link":"/guide/getting-started"}],"image":{"light":"/logo_large_light.png","dark":"/logo_large_dark.png","alt":"QuickRetro"}},"features":[{"title":"No Signups","details":"That's right! No need to signup or login"},{"title":"No Board Limits","details":"Create Boards or Invite Users without limits"},{"title":"Mobile Friendly UI","details":"Easily participate from your mobile phone"},{"title":"Customize Column Names","details":"Choose upto 5 columns with any name"},{"title":"Mask/Blur messages","details":"Avoid revealing messages of other participants"},{"title":"Anonymous Messages","details":"Post messages without revealing your name"},{"title":"Download as PDF","details":"Download messages as PDF"},{"title":"Countdown Timer","details":"Stopwatch with max 1 hour limit"},{"title":"Board Lock","details":"Lock to stop addition/updation of messages"},{"title":"Dark Theme","details":"Easily switch to use a Dark theme"},{"title":"Focussed View","details":"Highlight cards just for a User at a time"},{"title":"Smart CAPTCHA Integration","details":"Built-in integration with Cloudflare Turnstile"},{"title":"Online Presence Display","details":"See participants present in the meeting"},{"title":"Auto-Delete data","details":"Auto-delete data with configurable retention duration"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":1748342669000}`),a={name:"index.md"};function o(s,r,n,l,d,p){return i(),t("div")}const c=e(a,[["render",o]]);export{u as __pageData,c as default}; 2 | -------------------------------------------------------------------------------- /homepage/assets/index.md.CU58q19j.js: -------------------------------------------------------------------------------- 1 | import{_ as e,c as t,o as i}from"./chunks/framework.CTVYQtO4.js";const u=JSON.parse(`{"title":"QuickRetro - Free and Open-Source Sprint Retrospective Meeting App","description":"","frontmatter":{"layout":"home","title":"QuickRetro - Free and Open-Source Sprint Retrospective Meeting App","hero":{"name":"QuickRetro","text":"Sprint Retrospective Meeting App for Remote Agile Teams","tagline":"Free, Open-Source & Self-hosted","actions":[{"theme":"brand","text":"Live Demo","link":"https://demo.quickretro.app"},{"theme":"alt","text":"Getting Started","link":"/guide/getting-started"}],"image":{"light":"/logo_large_light.png","dark":"/logo_large_dark.png","alt":"QuickRetro"}},"features":[{"title":"No Signups","details":"That's right! No need to signup or login"},{"title":"No Board Limits","details":"Create Boards or Invite Users without limits"},{"title":"Mobile Friendly UI","details":"Easily participate from your mobile phone"},{"title":"Customize Column Names","details":"Choose upto 5 columns with any name"},{"title":"Mask/Blur messages","details":"Avoid revealing messages of other participants"},{"title":"Anonymous Messages","details":"Post messages without revealing your name"},{"title":"Download as PDF","details":"Download messages as PDF"},{"title":"Countdown Timer","details":"Stopwatch with max 1 hour limit"},{"title":"Board Lock","details":"Lock to stop addition/updation of messages"},{"title":"Dark Theme","details":"Easily switch to use a Dark theme"},{"title":"Focussed View","details":"Highlight cards just for a User at a time"},{"title":"Smart CAPTCHA Integration","details":"Built-in integration with Cloudflare Turnstile"},{"title":"Online Presence Display","details":"See participants present in the meeting"},{"title":"Auto-Delete data","details":"Auto-delete data with configurable retention duration"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":1743150417000}`),a={name:"index.md"};function o(s,r,n,l,d,p){return i(),t("div")}const c=e(a,[["render",o]]);export{u as __pageData,c as default}; 2 | -------------------------------------------------------------------------------- /homepage/assets/index.md.CU58q19j.lean.js: -------------------------------------------------------------------------------- 1 | import{_ as e,c as t,o as i}from"./chunks/framework.CTVYQtO4.js";const u=JSON.parse(`{"title":"QuickRetro - Free and Open-Source Sprint Retrospective Meeting App","description":"","frontmatter":{"layout":"home","title":"QuickRetro - Free and Open-Source Sprint Retrospective Meeting App","hero":{"name":"QuickRetro","text":"Sprint Retrospective Meeting App for Remote Agile Teams","tagline":"Free, Open-Source & Self-hosted","actions":[{"theme":"brand","text":"Live Demo","link":"https://demo.quickretro.app"},{"theme":"alt","text":"Getting Started","link":"/guide/getting-started"}],"image":{"light":"/logo_large_light.png","dark":"/logo_large_dark.png","alt":"QuickRetro"}},"features":[{"title":"No Signups","details":"That's right! No need to signup or login"},{"title":"No Board Limits","details":"Create Boards or Invite Users without limits"},{"title":"Mobile Friendly UI","details":"Easily participate from your mobile phone"},{"title":"Customize Column Names","details":"Choose upto 5 columns with any name"},{"title":"Mask/Blur messages","details":"Avoid revealing messages of other participants"},{"title":"Anonymous Messages","details":"Post messages without revealing your name"},{"title":"Download as PDF","details":"Download messages as PDF"},{"title":"Countdown Timer","details":"Stopwatch with max 1 hour limit"},{"title":"Board Lock","details":"Lock to stop addition/updation of messages"},{"title":"Dark Theme","details":"Easily switch to use a Dark theme"},{"title":"Focussed View","details":"Highlight cards just for a User at a time"},{"title":"Smart CAPTCHA Integration","details":"Built-in integration with Cloudflare Turnstile"},{"title":"Online Presence Display","details":"See participants present in the meeting"},{"title":"Auto-Delete data","details":"Auto-delete data with configurable retention duration"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":1743150417000}`),a={name:"index.md"};function o(s,r,n,l,d,p){return i(),t("div")}const c=e(a,[["render",o]]);export{u as __pageData,c as default}; 2 | -------------------------------------------------------------------------------- /homepage/assets/index.md.DJqEP-JG.js: -------------------------------------------------------------------------------- 1 | import{_ as e,c as t,o as i}from"./chunks/framework.CTVYQtO4.js";const c=JSON.parse(`{"title":"QuickRetro - Free and Open-Source Sprint Retrospective App","description":"","frontmatter":{"layout":"home","title":"QuickRetro - Free and Open-Source Sprint Retrospective App","hero":{"name":"QuickRetro","text":"Self-hosted, Free & Open-source","tagline":"Easily conduct a Sprint retrospective meeting remotely","actions":[{"theme":"brand","text":"Live Demo","link":"https://demo.quickretro.app"},{"theme":"alt","text":"Getting Started","link":"/guide/getting-started"}],"image":{"light":"/logo_large_light.png","dark":"/logo_large_dark.png","alt":"QuickRetro"}},"features":[{"title":"No Signups","details":"That's right! No need to signup or login"},{"title":"No Board Limits","details":"Create Boards or Invite Users without limits"},{"title":"Mobile Friendly UI","details":"Easily participate from your mobile phone"},{"title":"Customize Column Names","details":"Choose upto 5 columns with any name"},{"title":"Mask/Blur messages","details":"Avoid revealing messages of other participants"},{"title":"Anonymous Messages","details":"Post messages without revealing your name"},{"title":"Download as PDF","details":"Download messages as PDF"},{"title":"Countdown Timer","details":"Stopwatch with max 1 hour limit"},{"title":"Board Lock","details":"Lock to stop addition/updation of messages"},{"title":"Dark Theme","details":"Easily switch to use a Dark theme"},{"title":"Focussed View","details":"Highlight cards just for a User at a time"},{"title":"Smart CAPTCHA Integration","details":"Built-in integration with Cloudflare Turnstile"},{"title":"Online Presence Display","details":"See participants present in the meeting"},{"title":"Auto-Delete data","details":"Auto-delete data with configurable retention duration"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":1743150417000}`),a={name:"index.md"};function o(s,r,n,l,d,p){return i(),t("div")}const u=e(a,[["render",o]]);export{c as __pageData,u as default}; 2 | -------------------------------------------------------------------------------- /homepage/assets/index.md.DJqEP-JG.lean.js: -------------------------------------------------------------------------------- 1 | import{_ as e,c as t,o as i}from"./chunks/framework.CTVYQtO4.js";const c=JSON.parse(`{"title":"QuickRetro - Free and Open-Source Sprint Retrospective App","description":"","frontmatter":{"layout":"home","title":"QuickRetro - Free and Open-Source Sprint Retrospective App","hero":{"name":"QuickRetro","text":"Self-hosted, Free & Open-source","tagline":"Easily conduct a Sprint retrospective meeting remotely","actions":[{"theme":"brand","text":"Live Demo","link":"https://demo.quickretro.app"},{"theme":"alt","text":"Getting Started","link":"/guide/getting-started"}],"image":{"light":"/logo_large_light.png","dark":"/logo_large_dark.png","alt":"QuickRetro"}},"features":[{"title":"No Signups","details":"That's right! No need to signup or login"},{"title":"No Board Limits","details":"Create Boards or Invite Users without limits"},{"title":"Mobile Friendly UI","details":"Easily participate from your mobile phone"},{"title":"Customize Column Names","details":"Choose upto 5 columns with any name"},{"title":"Mask/Blur messages","details":"Avoid revealing messages of other participants"},{"title":"Anonymous Messages","details":"Post messages without revealing your name"},{"title":"Download as PDF","details":"Download messages as PDF"},{"title":"Countdown Timer","details":"Stopwatch with max 1 hour limit"},{"title":"Board Lock","details":"Lock to stop addition/updation of messages"},{"title":"Dark Theme","details":"Easily switch to use a Dark theme"},{"title":"Focussed View","details":"Highlight cards just for a User at a time"},{"title":"Smart CAPTCHA Integration","details":"Built-in integration with Cloudflare Turnstile"},{"title":"Online Presence Display","details":"See participants present in the meeting"},{"title":"Auto-Delete data","details":"Auto-delete data with configurable retention duration"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":1743150417000}`),a={name:"index.md"};function o(s,r,n,l,d,p){return i(),t("div")}const u=e(a,[["render",o]]);export{c as __pageData,u as default}; 2 | -------------------------------------------------------------------------------- /homepage/assets/inter-italic-cyrillic-ext.r48I6akx.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 -------------------------------------------------------------------------------- /homepage/assets/inter-italic-cyrillic.By2_1cv3.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/assets/inter-italic-cyrillic.By2_1cv3.woff2 -------------------------------------------------------------------------------- /homepage/assets/inter-italic-greek-ext.1u6EdAuj.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/assets/inter-italic-greek-ext.1u6EdAuj.woff2 -------------------------------------------------------------------------------- /homepage/assets/inter-italic-greek.DJ8dCoTZ.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/assets/inter-italic-greek.DJ8dCoTZ.woff2 -------------------------------------------------------------------------------- /homepage/assets/inter-italic-latin-ext.CN1xVJS-.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/assets/inter-italic-latin-ext.CN1xVJS-.woff2 -------------------------------------------------------------------------------- /homepage/assets/inter-italic-latin.C2AdPX0b.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/assets/inter-italic-latin.C2AdPX0b.woff2 -------------------------------------------------------------------------------- /homepage/assets/inter-italic-vietnamese.BSbpV94h.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/assets/inter-italic-vietnamese.BSbpV94h.woff2 -------------------------------------------------------------------------------- /homepage/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 -------------------------------------------------------------------------------- /homepage/assets/inter-roman-cyrillic.C5lxZ8CY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 -------------------------------------------------------------------------------- /homepage/assets/inter-roman-greek-ext.CqjqNYQ-.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 -------------------------------------------------------------------------------- /homepage/assets/inter-roman-greek.BBVDIX6e.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/assets/inter-roman-greek.BBVDIX6e.woff2 -------------------------------------------------------------------------------- /homepage/assets/inter-roman-latin-ext.4ZJIpNVo.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 -------------------------------------------------------------------------------- /homepage/assets/inter-roman-latin.Di8DUHzh.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/assets/inter-roman-latin.Di8DUHzh.woff2 -------------------------------------------------------------------------------- /homepage/assets/inter-roman-vietnamese.BjW4sHH5.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/assets/inter-roman-vietnamese.BjW4sHH5.woff2 -------------------------------------------------------------------------------- /homepage/createboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/createboard.png -------------------------------------------------------------------------------- /homepage/createboard_turnstile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/createboard_turnstile.png -------------------------------------------------------------------------------- /homepage/dashboard_add_cards.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/dashboard_add_cards.png -------------------------------------------------------------------------------- /homepage/dashboard_focus_panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/dashboard_focus_panel.png -------------------------------------------------------------------------------- /homepage/dashboard_guest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/dashboard_guest.png -------------------------------------------------------------------------------- /homepage/dashboard_move.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/dashboard_move.png -------------------------------------------------------------------------------- /homepage/dashboard_owner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/dashboard_owner.png -------------------------------------------------------------------------------- /homepage/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/favicon.ico -------------------------------------------------------------------------------- /homepage/hashmap.json: -------------------------------------------------------------------------------- 1 | {"guide_configurations.md":"CT2ZnaGS","guide_create-board.md":"Dg_d45NA","guide_dashboard.md":"DNu-gDWb","guide_development.md":"Ds4A4Cqt","guide_getting-started.md":"CP0vWESL","guide_self-hosting.md":"D4ovObpY","index.md":"6AvOtPGe"} 2 | -------------------------------------------------------------------------------- /homepage/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/logo.png -------------------------------------------------------------------------------- /homepage/logo_large_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/logo_large_dark.png -------------------------------------------------------------------------------- /homepage/logo_large_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/logo_large_light.png -------------------------------------------------------------------------------- /homepage/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | 4 | Sitemap: https://quickretro.app/sitemap.xml -------------------------------------------------------------------------------- /homepage/sitemap.xml: -------------------------------------------------------------------------------- 1 | https://quickretro.app/guide/configurations2025-03-28T08:26:57.000Z0.5https://quickretro.app/guide/create-board2025-03-28T08:26:57.000Z0.9https://quickretro.app/createboard.pngCreate boardCreate boardhttps://quickretro.app/guide/dashboard2025-05-27T10:44:29.000Z1.0https://quickretro.app/dashboard_owner.pngDashboard featuresDashboard featureshttps://quickretro.app/guide/development2025-03-28T08:26:57.000Z0.9https://quickretro.app/guide/getting-started2025-03-28T08:26:57.000Z0.9https://quickretro.app/guide/self-hosting2025-03-28T08:26:57.000Z0.5https://quickretro.app/2025-05-27T10:44:29.000Z1.0https://quickretro.app/logo.pngFree and Open-Source Sprint Retrospective AppQuickRetro | Free and Open-Source Sprint Retrospective Meeting Apphttps://demo.quickretro.app/1.0 -------------------------------------------------------------------------------- /homepage/videos/add-update-message.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/videos/add-update-message.mp4 -------------------------------------------------------------------------------- /homepage/videos/create-board.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/videos/create-board.mp4 -------------------------------------------------------------------------------- /homepage/videos/start-stop-timer.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage/videos/start-stop-timer.mp4 -------------------------------------------------------------------------------- /homepage/vp-icons.css: -------------------------------------------------------------------------------- 1 | .vpi-social-github{--icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E")} -------------------------------------------------------------------------------- /homepage_deprecated/images/addmessage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage_deprecated/images/addmessage.png -------------------------------------------------------------------------------- /homepage_deprecated/images/createboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage_deprecated/images/createboard.png -------------------------------------------------------------------------------- /homepage_deprecated/images/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage_deprecated/images/dashboard.png -------------------------------------------------------------------------------- /homepage_deprecated/images/owneractions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage_deprecated/images/owneractions.png -------------------------------------------------------------------------------- /homepage_deprecated/images/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vijeeshr/quickretro/ea439e3344c6fa86ead63d2ae8fdf44ee1372ef1/homepage_deprecated/images/share.png -------------------------------------------------------------------------------- /onlybackend.Dockerfile: -------------------------------------------------------------------------------- 1 | # docker build -f onlybackend.Dockerfile -t quickretro-app . 2 | 3 | FROM golang:1.24.2-alpine AS builder 4 | WORKDIR /app 5 | COPY src/go.mod src/go.sum ./ 6 | RUN go mod download 7 | # COPY src/ . 8 | COPY src/*.go . 9 | COPY src/frontend/dist frontend/dist 10 | COPY src/config.toml . 11 | RUN CGO_ENABLED=0 GOOS=linux go build -o retroapp . 12 | 13 | FROM alpine:latest AS certificates 14 | RUN apk --no-cache add ca-certificates 15 | 16 | FROM scratch 17 | WORKDIR /app 18 | COPY --from=certificates /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 19 | COPY --from=builder /app/retroapp . 20 | COPY --from=builder /app/config.toml . 21 | EXPOSE 8080 22 | CMD ["./retroapp"] -------------------------------------------------------------------------------- /onlybackend.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules -------------------------------------------------------------------------------- /redis/users.acl: -------------------------------------------------------------------------------- 1 | user default off 2 | user admin on >mysecretadminpassword ~* &* +@all 3 | user app-user on >mysecretpassword ~* &* +@read +@write +@pubsub -@dangerous -FLUSHDB -CONFIG +PING -------------------------------------------------------------------------------- /src/config.toml: -------------------------------------------------------------------------------- 1 | [server] 2 | # When self-hosting, add your domain to allowed_origins list. 3 | # For e.g. if you are hosting your site at https://example.com, allowed_origins will look like - 4 | # allowed_origins = [ 5 | # "https://example.com" 6 | # ] 7 | allowed_origins = [ 8 | "http://localhost:8080", 9 | "https://localhost:8080", 10 | "http://localhost:5173", 11 | "https://localhost", 12 | "https://quickretro.app", 13 | "https://demo.quickretro.app" 14 | ] 15 | turnstile_site_verify_url = "https://challenges.cloudflare.com/turnstile/v0/siteverify" 16 | 17 | [websocket] 18 | # Maximum message size (in bytes) allowed from peer for the websocket connection 19 | # For the front-end validation, keep the same value in (src/frontend/.env [VITE_MAX_WEBSOCKET_MESSAGE_SIZE_BYTES]) 20 | max_message_size_bytes = 1024 21 | 22 | [data] 23 | # Format: 24 | # Units: s=seconds, m=minutes, h=hours, d=days 25 | # Examples: "50s" for 50 seconds, "5m" for 5 minutes, "2h" for 2 hours, "7d" for 7 days 26 | auto_delete_duration = "2h" -------------------------------------------------------------------------------- /src/event.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log/slog" 6 | ) 7 | 8 | type Event struct { 9 | Type string `json:"typ"` // Values can be one of "reg", "msg", "del", "like", "mask", "timer", "catchng". "closing" is not initiated from UI. 10 | Payload json.RawMessage `json:"pyl"` 11 | } 12 | 13 | // Handle event 14 | func (e *Event) Handle(h *Hub) { 15 | payload := e.ParsePayload() 16 | if payload == nil { 17 | return 18 | } 19 | // Call individual handlers 20 | switch e.Type { 21 | case "mask": 22 | payload.(*MaskEvent).Handle(e, h) 23 | case "lock": 24 | payload.(*LockEvent).Handle(e, h) 25 | case "reg": 26 | payload.(*RegisterEvent).Handle(e, h) 27 | case "msg": 28 | payload.(*MessageEvent).Handle(e, h) 29 | case "like": 30 | payload.(*LikeMessageEvent).Handle(e, h) 31 | case "del": 32 | payload.(*DeleteMessageEvent).Handle(e, h) 33 | case "catchng": 34 | payload.(*CategoryChangeEvent).Handle(e, h) 35 | case "timer": 36 | payload.(*TimerEvent).Handle(e, h) 37 | } 38 | } 39 | 40 | // Broadcast event. This is executed when Redis pubsub sends message/data. Hub gets the message first, which is forwarded here. 41 | func (e *Event) Broadcast(m *Message, h *Hub) { 42 | payload := e.ParsePayload() 43 | if payload == nil { 44 | return 45 | } 46 | // Call individual broadcasters 47 | switch e.Type { 48 | case "mask": 49 | payload.(*MaskEvent).Broadcast(h) 50 | case "lock": 51 | payload.(*LockEvent).Broadcast(h) 52 | case "reg": 53 | payload.(*RegisterEvent).Broadcast(h) 54 | case "msg": 55 | payload.(*MessageEvent).Broadcast(m, h) 56 | case "like": 57 | payload.(*LikeMessageEvent).Broadcast(m, h) 58 | case "del": 59 | payload.(*DeleteMessageEvent).Broadcast(m, h) 60 | case "catchng": 61 | payload.(*CategoryChangeEvent).Broadcast(h) 62 | case "timer": 63 | payload.(*TimerEvent).Broadcast(h) 64 | case "closing": 65 | payload.(*UserClosingEvent).Broadcast(h) 66 | } 67 | } 68 | 69 | func (e *Event) ParsePayload() interface{} { 70 | // Todo: Check allocations. 71 | payloadMap := map[string]interface{}{ 72 | "mask": &MaskEvent{}, 73 | "lock": &LockEvent{}, 74 | "reg": &RegisterEvent{}, 75 | "msg": &MessageEvent{}, 76 | "like": &LikeMessageEvent{}, 77 | "del": &DeleteMessageEvent{}, 78 | "catchng": &CategoryChangeEvent{}, 79 | "timer": &TimerEvent{}, 80 | "closing": &UserClosingEvent{}, 81 | } 82 | payload, ok := payloadMap[e.Type] 83 | if !ok { 84 | slog.Error("Unsupported command type", "commandType", e.Type) 85 | return nil 86 | } 87 | if err := json.Unmarshal(e.Payload, payload); err != nil { 88 | slog.Error("Error unmarshalling event payload", "details", err.Error()) 89 | return nil 90 | } 91 | slog.Debug("Unmarshalled event payload", "payload", payload) 92 | return payload 93 | } 94 | -------------------------------------------------------------------------------- /src/eventresponses.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type UserDetails struct { 4 | Nickname string `json:"nickname"` 5 | Xid string `json:"xid"` 6 | } 7 | 8 | type RegisterResponse struct { 9 | Type string `json:"typ"` 10 | Mine bool `json:"mine"` 11 | BoardName string `json:"boardName"` 12 | BoardTeam string `json:"boardTeam"` 13 | BoardColumns []*BoardColumn `json:"columns"` // Using same BoardColumn struct that is used for request and redis store. Todo - refactor later. 14 | BoardStatus string `json:"boardStatus"` 15 | BoardMasking bool `json:"boardMasking"` 16 | BoardLock bool `json:"boardLock"` 17 | IsBoardOwner bool `json:"isBoardOwner"` 18 | Users []*UserDetails `json:"users"` 19 | Messages []MessageResponse `json:"messages"` //Todo: Change to *MessageResponse 20 | TimerExpiresInSeconds uint16 `json:"timerExpiresInSeconds"` // uint16 since we are restricting timer to max 1 hour (3600 seconds) 21 | } 22 | 23 | type UserClosingResponse struct { 24 | Type string `json:"typ"` 25 | Users []*UserDetails `json:"users"` 26 | } 27 | 28 | type MaskResponse struct { 29 | Type string `json:"typ"` 30 | Mask bool `json:"mask"` 31 | } 32 | 33 | type LockResponse struct { 34 | Type string `json:"typ"` 35 | Lock bool `json:"lock"` 36 | } 37 | 38 | type MessageResponse struct { 39 | Type string `json:"typ"` 40 | Id string `json:"id"` 41 | ByNickname string `json:"nickname"` 42 | Content string `json:"msg"` 43 | Category string `json:"cat"` 44 | Likes string `json:"likes"` 45 | Liked bool `json:"liked"` // True if receiving user has liked this message. 46 | Mine bool `json:"mine"` 47 | Anonymous bool `json:"anon"` 48 | } 49 | 50 | type LikeMessageResponse struct { 51 | Type string `json:"typ"` 52 | Id string `json:"id"` 53 | Likes string `json:"likes"` 54 | Liked bool `json:"liked"` // True if receiving user has liked this message. 55 | } 56 | 57 | type DeleteMessageResponse struct { 58 | Type string `json:"typ"` 59 | Id string `json:"id"` 60 | } 61 | 62 | type CategoryChangeResponse struct { 63 | Type string `json:"typ"` 64 | MessageId string `json:"id"` 65 | NewCategory string `json:"newcat"` 66 | } 67 | 68 | type TimerResponse struct { 69 | Type string `json:"typ"` 70 | ExpiresInSeconds uint16 `json:"expiresInSeconds"` 71 | } 72 | -------------------------------------------------------------------------------- /src/frontend/.env: -------------------------------------------------------------------------------- 1 | VITE_WS_PROTOCOL=wss 2 | VITE_SHOW_CONSOLE_LOGS=false 3 | # Triggers message size validation. 4 | # It is recommended to keep the same value as what's allowed in backend server (defined in src/config.toml [websocket].max_message_size_bytes). 5 | # To avoid message size validation, comment out below line. However, this will break the server websocket connection when the limit is breached. 6 | VITE_MAX_WEBSOCKET_MESSAGE_SIZE_BYTES=1024 7 | VITE_TURNSTILE_SCRIPT_URL=https://challenges.cloudflare.com/turnstile/v0/api.js -------------------------------------------------------------------------------- /src/frontend/.env.development: -------------------------------------------------------------------------------- 1 | VITE_WS_PROTOCOL=ws 2 | VITE_SHOW_CONSOLE_LOGS=true -------------------------------------------------------------------------------- /src/frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | // add more generic rulesets here, such as: 4 | // 'eslint:recommended', 5 | 'plugin:vue/vue3-recommended', 6 | // 'plugin:vue/recommended' // Use this if you are using Vue.js 2.x. 7 | ], 8 | rules: { 9 | // override/add rules settings here, such as: 10 | // 'vue/no-unused-vars': 'error' 11 | } 12 | } -------------------------------------------------------------------------------- /src/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/frontend/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /src/frontend/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + TypeScript + Vite 2 | 3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` 8 | QuickRetro 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quickretroapp", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc && vite build", 9 | "build-dev": "vue-tsc && vite build --mode development", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@headlessui/vue": "^1.7.19", 14 | "dompurify": "^3.2.4", 15 | "jspdf": "^2.5.1", 16 | "jspdf-autotable": "^3.8.2", 17 | "vue": "^3.3.11", 18 | "vue-i18n": "^11.1.2", 19 | "vue-router": "^4.2.5", 20 | "vue-toast-notification": "^3.1.3" 21 | }, 22 | "devDependencies": { 23 | "@vitejs/plugin-vue": "^4.5.2", 24 | "autoprefixer": "^10.4.17", 25 | "eslint": "^8.56.0", 26 | "eslint-plugin-vue": "^9.20.1", 27 | "postcss": "^8.4.33", 28 | "tailwindcss": "^3.4.1", 29 | "typescript": "^5.2.2", 30 | "vite": "^5.0.8", 31 | "vue-tsc": "^2.2.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/frontend/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { BoardColumn } from "../models/BoardColumn" 2 | 3 | const createBoardUrl = `/api/board/create` 4 | 5 | export interface CreateBoardRequest { 6 | name: string 7 | team: string 8 | owner: string 9 | columns: BoardColumn[] 10 | cfTurnstileResponse: string 11 | } 12 | 13 | export interface CreateBoardResponse { 14 | id: string 15 | } 16 | 17 | export const createBoard = async (payload: CreateBoardRequest): Promise => { 18 | try { 19 | const response = await fetch(createBoardUrl, { 20 | method: "POST", 21 | headers: { 22 | "Content-Type": "application/json", 23 | }, 24 | body: JSON.stringify(payload), 25 | }) 26 | 27 | if (!response.ok) { 28 | throw new Error('Network response was not ok') 29 | } 30 | const data: CreateBoardResponse = await response.json() 31 | if (!data.id) { 32 | throw new Error('Error getting board id from response') 33 | } 34 | 35 | return data 36 | 37 | } catch (error) { 38 | console.error('Error:', error) 39 | throw error // Re-throw the error to maintain the Promise rejection 40 | } 41 | } -------------------------------------------------------------------------------- /src/frontend/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/src/components/Avatar.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | -------------------------------------------------------------------------------- /src/frontend/src/components/Category.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | -------------------------------------------------------------------------------- /src/frontend/src/components/CountdownTimer.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | -------------------------------------------------------------------------------- /src/frontend/src/components/DarkModeToggle.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | -------------------------------------------------------------------------------- /src/frontend/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 32 | 33 | 38 | -------------------------------------------------------------------------------- /src/frontend/src/components/LanguageSelector.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/frontend/src/components/NewAnonymousCard.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /src/frontend/src/components/NewCard.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | -------------------------------------------------------------------------------- /src/frontend/src/components/TurnstileWidget.vue: -------------------------------------------------------------------------------- 1 | 123 | 124 | -------------------------------------------------------------------------------- /src/frontend/src/composables/useLanguage.ts: -------------------------------------------------------------------------------- 1 | import { useI18n } from 'vue-i18n' 2 | import { computed } from 'vue' 3 | 4 | export const availableLocales = ['en', 'zhCN', 'es', 'de', 'fr', 'ptBR', 'ru', 'ja', 'nl', 'ko', 'it', 'pt', 'uk', 'frCA'] as const 5 | 6 | export type AvailableLocales = typeof availableLocales[number] 7 | 8 | export function useLanguage() { 9 | const { locale: i18nLocale, getLocaleMessage } = useI18n() 10 | 11 | const locale = computed({ 12 | get: () => i18nLocale.value as AvailableLocales, 13 | set: (value) => { 14 | setLocale(value) 15 | } 16 | }) 17 | 18 | const setLocale = (newLocale: AvailableLocales) => { 19 | i18nLocale.value = newLocale 20 | try { 21 | localStorage.setItem('lang', newLocale) 22 | } catch (error) { 23 | console.error('Failed to save locale:', error) 24 | } 25 | } 26 | 27 | const languageOptions = computed(() => 28 | availableLocales.map(code => ({ 29 | code, 30 | name: getLocaleMessage(code).langName 31 | })) 32 | ) 33 | 34 | return { 35 | locale, 36 | setLocale, 37 | languageOptions, 38 | getLocaleMessage 39 | } 40 | } -------------------------------------------------------------------------------- /src/frontend/src/composables/useSanitize.ts: -------------------------------------------------------------------------------- 1 | import DOMPurify from 'dompurify'; 2 | 3 | export default () => ({ 4 | sanitize: (dirty: string) => DOMPurify.sanitize(dirty, { 5 | ALLOWED_TAGS: ['b', 'i', 'u', 'em', 'strong', 'br'], 6 | ALLOWED_ATTR: [] 7 | }) 8 | }) -------------------------------------------------------------------------------- /src/frontend/src/i18n/de.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | langName: 'Deutsch', 3 | common: { 4 | anonymous: 'Anonym', 5 | minutes: 'Minuten', 6 | seconds: 'Sekunden', 7 | start: 'Starten', 8 | stop: 'Stoppen', 9 | copy: 'Kopieren', 10 | board: 'Board', 11 | toolTips: { 12 | darkTheme: 'Dunkles Design aktivieren', 13 | lightTheme: 'Helles Design aktivieren' 14 | }, 15 | contentOverloadError: 'Inhalt überschreitet Limit.', 16 | contentStrippingError: 'Inhalt zu lang. Überschüssiger Text wurde entfernt.' 17 | }, 18 | join: { 19 | label: 'Als Gast beitreten', 20 | namePlaceholder: 'Namen hier eingeben!', 21 | nameRequired: 'Bitte Namen eingeben', 22 | button: 'Beitreten' 23 | }, 24 | createBoard: { 25 | label: 'Board erstellen', 26 | namePlaceholder: 'Boardnamen hier eingeben!', 27 | nameRequired: 'Bitte Boardnamen eingeben', 28 | teamNamePlaceholder: 'Teamnamen hier eingeben!', 29 | invalidColumnSelection: 'Bitte Spalte(n) auswählen', 30 | colOnePlaceholder: 'Gut', 31 | button: 'Erstellen', 32 | buttonProgress: 'Wird erstellt..', 33 | captchaInfo: 'Bitte lösen Sie das CAPTCHA, um fortzufahren', 34 | boardCreationError: 'Fehler beim Erstellen des Boards' 35 | }, 36 | dashboard: { 37 | timer: { 38 | oneMinuteLeft: 'Noch eine Minute', 39 | timeCompleted: 'Zeit abgelaufen!', 40 | title: 'Timer Starten/Stoppen', 41 | helpTip: 'Minuten/Sekunden mit +/- oder Pfeiltasten einstellen. Maximal 1 Stunde.', 42 | invalid: 'Ungültige Werte. Erlaubt: 1 Sekunde bis 60 Minuten.', 43 | tooltip: 'Countdown-Timer' 44 | }, 45 | share: { 46 | title: 'URL an Teilnehmer kopieren', 47 | linkCopied: 'Link kopiert!', 48 | linkCopyError: 'Kopieren fehlgeschlagen. Bitte manuell kopieren.', 49 | toolTip: 'Board teilen' 50 | }, 51 | mask: { 52 | maskTooltip: 'Nachrichten verdecken', 53 | unmaskTooltip: 'Nachrichten zeigen' 54 | }, 55 | lock: { 56 | lockTooltip: 'Board sperren', 57 | unlockTooltip: 'Board entsperren', 58 | message: 'Board ist gesperrt.', 59 | discardChanges: 'Board gesperrt! Ungespeicherte Nachrichten verworfen' 60 | }, 61 | spotlight: { 62 | noCardsToFocus: 'Keine Karten vorhanden', 63 | tooltip: 'Karten fokussieren' 64 | }, 65 | download: { 66 | tooltip: 'Als PDF herunterladen' 67 | }, 68 | language: { 69 | tooltip : 'Sprache ändern' 70 | }, 71 | columns: { 72 | col01: 'Was gut lief', 73 | col02: 'Herausforderungen', 74 | col03: 'Aktionspunkte', 75 | col04: 'Dankbarkeiten', 76 | col05: 'Verbesserungen' 77 | }, 78 | pdfFooter: 'Erstellt mit', 79 | offline: 'Offline.' 80 | } 81 | } -------------------------------------------------------------------------------- /src/frontend/src/i18n/en.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | langName: 'English', 3 | common: { 4 | anonymous: 'Anonymous', 5 | minutes: 'Minutes', 6 | seconds: 'Seconds', 7 | start: 'Start', 8 | stop: 'Stop', 9 | copy: 'Copy', 10 | board: 'Board', 11 | toolTips: { 12 | darkTheme: 'Turn on dark theme', 13 | lightTheme: 'Turn on light theme' 14 | }, 15 | contentOverloadError: 'Content more than allowed limit.', 16 | contentStrippingError: 'Content more than allowed limit. Extra text is stripped from the end.' 17 | }, 18 | join: { 19 | label: 'Join as guest', 20 | namePlaceholder: 'Type your name here!', 21 | nameRequired: 'Please enter your name', 22 | button: 'Join' 23 | }, 24 | createBoard: { 25 | label: 'Create Board', 26 | namePlaceholder: 'Type board name here!', 27 | nameRequired: 'Please enter board name', 28 | teamNamePlaceholder: 'Type team name here!', 29 | invalidColumnSelection: 'Please select column(s)', 30 | colOnePlaceholder: 'Good', 31 | button: 'Create', 32 | buttonProgress: 'Creating..', 33 | captchaInfo: 'Please complete the CAPTCHA to continue', 34 | boardCreationError: 'Error when creating board' 35 | }, 36 | dashboard: { 37 | timer: { 38 | oneMinuteLeft: 'One minute left for countdown', 39 | timeCompleted: "Hey! You've run out of time", 40 | title: 'Start/Stop Timer', 41 | helpTip: 'Adjust minutes and seconds using the + and - controls, or the Up and Down arrows on keyboard. Max allowed is 1 hour.', 42 | invalid: 'Please enter valid minutes/seconds values. Allowed range is 1 second to 60 minutes.', 43 | tooltip: 'Countdown Timer' 44 | }, 45 | share: { 46 | title: 'Copy and share below url to participants', 47 | linkCopied: 'Link copied!', 48 | linkCopyError: 'Failed to copy. Please copy directly.', 49 | toolTip: 'Share board with others' 50 | }, 51 | mask: { 52 | maskTooltip: 'Mask messages', 53 | unmaskTooltip: 'Unmask messages' 54 | }, 55 | lock: { 56 | lockTooltip: 'Lock board', 57 | unlockTooltip: 'Unlock board', 58 | message: 'Cannot add or update. Board is locked by owner.', 59 | discardChanges: 'Board locked! Unsaved messages discarded' 60 | }, 61 | spotlight: { 62 | noCardsToFocus: 'There are no cards to focus', 63 | tooltip: 'Focus cards' 64 | }, 65 | download: { 66 | tooltip: 'Download as Pdf' 67 | }, 68 | language: { 69 | tooltip : 'Change language' 70 | }, 71 | columns: { 72 | col01: 'What went well', 73 | col02: 'Challenges', 74 | col03: 'Action Items', 75 | col04: 'Appreciations', 76 | col05: 'Improvements' 77 | }, 78 | pdfFooter: 'Created with', 79 | offline: 'You seem to be offline.' 80 | } 81 | } -------------------------------------------------------------------------------- /src/frontend/src/i18n/es.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | langName: 'Español', 3 | common: { 4 | anonymous: 'Anónimo', 5 | minutes: 'Minutos', 6 | seconds: 'Segundos', 7 | start: 'Iniciar', 8 | stop: 'Detener', 9 | copy: 'Copiar', 10 | board: 'Tablero', 11 | toolTips: { 12 | darkTheme: 'Activar tema oscuro', 13 | lightTheme: 'Activar tema claro' 14 | }, 15 | contentOverloadError: 'Contenido excede el límite permitido.', 16 | contentStrippingError: 'Contenido excede el límite permitido. El texto adicional ha sido eliminado.' 17 | }, 18 | join: { 19 | label: 'Unirse como invitado', 20 | namePlaceholder: '¡Escribe tu nombre aquí!', 21 | nameRequired: 'Por favor ingresa tu nombre', 22 | button: 'Unirse' 23 | }, 24 | createBoard: { 25 | label: 'Crear tablero', 26 | namePlaceholder: '¡Escribe el nombre del tablero aquí!', 27 | nameRequired: 'Por favor ingresa el nombre del tablero', 28 | teamNamePlaceholder: '¡Escribe el nombre del equipo aquí!', 29 | invalidColumnSelection: 'Por favor selecciona columna(s)', 30 | colOnePlaceholder: 'Bien', 31 | button: 'Crear', 32 | buttonProgress: 'Creando..', 33 | captchaInfo: 'Por favor, complete el CAPTCHA para continuar', 34 | boardCreationError: 'Error al crear el tablero' 35 | }, 36 | dashboard: { 37 | timer: { 38 | oneMinuteLeft: 'Queda un minuto', 39 | timeCompleted: '¡Se ha acabado el tiempo!', 40 | title: 'Iniciar/Detener temporizador', 41 | helpTip: 'Ajusta minutos y segundos con los controles + - o flechas del teclado. Máximo: 1 hora.', 42 | invalid: 'Valores inválidos. Rango permitido: 1 segundo a 60 minutos.', 43 | tooltip: 'Temporizador regresivo' 44 | }, 45 | share: { 46 | title: 'Copia y comparte esta URL con participantes', 47 | linkCopied: '¡Enlace copiado!', 48 | linkCopyError: 'Error al copiar. Copia manualmente.', 49 | toolTip: 'Compartir tablero' 50 | }, 51 | mask: { 52 | maskTooltip: 'Ocultar mensajes', 53 | unmaskTooltip: 'Mostrar mensajes' 54 | }, 55 | lock: { 56 | lockTooltip: 'Bloquear tablero', 57 | unlockTooltip: 'Desbloquear tablero', 58 | message: 'Tablero bloqueado por el propietario.', 59 | discardChanges: '¡Tablero bloqueado! Los mensajes no guardados se han descartado' 60 | }, 61 | spotlight: { 62 | noCardsToFocus: 'No hay tarjetas para enfocar', 63 | tooltip: 'Enfocar tarjetas' 64 | }, 65 | download: { 66 | tooltip: 'Descargar PDF' 67 | }, 68 | language: { 69 | tooltip : 'Cambiar idioma' 70 | }, 71 | columns: { 72 | col01: 'Lo que salió bien', 73 | col02: 'Desafíos', 74 | col03: 'Acciones', 75 | col04: 'Agradecimientos', 76 | col05: 'Mejoras' 77 | }, 78 | pdfFooter: 'Creado con', 79 | offline: 'Sin conexión.' 80 | } 81 | } -------------------------------------------------------------------------------- /src/frontend/src/i18n/fr-CA.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | langName: 'Français (Canada)', 3 | common: { 4 | anonymous: 'Anonyme', 5 | minutes: 'Minutes', 6 | seconds: 'Secondes', 7 | start: 'Démarrer', 8 | stop: 'Arrêter', 9 | copy: 'Copier', 10 | board: 'Tableau', 11 | toolTips: { 12 | darkTheme: 'Activer le mode sombre', 13 | lightTheme: 'Activer le mode clair' 14 | }, 15 | contentOverloadError: 'Contenu dépasse la limite permise.', 16 | contentStrippingError: 'Texte excédentaire supprimé.' 17 | }, 18 | join: { 19 | label: 'Joindre comme invité', 20 | namePlaceholder: 'Entrez votre nom ici !', 21 | nameRequired: 'Veuillez entrer votre nom', 22 | button: 'Joindre' 23 | }, 24 | createBoard: { 25 | label: 'Créer un tableau', 26 | namePlaceholder: 'Nom du tableau ici !', 27 | nameRequired: 'Veuillez entrer le nom du tableau', 28 | teamNamePlaceholder: 'Nom de l\'équipe ici !', 29 | invalidColumnSelection: 'Veuillez sélectionner des colonnes', 30 | colOnePlaceholder: 'Bon', 31 | button: 'Créer', 32 | buttonProgress: 'Création en cours..', 33 | captchaInfo: 'Veuillez compléter le CAPTCHA', 34 | boardCreationError: 'Erreur lors de la création du tableau' 35 | }, 36 | dashboard: { 37 | timer: { 38 | oneMinuteLeft: 'Une minute restante', 39 | timeCompleted: 'Le temps est écoulé !', 40 | title: 'Démarrer/Arrêter la minuterie', 41 | helpTip: 'Ajustez avec les boutons + - ou flèches. Maximum 1 heure.', 42 | invalid: 'Valeurs invalides (1 seconde à 60 minutes)', 43 | tooltip: 'Minuterie décompte' 44 | }, 45 | share: { 46 | title: 'Copier et partager le lien', 47 | linkCopied: 'Lien copié !', 48 | linkCopyError: 'Échec de copie. Copiez manuellement.', 49 | toolTip: 'Partager le tableau' 50 | }, 51 | mask: { 52 | maskTooltip: 'Masquer les messages', 53 | unmaskTooltip: 'Afficher les messages' 54 | }, 55 | lock: { 56 | lockTooltip: 'Verrouiller le tableau', 57 | unlockTooltip: 'Déverrouiller le tableau', 58 | message: 'Tableau verrouillé par le propriétaire.', 59 | discardChanges: 'Tableau verrouillé ! Messages non sauvegardés supprimés' 60 | }, 61 | spotlight: { 62 | noCardsToFocus: 'Aucune carte à mettre en évidence', 63 | tooltip: 'Mettre en évidence' 64 | }, 65 | download: { 66 | tooltip: 'Télécharger en PDF' 67 | }, 68 | language: { 69 | tooltip : 'Changer de langue' 70 | }, 71 | columns: { 72 | col01: 'Ce qui a bien fonctionné', 73 | col02: 'Défis', 74 | col03: 'Actions', 75 | col04: 'Reconnaissance', 76 | col05: 'Améliorations' 77 | }, 78 | pdfFooter: 'Créé avec', 79 | offline: 'Hors ligne.' 80 | } 81 | } -------------------------------------------------------------------------------- /src/frontend/src/i18n/fr.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | langName: 'Français', 3 | common: { 4 | anonymous: 'Anonyme', 5 | minutes: 'Minutes', 6 | seconds: 'Secondes', 7 | start: 'Démarrer', 8 | stop: 'Arrêter', 9 | copy: 'Copier', 10 | board: 'Tableau', 11 | toolTips: { 12 | darkTheme: 'Activer le mode sombre', 13 | lightTheme: 'Activer le mode clair' 14 | }, 15 | contentOverloadError: 'Contenu dépasse la limite.', 16 | contentStrippingError: 'Contenu trop long. Texte excédentaire supprimé.' 17 | }, 18 | join: { 19 | label: 'Rejoindre en invité', 20 | namePlaceholder: 'Saisissez votre nom ici !', 21 | nameRequired: 'Veuillez saisir votre nom', 22 | button: 'Rejoindre' 23 | }, 24 | createBoard: { 25 | label: 'Créer un tableau', 26 | namePlaceholder: 'Nom du tableau ici !', 27 | nameRequired: 'Veuillez saisir le nom du tableau', 28 | teamNamePlaceholder: 'Nom de l\'équipe ici !', 29 | invalidColumnSelection: 'Veuillez sélectionner des colonnes', 30 | colOnePlaceholder: 'Bon', 31 | button: 'Créer', 32 | buttonProgress: 'Création en cours..', 33 | captchaInfo: 'Veuillez compléter le CAPTCHA pour continuer', 34 | boardCreationError: 'Erreur lors de la création du tableau' 35 | }, 36 | dashboard: { 37 | timer: { 38 | oneMinuteLeft: 'Une minute restante', 39 | timeCompleted: 'Temps écoulé !', 40 | title: 'Démarrer/Arrêter le chrono', 41 | helpTip: 'Ajustez les minutes/secondes avec +/- ou flèches. Maximum 1 heure.', 42 | invalid: 'Valeurs invalides. Plage autorisée : 1 seconde à 60 minutes.', 43 | tooltip: 'Minuteur' 44 | }, 45 | share: { 46 | title: 'Copier et partager l\'URL', 47 | linkCopied: 'Lien copié !', 48 | linkCopyError: 'Échec de copie. Copiez manuellement.', 49 | toolTip: 'Partager le tableau' 50 | }, 51 | mask: { 52 | maskTooltip: 'Masquer messages', 53 | unmaskTooltip: 'Afficher messages' 54 | }, 55 | lock: { 56 | lockTooltip: 'Verrouiller tableau', 57 | unlockTooltip: 'Déverrouiller tableau', 58 | message: 'Tableau verrouillé.', 59 | discardChanges: 'Tableau verrouillé ! Messages non enregistrés supprimés' 60 | }, 61 | spotlight: { 62 | noCardsToFocus: 'Aucune carte à focaliser', 63 | tooltip: 'Mettre en avant' 64 | }, 65 | download: { 66 | tooltip: 'Télécharger PDF' 67 | }, 68 | language: { 69 | tooltip : 'Changer de langue' 70 | }, 71 | columns: { 72 | col01: 'Ce qui a bien fonctionné', 73 | col02: 'Défis', 74 | col03: 'Actions', 75 | col04: 'Reconnaissance', 76 | col05: 'Améliorations' 77 | }, 78 | pdfFooter: 'Créé avec', 79 | offline: 'Hors ligne.' 80 | } 81 | } -------------------------------------------------------------------------------- /src/frontend/src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | import en from './en' 3 | import es from './es' 4 | import de from './de' 5 | import fr from './fr' 6 | import frCA from './fr-CA' 7 | import ptBR from './pt-BR' 8 | import pt from './pt' 9 | import nl from './nl' 10 | import it from './it' 11 | import zhCN from './zh-CN' 12 | import ja from './ja' 13 | import ko from './ko' 14 | import ru from './ru' 15 | import uk from './uk' 16 | 17 | type MessageSchema = typeof en 18 | 19 | const savedLanguage = localStorage.getItem('lang') 20 | 21 | export default createI18n<[MessageSchema], 'en' | 'zhCN' | 'es' | 'de' | 'fr' | 'ptBR' | 'ru' | 'ja' | 'nl' | 'ko' | 'it' | 'pt' | 'uk' | 'frCA'>({ 22 | legacy: false, 23 | locale: savedLanguage || 'en', 24 | fallbackLocale: 'en', 25 | messages : { 26 | en, zhCN, es, de, fr, ptBR, ru, ja, nl, ko, it, pt, uk, frCA 27 | } 28 | }) 29 | 30 | declare module 'vue-i18n' { 31 | // Define the vue-i18n type schema 32 | export interface DefineLocaleMessage extends MessageSchema {} 33 | } -------------------------------------------------------------------------------- /src/frontend/src/i18n/it.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | langName: 'Italiano', 3 | common: { 4 | anonymous: 'Anonimo', 5 | minutes: 'Minuti', 6 | seconds: 'Secondi', 7 | start: 'Avvia', 8 | stop: 'Ferma', 9 | copy: 'Copia', 10 | board: 'Bacheca', 11 | toolTips: { 12 | darkTheme: 'Attiva tema scuro', 13 | lightTheme: 'Attiva tema chiaro' 14 | }, 15 | contentOverloadError: 'Contenuto oltre il limite consentito.', 16 | contentStrippingError: 'Testo in eccesso rimosso.' 17 | }, 18 | join: { 19 | label: 'Partecipa come ospite', 20 | namePlaceholder: 'Inserisci il tuo nome qui!', 21 | nameRequired: 'Inserisci il tuo nome', 22 | button: 'Unisciti' 23 | }, 24 | createBoard: { 25 | label: 'Crea bacheca', 26 | namePlaceholder: 'Nome della bacheca qui!', 27 | nameRequired: 'Inserisci il nome della bacheca', 28 | teamNamePlaceholder: 'Nome del team qui!', 29 | invalidColumnSelection: 'Seleziona colonna(e)', 30 | colOnePlaceholder: 'Buono', 31 | button: 'Crea', 32 | buttonProgress: 'Creazione..', 33 | captchaInfo: 'Completa il CAPTCHA per continuare', 34 | boardCreationError: 'Errore durante la creazione della bacheca' 35 | }, 36 | dashboard: { 37 | timer: { 38 | oneMinuteLeft: 'Un minuto rimasto', 39 | timeCompleted: 'Tempo scaduto!', 40 | title: 'Avvia/Ferma timer', 41 | helpTip: 'Regola minuti/secondi con + - o frecce. Massimo 1 ora.', 42 | invalid: 'Valori non validi (1 secondo - 60 minuti)', 43 | tooltip: 'Timer conto alla rovescia' 44 | }, 45 | share: { 46 | title: 'Copia e condividi il link', 47 | linkCopied: 'Link copiato!', 48 | linkCopyError: 'Copia fallita. Copia manualmente.', 49 | toolTip: 'Condividi bacheca' 50 | }, 51 | mask: { 52 | maskTooltip: 'Nascondi messaggi', 53 | unmaskTooltip: 'Mostra messaggi' 54 | }, 55 | lock: { 56 | lockTooltip: 'Blocca bacheca', 57 | unlockTooltip: 'Sblocca bacheca', 58 | message: 'Bacheca bloccata.', 59 | discardChanges: 'Bacheca bloccata! Messaggi non salvati eliminati' 60 | }, 61 | spotlight: { 62 | noCardsToFocus: 'Nessuna carta da focalizzare', 63 | tooltip: 'Evidenzia carte' 64 | }, 65 | download: { 66 | tooltip: 'Scarica PDF' 67 | }, 68 | language: { 69 | tooltip : 'Cambia lingua' 70 | }, 71 | columns: { 72 | col01: 'Cosa ha funzionato', 73 | col02: 'Sfide', 74 | col03: 'Azioni', 75 | col04: 'Apprezzamenti', 76 | col05: 'Miglioramenti' 77 | }, 78 | pdfFooter: 'Creato con', 79 | offline: 'Disconnesso.' 80 | } 81 | } -------------------------------------------------------------------------------- /src/frontend/src/i18n/ja.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | langName: '日本語 (ja)', 3 | common: { 4 | anonymous: '匿名', 5 | minutes: '分', 6 | seconds: '秒', 7 | start: '開始', 8 | stop: '停止', 9 | copy: 'コピー', 10 | board: 'ボード', 11 | toolTips: { 12 | darkTheme: 'ダークテーマ', 13 | lightTheme: 'ライトテーマ' 14 | }, 15 | contentOverloadError: 'コンテンツ制限超過', 16 | contentStrippingError: '末尾のテキストが削除されました' 17 | }, 18 | join: { 19 | label: 'ゲスト参加', 20 | namePlaceholder: '名前を入力してください!', 21 | nameRequired: '名前を入力してください', 22 | button: '参加' 23 | }, 24 | createBoard: { 25 | label: 'ボード作成', 26 | namePlaceholder: 'ボード名を入力!', 27 | nameRequired: 'ボード名を入力してください', 28 | teamNamePlaceholder: 'チーム名を入力!', 29 | invalidColumnSelection: '列を選択してください', 30 | colOnePlaceholder: '良い', 31 | button: '作成', 32 | buttonProgress: '作成中..', 33 | captchaInfo: 'CAPTCHAを完了してください', 34 | boardCreationError: 'ボードの作成中にエラーが発生しました' 35 | }, 36 | dashboard: { 37 | timer: { 38 | oneMinuteLeft: '残り1分', 39 | timeCompleted: '時間切れです!', 40 | title: 'タイマー開始/停止', 41 | helpTip: '+/-または矢印キーで調整 最大1時間', 42 | invalid: '無効な時間です(1秒~60分)', 43 | tooltip: 'カウントダウンタイマー' 44 | }, 45 | share: { 46 | title: 'URLを共有', 47 | linkCopied: 'コピーしました!', 48 | linkCopyError: 'コピー失敗 手動でコピーしてください', 49 | toolTip: 'ボードを共有' 50 | }, 51 | mask: { 52 | maskTooltip: 'メッセージを非表示', 53 | unmaskTooltip: 'メッセージを表示' 54 | }, 55 | lock: { 56 | lockTooltip: 'ボードをロック', 57 | unlockTooltip: 'ロック解除', 58 | message: 'ボードがロックされています', 59 | discardChanges: 'ボードがロックされました!保存されていないメッセージは破棄されました' 60 | }, 61 | spotlight: { 62 | noCardsToFocus: 'カードがありません', 63 | tooltip: 'カードをフォーカス' 64 | }, 65 | download: { 66 | tooltip: '印刷' 67 | }, 68 | language: { 69 | tooltip : '言語を変更' 70 | }, 71 | columns: { 72 | col01: '良かった点', 73 | col02: '課題', 74 | col03: 'アクション項目', 75 | col04: '感謝', 76 | col05: '改善点' 77 | }, 78 | pdfFooter: '作成者', 79 | offline: 'オフライン' 80 | } 81 | } -------------------------------------------------------------------------------- /src/frontend/src/i18n/ko.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | langName: '한국어 (ko)', 3 | common: { 4 | anonymous: '익명', 5 | minutes: '분', 6 | seconds: '초', 7 | start: '시작', 8 | stop: '중지', 9 | copy: '복사', 10 | board: '보드', 11 | toolTips: { 12 | darkTheme: '다크 모드 켜기', 13 | lightTheme: '라이트 모드 켜기' 14 | }, 15 | contentOverloadError: '허용된 내용 초과', 16 | contentStrippingError: '초과된 텍스트가 삭제되었습니다' 17 | }, 18 | join: { 19 | label: '게스트로 참여', 20 | namePlaceholder: '이름을 입력하세요!', 21 | nameRequired: '이름을 입력해 주세요', 22 | button: '참여' 23 | }, 24 | createBoard: { 25 | label: '보드 생성', 26 | namePlaceholder: '보드 이름 입력!', 27 | nameRequired: '보드 이름을 입력해 주세요', 28 | teamNamePlaceholder: '팀 이름 입력!', 29 | invalidColumnSelection: '열을 선택해 주세요', 30 | colOnePlaceholder: '좋음', 31 | button: '생성', 32 | buttonProgress: '생성 중..', 33 | captchaInfo: '계속하려면 CAPTCHA를 완료하세요', 34 | boardCreationError: '보드 생성 중 오류 발생' 35 | }, 36 | dashboard: { 37 | timer: { 38 | oneMinuteLeft: '1분 남음', 39 | timeCompleted: '시간 종료!', 40 | title: '타이머 시작/중지', 41 | helpTip: '+ - 또는 방향키로 시간 조절. 최대 1시간.', 42 | invalid: '유효하지 않은 시간 (1초 ~ 60분)', 43 | tooltip: '카운트다운 타이머' 44 | }, 45 | share: { 46 | title: '링크 공유', 47 | linkCopied: '링크 복사됨!', 48 | linkCopyError: '복사 실패. 직접 복사해 주세요.', 49 | toolTip: '보드 공유' 50 | }, 51 | mask: { 52 | maskTooltip: '메시지 숨기기', 53 | unmaskTooltip: '메시지 표시' 54 | }, 55 | lock: { 56 | lockTooltip: '보드 잠금', 57 | unlockTooltip: '잠금 해제', 58 | message: '보드가 잠겨 있습니다', 59 | discardChanges: '보드 잠김! 저장되지 않은 메시지가 삭제되었습니다' 60 | }, 61 | spotlight: { 62 | noCardsToFocus: '포커스할 카드 없음', 63 | tooltip: '카드 강조' 64 | }, 65 | download: { 66 | tooltip: '인쇄' 67 | }, 68 | language: { 69 | tooltip : '언어 변경' 70 | }, 71 | columns: { 72 | col01: '잘된 점', 73 | col02: '어려운 점', 74 | col03: '액션 항목', 75 | col04: '감사한 점', 76 | col05: '개선점' 77 | }, 78 | pdfFooter: '생성 도구', 79 | offline: '오프라인 상태' 80 | } 81 | } -------------------------------------------------------------------------------- /src/frontend/src/i18n/nl.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | langName: 'Nederlands', 3 | common: { 4 | anonymous: 'Anoniem', 5 | minutes: 'Minuten', 6 | seconds: 'Seconden', 7 | start: 'Start', 8 | stop: 'Stop', 9 | copy: 'Kopiëren', 10 | board: 'Bord', 11 | toolTips: { 12 | darkTheme: 'Donker thema inschakelen', 13 | lightTheme: 'Licht thema inschakelen' 14 | }, 15 | contentOverloadError: 'Inhoud overschrijdt limiet.', 16 | contentStrippingError: 'Extra tekst verwijderd.' 17 | }, 18 | join: { 19 | label: 'Als gast deelnemen', 20 | namePlaceholder: 'Vul je naam hier in!', 21 | nameRequired: 'Voer je naam in', 22 | button: 'Deelnemen' 23 | }, 24 | createBoard: { 25 | label: 'Bord aanmaken', 26 | namePlaceholder: 'Bordnaam hier invullen!', 27 | nameRequired: 'Voer bordnaam in', 28 | teamNamePlaceholder: 'Teamnaam hier invullen!', 29 | invalidColumnSelection: 'Selecteer kolom(en)', 30 | colOnePlaceholder: 'Goed', 31 | button: 'Aanmaken', 32 | buttonProgress: 'Aanmaken..', 33 | captchaInfo: 'Voltooi de CAPTCHA om door te gaan', 34 | boardCreationError: 'Fout bij het aanmaken van het bord' 35 | }, 36 | dashboard: { 37 | timer: { 38 | oneMinuteLeft: 'Nog 1 minuut', 39 | timeCompleted: 'Tijd is om!', 40 | title: 'Timer Starten/Stoppen', 41 | helpTip: 'Pas tijd aan met +/- of pijltjes. Maximaal 1 uur.', 42 | invalid: 'Ongeldige tijd (1 seconde - 60 minuten)', 43 | tooltip: 'Countdown-timer' 44 | }, 45 | share: { 46 | title: 'Deel deze link', 47 | linkCopied: 'Link gekopieerd!', 48 | linkCopyError: 'Kopieer handmatig.', 49 | toolTip: 'Bord delen' 50 | }, 51 | mask: { 52 | maskTooltip: 'Berichten verbergen', 53 | unmaskTooltip: 'Berichten tonen' 54 | }, 55 | lock: { 56 | lockTooltip: 'Bord vergrendelen', 57 | unlockTooltip: 'Bord ontgrendelen', 58 | message: 'Bord is vergrendeld.', 59 | discardChanges: 'Board vergrendeld! Niet-opgeslagen berichten verwijderd' 60 | }, 61 | spotlight: { 62 | noCardsToFocus: 'Geen kaarten om te focussen', 63 | tooltip: 'Focus kaarten' 64 | }, 65 | download: { 66 | tooltip: 'Download als PDF' 67 | }, 68 | language: { 69 | tooltip : 'Taal wijzigen' 70 | }, 71 | columns: { 72 | col01: 'Wat ging goed', 73 | col02: 'Uitdagingen', 74 | col03: 'Actiepunten', 75 | col04: 'Waardering', 76 | col05: 'Verbeterpunten' 77 | }, 78 | pdfFooter: 'Gemaakt met', 79 | offline: 'Offline.' 80 | } 81 | } -------------------------------------------------------------------------------- /src/frontend/src/i18n/pt-BR.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | langName: 'Português (Brasil)', 3 | common: { 4 | anonymous: 'Anônimo', 5 | minutes: 'Minutos', 6 | seconds: 'Segundos', 7 | start: 'Iniciar', 8 | stop: 'Parar', 9 | copy: 'Copiar', 10 | board: 'Quadro', 11 | toolTips: { 12 | darkTheme: 'Ativar tema escuro', 13 | lightTheme: 'Ativar tema claro' 14 | }, 15 | contentOverloadError: 'Conteúdo excede o limite permitido.', 16 | contentStrippingError: 'Texto adicional foi removido do final.' 17 | }, 18 | join: { 19 | label: 'Entrar como visitante', 20 | namePlaceholder: 'Digite seu nome aqui!', 21 | nameRequired: 'Por favor, digite seu nome', 22 | button: 'Entrar' 23 | }, 24 | createBoard: { 25 | label: 'Criar quadro', 26 | namePlaceholder: 'Digite o nome do quadro aqui!', 27 | nameRequired: 'Por favor, digite o nome do quadro', 28 | teamNamePlaceholder: 'Digite o nome do time aqui!', 29 | invalidColumnSelection: 'Selecione pelo menos uma coluna', 30 | colOnePlaceholder: 'Bom', 31 | button: 'Criar', 32 | buttonProgress: 'Criando..', 33 | captchaInfo: 'Complete o CAPTCHA para prosseguir', 34 | boardCreationError: 'Erro ao criar o quadro' 35 | }, 36 | dashboard: { 37 | timer: { 38 | oneMinuteLeft: 'Último minuto restante', 39 | timeCompleted: 'Tempo esgotado!', 40 | title: 'Iniciar/Parar temporizador', 41 | helpTip: 'Ajuste minutos/segundos com os botões + - ou teclas direcionais. Máximo de 1 hora.', 42 | invalid: 'Valores inválidos. Intervalo permitido: 1 segundo a 60 minutos.', 43 | tooltip: 'Temporizador regressivo' 44 | }, 45 | share: { 46 | title: 'Copie e compartilhe o link abaixo', 47 | linkCopied: 'Link copiado!', 48 | linkCopyError: 'Falha ao copiar. Copie manualmente.', 49 | toolTip: 'Compartilhar quadro' 50 | }, 51 | mask: { 52 | maskTooltip: 'Ocultar mensagens', 53 | unmaskTooltip: 'Exibir mensagens' 54 | }, 55 | lock: { 56 | lockTooltip: 'Bloquear quadro', 57 | unlockTooltip: 'Desbloquear quadro', 58 | message: 'Quadro bloqueado pelo dono.', 59 | discardChanges: 'Quadro bloqueado! Mensagens não salvas descartadas' 60 | }, 61 | spotlight: { 62 | noCardsToFocus: 'Nenhum card para destacar', 63 | tooltip: 'Destacar cards' 64 | }, 65 | download: { 66 | tooltip: 'Baixar como PDF' 67 | }, 68 | language: { 69 | tooltip : 'Mudar idioma' 70 | }, 71 | columns: { 72 | col01: 'O que funcionou bem', 73 | col02: 'Desafios', 74 | col03: 'Ações', 75 | col04: 'Agradecimentos', 76 | col05: 'Melhorias' 77 | }, 78 | pdfFooter: 'Criado com', 79 | offline: 'Você parece estar offline.' 80 | } 81 | } -------------------------------------------------------------------------------- /src/frontend/src/i18n/pt.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | langName: 'Português', 3 | common: { 4 | anonymous: 'Anônimo', 5 | minutes: 'Minutos', 6 | seconds: 'Segundos', 7 | start: 'Iniciar', 8 | stop: 'Parar', 9 | copy: 'Copiar', 10 | board: 'Quadro', 11 | toolTips: { 12 | darkTheme: 'Ativar tema escuro', 13 | lightTheme: 'Ativar tema claro' 14 | }, 15 | contentOverloadError: 'Conteúdo excede o limite.', 16 | contentStrippingError: 'Texto extra removido do final.' 17 | }, 18 | join: { 19 | label: 'Entrar como convidado', 20 | namePlaceholder: 'Digite seu nome aqui!', 21 | nameRequired: 'Digite seu nome', 22 | button: 'Entrar' 23 | }, 24 | createBoard: { 25 | label: 'Criar quadro', 26 | namePlaceholder: 'Nome do quadro aqui!', 27 | nameRequired: 'Digite o nome do quadro', 28 | teamNamePlaceholder: 'Nome do time aqui!', 29 | invalidColumnSelection: 'Selecione coluna(s)', 30 | colOnePlaceholder: 'Bom', 31 | button: 'Criar', 32 | buttonProgress: 'Criando..', 33 | captchaInfo: 'Complete o CAPTCHA para continuar', 34 | boardCreationError: 'Erro ao criar o quadro' 35 | }, 36 | dashboard: { 37 | timer: { 38 | oneMinuteLeft: 'Último minuto', 39 | timeCompleted: 'Tempo esgotado!', 40 | title: 'Iniciar/Parar timer', 41 | helpTip: 'Ajuste minutos/segundos com + - ou setas. Máx 1 hora.', 42 | invalid: 'Valores inválidos. Permitido: 1 segundo a 60 minutos.', 43 | tooltip: 'Temporizador' 44 | }, 45 | share: { 46 | title: 'Compartilhe esta URL', 47 | linkCopied: 'Link copiado!', 48 | linkCopyError: 'Falha ao copiar. Copie manualmente.', 49 | toolTip: 'Compartilhar quadro' 50 | }, 51 | mask: { 52 | maskTooltip: 'Ocultar mensagens', 53 | unmaskTooltip: 'Mostrar mensagens' 54 | }, 55 | lock: { 56 | lockTooltip: 'Bloquear quadro', 57 | unlockTooltip: 'Desbloquear quadro', 58 | message: 'Quadro bloqueado.', 59 | discardChanges: 'Quadro bloqueado! Mensagens não guardadas foram descartadas' 60 | }, 61 | spotlight: { 62 | noCardsToFocus: 'Nenhum cartão para focar', 63 | tooltip: 'Focar cartões' 64 | }, 65 | download: { 66 | tooltip: 'Baixar PDF' 67 | }, 68 | language: { 69 | tooltip : 'Mudar idioma' 70 | }, 71 | columns: { 72 | col01: 'O que deu certo', 73 | col02: 'Desafios', 74 | col03: 'Ações', 75 | col04: 'Agradecimentos', 76 | col05: 'Melhorias' 77 | }, 78 | pdfFooter: 'Criado com', 79 | offline: 'Offline.' 80 | } 81 | } -------------------------------------------------------------------------------- /src/frontend/src/i18n/ru.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | langName: 'Русский (ru)', 3 | common: { 4 | anonymous: 'Аноним', 5 | minutes: 'Минуты', 6 | seconds: 'Секунды', 7 | start: 'Старт', 8 | stop: 'Стоп', 9 | copy: 'Копировать', 10 | board: 'Доска', 11 | toolTips: { 12 | darkTheme: 'Тёмная тема', 13 | lightTheme: 'Светлая тема' 14 | }, 15 | contentOverloadError: 'Превышен лимит содержимого', 16 | contentStrippingError: 'Лишний текст удалён' 17 | }, 18 | join: { 19 | label: 'Войти как гость', 20 | namePlaceholder: 'Введите имя здесь!', 21 | nameRequired: 'Введите имя', 22 | button: 'Присоединиться' 23 | }, 24 | createBoard: { 25 | label: 'Создать доску', 26 | namePlaceholder: 'Название доски здесь!', 27 | nameRequired: 'Введите название доски', 28 | teamNamePlaceholder: 'Название команды здесь!', 29 | invalidColumnSelection: 'Выберите столбцы', 30 | colOnePlaceholder: 'Хорошо', 31 | button: 'Создать', 32 | buttonProgress: 'Создание..', 33 | captchaInfo: 'Пройдите CAPTCHA для продолжения', 34 | boardCreationError: 'Ошибка при создании доски' 35 | }, 36 | dashboard: { 37 | timer: { 38 | oneMinuteLeft: 'Осталась минута', 39 | timeCompleted: 'Время вышло!', 40 | title: 'Старт/Стоп таймер', 41 | helpTip: 'Используйте +/- или стрелки. Макс. 1 час.', 42 | invalid: 'Недопустимое время (1 сек - 60 мин)', 43 | tooltip: 'Таймер обратного отсчёта' 44 | }, 45 | share: { 46 | title: 'Скопируйте и поделитесь ссылкой', 47 | linkCopied: 'Ссылка скопирована!', 48 | linkCopyError: 'Ошибка копирования', 49 | toolTip: 'Поделиться доской' 50 | }, 51 | mask: { 52 | maskTooltip: 'Скрыть сообщения', 53 | unmaskTooltip: 'Показать сообщения' 54 | }, 55 | lock: { 56 | lockTooltip: 'Заблокировать доску', 57 | unlockTooltip: 'Разблокировать доску', 58 | message: 'Доска заблокирована', 59 | discardChanges: 'Доска заблокирована! Несохранённые сообщения удалены' 60 | }, 61 | spotlight: { 62 | noCardsToFocus: 'Нет карточек', 63 | tooltip: 'Выделить карточки' 64 | }, 65 | download: { 66 | tooltip: 'Печать' 67 | }, 68 | language: { 69 | tooltip : 'Сменить язык' 70 | }, 71 | columns: { 72 | col01: 'Что прошло хорошо', 73 | col02: 'Сложности', 74 | col03: 'Действия', 75 | col04: 'Благодарности', 76 | col05: 'Улучшения' 77 | }, 78 | pdfFooter: 'Создано с', 79 | offline: 'Офлайн' 80 | } 81 | } -------------------------------------------------------------------------------- /src/frontend/src/i18n/uk.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | langName: 'Українська (uk)', 3 | common: { 4 | anonymous: 'Анонім', 5 | minutes: 'Хвилини', 6 | seconds: 'Секунди', 7 | start: 'Старт', 8 | stop: 'Стоп', 9 | copy: 'Копіювати', 10 | board: 'Дошка', 11 | toolTips: { 12 | darkTheme: 'Увімкнути темну тему', 13 | lightTheme: 'Увімкнути світлу тему' 14 | }, 15 | contentOverloadError: 'Перевищено допустимий обсяг контенту.', 16 | contentStrippingError: 'Текст було скорочено через перевищення ліміту.' 17 | }, 18 | join: { 19 | label: 'Приєднатися як гість', 20 | namePlaceholder: 'Введіть ваше імʼя тут!', 21 | nameRequired: 'Будь ласка, введіть імʼя', 22 | button: 'Приєднатися' 23 | }, 24 | createBoard: { 25 | label: 'Створити дошку', 26 | namePlaceholder: 'Введіть назву дошки тут!', 27 | nameRequired: 'Будь ласка, введіть назву дошки', 28 | teamNamePlaceholder: 'Введіть назву команди тут!', 29 | invalidColumnSelection: 'Оберіть колонку(и)', 30 | colOnePlaceholder: 'Добре', 31 | button: 'Створити', 32 | buttonProgress: 'Створення..', 33 | captchaInfo: 'Будь ласка, пройдіть CAPTCHA', 34 | boardCreationError: 'Помилка при створенні дошки' 35 | }, 36 | dashboard: { 37 | timer: { 38 | oneMinuteLeft: 'Залишилася 1 хвилина', 39 | timeCompleted: 'Час вийшов!', 40 | title: 'Старт/Стоп таймер', 41 | helpTip: 'Користуйтеся + - або стрілками. Максимум 1 година.', 42 | invalid: 'Невірні значення (1 секунда - 60 хвилин)', 43 | tooltip: 'Таймер зворотного відліку' 44 | }, 45 | share: { 46 | title: 'Скопіюйте та поділіться посиланням', 47 | linkCopied: 'Посилання скопійовано!', 48 | linkCopyError: 'Помилка копіювання. Скопіюйте вручну.', 49 | toolTip: 'Поділитися дошкою' 50 | }, 51 | mask: { 52 | maskTooltip: 'Приховати повідомлення', 53 | unmaskTooltip: 'Показати повідомлення' 54 | }, 55 | lock: { 56 | lockTooltip: 'Заблокувати дошку', 57 | unlockTooltip: 'Розблокувати дошку', 58 | message: 'Дошка заблокована власником.', 59 | discardChanges: 'Дошку заблоковано! Незбережені повідомлення видалено' 60 | }, 61 | spotlight: { 62 | noCardsToFocus: 'Немає карток для фокусування', 63 | tooltip: 'Фокусувати картки' 64 | }, 65 | download: { 66 | tooltip: 'Друк' 67 | }, 68 | language: { 69 | tooltip : 'Змінити мову' 70 | }, 71 | columns: { 72 | col01: 'Що вдалося', 73 | col02: 'Складності', 74 | col03: 'Завдання', 75 | col04: 'Подяки', 76 | col05: 'Покращення' 77 | }, 78 | pdfFooter: 'Створено за допомогою', 79 | offline: 'Відсутнє інтернет-зʼєднання.' 80 | } 81 | } -------------------------------------------------------------------------------- /src/frontend/src/i18n/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | langName: '简体中文 (zh-CN)', 3 | common: { 4 | anonymous: '匿名', 5 | minutes: '分钟', 6 | seconds: '秒', 7 | start: '开始', 8 | stop: '停止', 9 | copy: '复制', 10 | board: '看板', 11 | toolTips: { 12 | darkTheme: '启用深色主题', 13 | lightTheme: '启用浅色主题' 14 | }, 15 | contentOverloadError: '内容超过允许限制', 16 | contentStrippingError: '内容超出限制,多余文字已被删除' 17 | }, 18 | join: { 19 | label: '以访客加入', 20 | namePlaceholder: '在此输入姓名!', 21 | nameRequired: '请输入姓名', 22 | button: '加入' 23 | }, 24 | createBoard: { 25 | label: '创建看板', 26 | namePlaceholder: '输入看板名称!', 27 | nameRequired: '请输入看板名称', 28 | teamNamePlaceholder: '输入团队名称!', 29 | invalidColumnSelection: '请选择列', 30 | colOnePlaceholder: '好', 31 | button: '创建', 32 | buttonProgress: '创建中..', 33 | captchaInfo: '请完成验证码以继续', 34 | boardCreationError: '创建看板时出错' 35 | }, 36 | dashboard: { 37 | timer: { 38 | oneMinuteLeft: '剩余一分钟', 39 | timeCompleted: '时间到!', 40 | title: '开始/停止计时器', 41 | helpTip: '使用+ -或方向键调整时间,最长1小时', 42 | invalid: '无效时间,允许范围:1秒至60分钟', 43 | tooltip: '倒计时器' 44 | }, 45 | share: { 46 | title: '复制并分享链接', 47 | linkCopied: '链接已复制!', 48 | linkCopyError: '复制失败,请手动复制', 49 | toolTip: '分享看板' 50 | }, 51 | mask: { 52 | maskTooltip: '隐藏消息', 53 | unmaskTooltip: '显示消息' 54 | }, 55 | lock: { 56 | lockTooltip: '锁定看板', 57 | unlockTooltip: '解锁看板', 58 | message: '看板已被锁定', 59 | discardChanges: '看板已锁定!未保存的消息已丢弃' 60 | }, 61 | spotlight: { 62 | noCardsToFocus: '没有可聚焦的卡片', 63 | tooltip: '聚焦卡片' 64 | }, 65 | download: { 66 | tooltip: '打印' 67 | }, 68 | language: { 69 | tooltip : '更改语言' 70 | }, 71 | columns: { 72 | col01: '做得好的', 73 | col02: '挑战', 74 | col03: '行动计划', 75 | col04: '感谢', 76 | col05: '改进建议' 77 | }, 78 | pdfFooter: '创建于', 79 | offline: '离线状态' 80 | } 81 | } -------------------------------------------------------------------------------- /src/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | /* ./src/index.css */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; -------------------------------------------------------------------------------- /src/frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | // import './style.css' 3 | import './index.css' 4 | import App from './App.vue' 5 | import router from './router' 6 | import ToastPlugin from 'vue-toast-notification' 7 | import i18n from './i18n' 8 | 9 | createApp(App).use(i18n).use(router).use(ToastPlugin).mount('#app') 10 | -------------------------------------------------------------------------------- /src/frontend/src/models/BoardColumn.ts: -------------------------------------------------------------------------------- 1 | export interface BoardColumn { 2 | id: string 3 | text: string 4 | isDefault: boolean // Used to identify if Board creator entered custom value for "text". Useful during multi-lang translation. 5 | color: string 6 | } -------------------------------------------------------------------------------- /src/frontend/src/models/CategoryChangeMessage.ts: -------------------------------------------------------------------------------- 1 | export interface CategoryChangeMessage { 2 | msgId: string 3 | newCategoryId: string 4 | oldCategoryId: string 5 | } -------------------------------------------------------------------------------- /src/frontend/src/models/DraftMessage.ts: -------------------------------------------------------------------------------- 1 | export interface DraftMessage { 2 | id: string 3 | msg: string 4 | cat: string 5 | anon: boolean 6 | } -------------------------------------------------------------------------------- /src/frontend/src/models/LikeMessage.ts: -------------------------------------------------------------------------------- 1 | export interface LikeMessage { 2 | msgId: string 3 | like: boolean 4 | } -------------------------------------------------------------------------------- /src/frontend/src/models/OnlineUser.ts: -------------------------------------------------------------------------------- 1 | export interface OnlineUser { 2 | nickname: string 3 | xid: string 4 | } -------------------------------------------------------------------------------- /src/frontend/src/models/Requests.ts: -------------------------------------------------------------------------------- 1 | import { BoardColumn } from "./BoardColumn" 2 | import { OnlineUser } from "./OnlineUser" 3 | 4 | export interface EventRequest { 5 | typ: string 6 | pyl: T 7 | } 8 | 9 | export interface RegisterEvent { 10 | by: string 11 | nickname: string 12 | xid: string 13 | grp: string 14 | } 15 | 16 | export interface MaskEvent { 17 | by: string 18 | grp: string 19 | mask: boolean 20 | } 21 | 22 | export interface LockEvent { 23 | by: string 24 | grp: string 25 | lock: boolean 26 | } 27 | 28 | export interface SaveMessageEvent { 29 | id: string 30 | by: string 31 | nickname: string 32 | grp: string 33 | msg: string 34 | cat: string 35 | anon: boolean 36 | } 37 | 38 | export interface LikeMessageEvent { 39 | msgId: string 40 | by: string 41 | like: boolean 42 | } 43 | 44 | export interface DeleteMessageEvent { 45 | msgId: string 46 | by: string 47 | grp: string 48 | } 49 | 50 | export interface CategoryChangeEvent { 51 | msgId: string 52 | by: string 53 | grp: string 54 | newcat: string 55 | oldcat: string 56 | } 57 | 58 | export interface TimerEvent { 59 | by: string 60 | grp: string 61 | expiryDurationInSeconds: number 62 | stop: boolean 63 | } 64 | 65 | export interface RegisterResponse { 66 | typ: 'reg' 67 | boardName: string 68 | boardTeam: string 69 | columns: BoardColumn[] 70 | boardStatus: string 71 | boardMasking: boolean 72 | boardLock: boolean 73 | isBoardOwner: boolean 74 | mine: boolean 75 | users: OnlineUser[] 76 | messages: MessageResponse[] 77 | timerExpiresInSeconds: number 78 | } 79 | 80 | export interface UserClosingResponse { 81 | typ: 'closing' 82 | users: OnlineUser[] 83 | } 84 | 85 | export interface MaskResponse { 86 | typ: 'mask' 87 | mask: boolean 88 | } 89 | 90 | export interface LockResponse { 91 | typ: 'lock' 92 | lock: boolean 93 | } 94 | 95 | export interface MessageResponse { 96 | typ: 'msg' 97 | id: string 98 | nickname: string 99 | msg: string 100 | cat: string 101 | likes: string 102 | liked: boolean 103 | mine: boolean 104 | anon: boolean 105 | } 106 | 107 | export interface LikeMessageResponse { 108 | typ: 'like' 109 | id: string 110 | likes: string 111 | liked: boolean 112 | } 113 | 114 | export interface DeleteMessageResponse { 115 | typ: 'del' 116 | id: string 117 | } 118 | 119 | export interface CategoryChangeResponse { 120 | typ: 'catchng' 121 | id: string 122 | newcat: string 123 | } 124 | 125 | export interface TimerResponse { 126 | typ: 'timer' 127 | expiresInSeconds: number 128 | } 129 | 130 | export type SocketResponse = RegisterResponse | MaskResponse | LockResponse | MessageResponse | 131 | LikeMessageResponse | DeleteMessageResponse | CategoryChangeResponse | UserClosingResponse | TimerResponse 132 | 133 | export function toSocketResponse(json: any): SocketResponse | null { 134 | 135 | if (json && json.typ) { 136 | switch (json.typ) { 137 | case 'reg': 138 | return json as RegisterResponse 139 | case 'mask': 140 | return json as MaskResponse 141 | case 'lock': 142 | return json as LockResponse 143 | case 'msg': 144 | return json as MessageResponse 145 | case 'like': 146 | return json as LikeMessageResponse 147 | case 'del': 148 | return json as DeleteMessageResponse 149 | case 'catchng': 150 | return json as CategoryChangeResponse 151 | case 'closing': 152 | return json as UserClosingResponse 153 | case 'timer': 154 | return json as TimerResponse 155 | // const data: MaskResponse = json 156 | // return data 157 | 158 | // return { 159 | // typ: 'reg', 160 | // boardName: json.boardName, 161 | // boardTeam: json.boardTeam, 162 | // boardStatus: json.boardStatus, 163 | // boardMasking: json.boardMasking, 164 | // isBoardOwner: json.isBoardOwner, 165 | // user: json.user as UserDetails, 166 | // } as RegisterResponse 167 | default: 168 | return null // Handle unknown "typ" values as needed 169 | } 170 | } 171 | 172 | return null 173 | } -------------------------------------------------------------------------------- /src/frontend/src/router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import Dashboard from './components/Dashboard.vue' 3 | import Join from './components/Join.vue' 4 | import CreateBoard from './components/CreateBoard.vue' 5 | 6 | export default createRouter({ 7 | history: createWebHistory(), 8 | routes: [ 9 | { 10 | path: '/', 11 | name: "start", 12 | component: Join, 13 | }, 14 | { 15 | path: '/create', 16 | name: 'create', 17 | component: CreateBoard, 18 | beforeEnter: () => { 19 | if (!localStorage.getItem("user") || !localStorage.getItem("xid") || !localStorage.getItem("nickname")) { 20 | return `/` 21 | } 22 | }, 23 | }, 24 | { 25 | path: '/board/:board', 26 | name: 'dashboard', 27 | component: Dashboard, 28 | beforeEnter: (to) => { 29 | if (!localStorage.getItem("user") || !localStorage.getItem("xid") || !localStorage.getItem("nickname")) { 30 | return `/board/${to.params.board}/join` 31 | } 32 | }, 33 | }, 34 | { 35 | path: '/board/:board/join', 36 | name: 'join', 37 | component: Join, 38 | }, 39 | ], 40 | }) 41 | -------------------------------------------------------------------------------- /src/frontend/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | a:hover { 22 | color: #535bf2; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | display: flex; 28 | place-items: center; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | } 32 | 33 | h1 { 34 | font-size: 3.2em; 35 | line-height: 1.1; 36 | } 37 | 38 | button { 39 | border-radius: 8px; 40 | border: 1px solid transparent; 41 | padding: 0.6em 1.2em; 42 | font-size: 1em; 43 | font-weight: 500; 44 | font-family: inherit; 45 | background-color: #1a1a1a; 46 | cursor: pointer; 47 | transition: border-color 0.25s; 48 | } 49 | button:hover { 50 | border-color: #646cff; 51 | } 52 | button:focus, 53 | button:focus-visible { 54 | outline: 4px auto -webkit-focus-ring-color; 55 | } 56 | 57 | .card { 58 | padding: 2em; 59 | } 60 | 61 | #app { 62 | max-width: 1280px; 63 | margin: 0 auto; 64 | padding: 2rem; 65 | text-align: center; 66 | } 67 | 68 | @media (prefers-color-scheme: light) { 69 | :root { 70 | color: #213547; 71 | background-color: #ffffff; 72 | } 73 | a:hover { 74 | color: #747bff; 75 | } 76 | button { 77 | background-color: #f9f9f9; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/frontend/src/types/turnstile.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Turnstile { 2 | interface RenderParameters { 3 | sitekey: string 4 | callback?: (token: string) => void 5 | 'error-callback'?: () => void 6 | 'expired-callback'?: () => void 7 | theme?: 'auto' | 'light' | 'dark', 8 | size?: 'normal' | 'flexible' | 'compact' 9 | language?: string 10 | } 11 | 12 | function remove(widgetId: string): void 13 | 14 | function reset(widgetId: string): void 15 | 16 | function render( 17 | container: string | HTMLElement, 18 | params: RenderParameters 19 | ): string 20 | } 21 | 22 | declare interface App_Config { 23 | turnstileEnabled: boolean 24 | turnstileSiteKey: string 25 | } 26 | 27 | declare interface Window { 28 | APP_CONFIG?: typeof App_Config 29 | turnstile: typeof Turnstile 30 | } -------------------------------------------------------------------------------- /src/frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 4 | theme: { 5 | extend: { 6 | minHeight: (theme) => ({ 7 | ...theme('spacing'), 8 | }), 9 | } 10 | }, 11 | darkMode: 'class', 12 | plugins: [], 13 | safelist: [ 14 | // Backgrounds 15 | { 16 | pattern: /^(bg|hover:bg|dark:bg|dark:hover:bg)-(red|green|yellow|fuchsia|orange)-(100|400|500|600|800)/, 17 | variants: ['hover', 'dark', 'dark:hover'], 18 | }, 19 | // Borders 20 | { 21 | pattern: /^(border|dark:border)-(red|green|yellow|fuchsia|orange)-(300|700)/, 22 | variants: ['dark'], 23 | }, 24 | // Text 25 | { 26 | pattern: /^(text|dark:text)-(red|green|yellow|fuchsia|orange)-(100|600)/, 27 | variants: ['dark'], 28 | } 29 | // Sizes 30 | // { 31 | // pattern: /w-(6|8)/, 32 | // }, 33 | // { 34 | // pattern: /h-(6|8)/, 35 | // } 36 | ], 37 | } -------------------------------------------------------------------------------- /src/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable", "ES2021.String"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /src/frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | server: { 8 | proxy: { 9 | '^/(ws)': { 10 | target: 'http://localhost:8080/', 11 | changeOrigin: true, 12 | ws: true, 13 | }, 14 | '^/(api)': { 15 | target: 'http://localhost:8080/', 16 | changeOrigin: true, 17 | }, 18 | '/config.js': { 19 | target: 'http://localhost:8080', 20 | changeOrigin: true, 21 | secure: false, 22 | } 23 | } 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /src/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vijeeshr/quickretro 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.5.0 7 | github.com/gorilla/mux v1.8.1 8 | github.com/gorilla/websocket v1.5.3 9 | github.com/lithammer/shortuuid/v4 v4.2.0 10 | github.com/redis/go-redis/v9 v9.7.3 11 | ) 12 | 13 | require ( 14 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 15 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 16 | github.com/google/uuid v1.6.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /src/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= 2 | github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 3 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 4 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 5 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 6 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 7 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 8 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 10 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 11 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 12 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 13 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 14 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 15 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 16 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 17 | github.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c= 18 | github.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y= 19 | github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= 20 | github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= 21 | -------------------------------------------------------------------------------- /src/helpers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // https://www.alexedwards.net/blog/how-to-properly-parse-a-json-request-body 4 | 5 | import ( 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "strconv" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | type malformedRequest struct { 17 | status int 18 | msg string 19 | } 20 | 21 | func (mr *malformedRequest) Error() string { 22 | return mr.msg 23 | } 24 | 25 | func decodeJSONBody(w http.ResponseWriter, r *http.Request, dst interface{}) error { 26 | ct := r.Header.Get("Content-Type") 27 | if ct != "" { 28 | mediaType := strings.ToLower(strings.TrimSpace(strings.Split(ct, ";")[0])) 29 | if mediaType != "application/json" { 30 | msg := "Content-Type header is not application/json" 31 | return &malformedRequest{status: http.StatusUnsupportedMediaType, msg: msg} 32 | } 33 | } 34 | 35 | r.Body = http.MaxBytesReader(w, r.Body, 1048576) 36 | 37 | dec := json.NewDecoder(r.Body) 38 | dec.DisallowUnknownFields() 39 | 40 | err := dec.Decode(&dst) 41 | if err != nil { 42 | var syntaxError *json.SyntaxError 43 | var unmarshalTypeError *json.UnmarshalTypeError 44 | 45 | switch { 46 | case errors.As(err, &syntaxError): 47 | msg := fmt.Sprintf("Request body contains badly-formed JSON (at position %d)", syntaxError.Offset) 48 | return &malformedRequest{status: http.StatusBadRequest, msg: msg} 49 | 50 | case errors.Is(err, io.ErrUnexpectedEOF): 51 | msg := fmt.Sprintf("Request body contains badly-formed JSON") 52 | return &malformedRequest{status: http.StatusBadRequest, msg: msg} 53 | 54 | case errors.As(err, &unmarshalTypeError): 55 | msg := fmt.Sprintf("Request body contains an invalid value for the %q field (at position %d)", unmarshalTypeError.Field, unmarshalTypeError.Offset) 56 | return &malformedRequest{status: http.StatusBadRequest, msg: msg} 57 | 58 | case strings.HasPrefix(err.Error(), "json: unknown field "): 59 | fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ") 60 | msg := fmt.Sprintf("Request body contains unknown field %s", fieldName) 61 | return &malformedRequest{status: http.StatusBadRequest, msg: msg} 62 | 63 | case errors.Is(err, io.EOF): 64 | msg := "Request body must not be empty" 65 | return &malformedRequest{status: http.StatusBadRequest, msg: msg} 66 | 67 | case err.Error() == "http: request body too large": 68 | msg := "Request body must not be larger than 1MB" 69 | return &malformedRequest{status: http.StatusRequestEntityTooLarge, msg: msg} 70 | 71 | default: 72 | return err 73 | } 74 | } 75 | 76 | err = dec.Decode(&struct{}{}) 77 | if !errors.Is(err, io.EOF) { 78 | msg := "Request body must only contain a single JSON object" 79 | return &malformedRequest{status: http.StatusBadRequest, msg: msg} 80 | } 81 | 82 | return nil 83 | } 84 | 85 | func parseDuration(s string) (time.Duration, error) { 86 | var multiplier time.Duration = 1 87 | switch { 88 | case strings.HasSuffix(s, "s"): 89 | multiplier = time.Second 90 | s = strings.TrimSuffix(s, "s") 91 | case strings.HasSuffix(s, "m"): 92 | multiplier = time.Minute 93 | s = strings.TrimSuffix(s, "m") 94 | case strings.HasSuffix(s, "h"): 95 | multiplier = time.Hour 96 | s = strings.TrimSuffix(s, "h") 97 | case strings.HasSuffix(s, "d"): 98 | multiplier = 24 * time.Hour 99 | s = strings.TrimSuffix(s, "d") 100 | default: 101 | return 0, fmt.Errorf("invalid duration format: missing unit (use s/m/h/d)") 102 | } 103 | 104 | value, err := strconv.Atoi(s) 105 | if err != nil { 106 | return 0, fmt.Errorf("invalid duration value: %w", err) 107 | } 108 | 109 | return time.Duration(value) * multiplier, nil 110 | } 111 | -------------------------------------------------------------------------------- /src/helpers_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func Test_parseDuration(t *testing.T) { 9 | tests := []struct { 10 | input string 11 | expected time.Duration 12 | hasError bool 13 | }{ 14 | {"10s", 10 * time.Second, false}, 15 | {"5m", 5 * time.Minute, false}, 16 | {"2h", 2 * time.Hour, false}, 17 | {"3d", 3 * 24 * time.Hour, false}, 18 | {"0s", 0, false}, 19 | {"100m", 100 * time.Minute, false}, 20 | {"-5m", -5 * time.Minute, false}, 21 | {"abc", 0, true}, 22 | {"10x", 0, true}, 23 | {"", 0, true}, 24 | } 25 | 26 | for _, tt := range tests { 27 | t.Run(tt.input, func(t *testing.T) { 28 | result, err := parseDuration(tt.input) 29 | if (err != nil) != tt.hasError { 30 | t.Errorf("parseDuration(%q) error = %v, wantErr %v", tt.input, err, tt.hasError) 31 | } 32 | if result != tt.expected { 33 | t.Errorf("parseDuration(%q) = %v, want %v", tt.input, result, tt.expected) 34 | } 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/hub.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log/slog" 6 | ) 7 | 8 | type Hub struct { 9 | clients map[string]map[*Client]bool // Board-wise clients. Board is like a typical "room". 10 | register chan *Client 11 | unregister chan *Client 12 | redis *RedisConnector 13 | } 14 | 15 | func newHub(r *RedisConnector) *Hub { 16 | return &Hub{ 17 | clients: make(map[string]map[*Client]bool), 18 | register: make(chan *Client), 19 | unregister: make(chan *Client), 20 | redis: r, 21 | } 22 | } 23 | 24 | func (hub *Hub) run() { 25 | for { 26 | select { 27 | case client := <-hub.register: 28 | // Create group/board/room if it doesn't exist 29 | if _, ok := hub.clients[client.group]; !ok { 30 | hub.clients[client.group] = make(map[*Client]bool) 31 | } 32 | hub.clients[client.group][client] = true // Insert or Update 33 | // hub.clients[client] = true 34 | case client := <-hub.unregister: 35 | // Delete the client and close the client's sending channel 36 | // Also delete the group/board/room when there are no clients attached to it. 37 | if _, ok := hub.clients[client.group]; ok { 38 | delete(hub.clients[client.group], client) 39 | close(client.send) 40 | // Delete group/board/room if no clients exist 41 | if len(hub.clients[client.group]) == 0 { 42 | delete(hub.clients, client.group) 43 | } 44 | } 45 | case broadcast := <-hub.redis.subscriber.Channel(): 46 | var args BroadcastArgs 47 | if err := json.Unmarshal([]byte(broadcast.Payload), &args); err != nil { 48 | slog.Error("Error unmarshalling to BroadcastArgs from redis channel in hub", "details", err.Error(), "payload", broadcast.Payload) 49 | } 50 | args.Event.Broadcast(args.Message, hub) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /src/message.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Store 4 | type Message struct { 5 | Id string `redis:"id"` 6 | By string `redis:"by"` 7 | ByNickname string `redis:"nickname"` 8 | Group string `redis:"group"` 9 | Content string `redis:"content"` 10 | Category string `redis:"category"` 11 | Anonymous bool `redis:"anon"` 12 | } 13 | 14 | func (m *MessageEvent) ToMessage() *Message { 15 | return &Message{ 16 | Id: m.Id, By: m.By, ByNickname: m.ByNickname, Group: m.Group, Content: m.Content, Category: m.Category, Anonymous: m.Anonymous} 17 | } 18 | 19 | func (m *Message) NewResponse(reqType string) interface{} { 20 | switch reqType { 21 | case "del": 22 | return DeleteMessageResponse{ 23 | Type: reqType, 24 | Id: m.Id, 25 | } 26 | case "like": 27 | return LikeMessageResponse{ 28 | Type: reqType, 29 | Id: m.Id, 30 | } 31 | default: 32 | return MessageResponse{ 33 | Type: reqType, 34 | Id: m.Id, 35 | ByNickname: m.ByNickname, 36 | Content: m.Content, 37 | Category: m.Category, 38 | Anonymous: m.Anonymous, 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/user.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Store 4 | type User struct { 5 | Id string `redis:"id"` 6 | Xid string `redis:"xid"` 7 | Nickname string `redis:"nickname"` 8 | } 9 | --------------------------------------------------------------------------------