New features for v2 (#83)

This commit is contained in:
Alexandre Lion
2024-04-06 11:48:47 +02:00
committed by GitHub
parent f575b24a45
commit 3f9be7e852
93 changed files with 5902 additions and 2723 deletions

View File

@@ -1,25 +1,39 @@
DATABASE_URL=postgres://claper:claper@db:5432/claper
SECRET_KEY_BASE=0LZiQBLw4WvqPlz4cz8RsHJlxNiSqM9B48y4ChyJ5v1oA0L/TPIqRjQNdPZN3iEG # Generate with `mix phx.gen.secret`
# Storage configuration
PRESENTATION_STORAGE=local PRESENTATION_STORAGE=local
PRESENTATION_STORAGE_DIR=/app/uploads PRESENTATION_STORAGE_DIR=/app/uploads
MAX_FILE_SIZE_MB=15 #MAX_FILE_SIZE_MB=15
AWS_ACCESS_KEY_ID=xxx #AWS_ACCESS_KEY_ID=xxx
AWS_SECRET_ACCESS_KEY=xxx #AWS_SECRET_ACCESS_KEY=xxx
AWS_REGION=eu-west-3 #AWS_REGION=eu-west-3
AWS_PRES_BUCKET=xxx #AWS_PRES_BUCKET=xxx
SMTP_RELAY=xx.example.com # Mail configuration
SMTP_USERNAME=johndoe@example.com
SMTP_PASSWORD=xxx
SMTP_PORT=465
SMTP_TLS=if_available
MAIL_TRANSPORT=local MAIL_TRANSPORT=local
MAIL_FROM=noreply@claper.co MAIL_FROM=noreply@claper.co
MAIL_FROM_NAME=Claper MAIL_FROM_NAME=Claper
ENABLE_ACCOUNT_CREATION=true #SMTP_RELAY=xx.example.com
ENABLE_MAILBOX_ROUTE=false #SMTP_USERNAME=johndoe@example.com
MAILBOX_USER=admin #SMTP_PASSWORD=xxx
MAILBOX_PASSWORD=admin #SMTP_PORT=465
#SMTP_TLS=if_available
GS_JPG_RESOLUTION=300x300 #ENABLE_MAILBOX_ROUTE=false
#MAILBOX_USER=admin
#MAILBOX_PASSWORD=admin
# Claper configuration
#ENABLE_ACCOUNT_CREATION=true
#GS_JPG_RESOLUTION=300x300
# Network configuration
ENDPOINT_PORT=4000
ENDPOINT_HOST=localhost

View File

@@ -1,6 +1,6 @@
[ [
import_deps: [:ecto, :phoenix], import_deps: [:ecto, :ecto_sql, :phoenix],
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"],
subdirectories: ["priv/*/migrations"], subdirectories: ["priv/*/migrations"],
plugins: [Phoenix.LiveView.HTMLFormatter] plugins: [Phoenix.LiveView.HTMLFormatter],
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
] ]

View File

@@ -18,7 +18,7 @@ jobs:
services: services:
db: db:
image: postgres:9 image: postgres:15
ports: ['5432:5432'] ports: ['5432:5432']
env: env:
POSTGRES_PASSWORD: claper POSTGRES_PASSWORD: claper
@@ -42,8 +42,8 @@ jobs:
- name: Set up Elixir - name: Set up Elixir
uses: erlef/setup-beam@988e02bfe678367a02564f65ca2e37726dc0268f uses: erlef/setup-beam@988e02bfe678367a02564f65ca2e37726dc0268f
with: with:
elixir-version: '1.13.2' elixir-version: '1.15.4'
otp-version: '24.1' otp-version: '26'
- name: Restore dependencies cache - name: Restore dependencies cache
uses: actions/cache@v3 uses: actions/cache@v3
with: with:

View File

@@ -1,3 +1,20 @@
## v2.0.0
- Add dynamic layout in the manager view
- Add quick event 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 tour guide for new users
- Add headers to exported CSV in reports
- Add the ability to embed attendees room in an iframe
- 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
## v1.7.0 ## v1.7.0
- Add keyboard shortcuts to control settings (#64) (@Dhanus3133) - Add keyboard shortcuts to control settings (#64) (@Dhanus3133)
@@ -12,6 +29,7 @@
- Security updates - Security updates
## v1.6.0 ## v1.6.0
- Improve QR code readability - Improve QR code readability
- Add ARM Docker image - Add ARM Docker image
- Refactor all runtime configuration - Refactor all runtime configuration
@@ -46,25 +64,21 @@
- Add MAX_FILE_SIZE_MB environment variable to limit file upload size - Add MAX_FILE_SIZE_MB environment variable to limit file upload size
- Add feature to deactivate messages during a presentation - Add feature to deactivate messages during a presentation
## v1.3.0 ## v1.3.0
- Add Form feature to collect data from your public - Add Form feature to collect data from your public
- Improve docs for Docker Compose - Improve docs for Docker Compose
- Improve Docker Compose file reference - Improve Docker Compose file reference
## v1.2.1 ## v1.2.1
- Fix presenter url (400 error in production) - Fix presenter url (400 error in production)
## v1.2.0 ## v1.2.0
- Added password change form in settings - Added password change form in settings
- Added more documentation on deployment in production - Added more documentation on deployment in production
## v1.1.1 ## v1.1.1
_Security updates_ _Security updates_
@@ -72,7 +86,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) - 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 - Restricted `/users/register` route if `ENABLE_ACCOUNT_CREATION` is false
## v1.1.0 ## v1.1.0
- Added password authentication - Added password authentication
@@ -81,7 +94,6 @@ _Security updates_
- Added new `ENABLE_ACCOUNT_CREATION` environment variable to enable or disable user registration - Added new `ENABLE_ACCOUNT_CREATION` environment variable to enable or disable user registration
- Improved french localization - Improved french localization
## v1.0.0 ## v1.0.0
This is the first version of the open-source project. Feel free to contribute! This is the first version of the open-source project. Feel free to contribute!

View File

@@ -11,4 +11,9 @@ Don't forget to give the project a star! Thanks again!
## Translations ## Translations
You can contribute to the translations by editing or addind PO files in `/priv/gettext/` 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.

View File

@@ -84,7 +84,7 @@ RUN mix release
# the compiled release and other runtime necessities # the compiled release and other runtime necessities
FROM ${RUNNER_IMAGE} FROM ${RUNNER_IMAGE}
RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales ghostscript \ RUN apt-get update -y && apt-get install -y curl libstdc++6 openssl libncurses5 locales ghostscript \
&& apt-get install -y libreoffice --no-install-recommends && apt-get clean && rm -f /var/lib/apt/lists/*_* && apt-get install -y libreoffice --no-install-recommends && apt-get clean && rm -f /var/lib/apt/lists/*_*
# Set the locale # Set the locale

View File

@@ -1,4 +1,3 @@
[![Contributors][contributors-shield]][contributors-url] [![Contributors][contributors-shield]][contributors-url]
[![Forks][forks-shield]][forks-url] [![Forks][forks-shield]][forks-url]
[![Stargazers][stars-shield]][stars-url] [![Stargazers][stars-shield]][stars-url]
@@ -26,14 +25,12 @@
</p> </p>
</div> </div>
[![Product Name Screen Shot][product-screenshot]](https://claper.co) [![Product Name Screen Shot][product-screenshot]](https://claper.co)
Claper turns your presentations into an interactive, engaging and exciting experience. Claper turns your presentations into an interactive, engaging and exciting experience.
Claper has a two-sided mission: 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 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. - 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.
@@ -43,12 +40,12 @@ Supported languages: 🇬🇧 English, 🇫🇷 French, 🇩🇪 German.
Claper is proudly powered by Phoenix and Elixir. Claper is proudly powered by Phoenix and Elixir.
* [![Phoenix][Phoenix]][Phoenix-url] - [![Phoenix][Phoenix]][Phoenix-url]
* [![Elixir][Elixir]][Elixir-url] - [![Elixir][Elixir]][Elixir-url]
* [![Tailwind][Tailwind]][Tailwind-url] - [![Tailwind][Tailwind]][Tailwind-url]
<!-- GETTING STARTED --> <!-- GETTING STARTED -->
## Getting Started ## Getting Started
This is an example of how you may give instructions on setting up your project locally. This is an example of how you may give instructions on setting up your project locally.
@@ -57,18 +54,20 @@ To get a local copy up and running follow these simple example steps.
### Prerequisites ### Prerequisites
To run Claper on your local environment you need to have: To run Claper on your local environment you need to have:
* Postgres >= 9
* Elixir >= 1.13.2 - Postgres >= 9
* Erlang >= 24 - Elixir >= 1.13.2
* NPM >= 6.14.17 - Erlang >= 24
* NodeJS >= 14.19.2 - NPM >= 6.14.17
* Ghostscript >= 9.5.0 (for PDF support) - NodeJS >= 14.19.2
* Libreoffice >= 6.4 (for PPT/PPTX support) - 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: You can also use Docker to easily run a Postgres instance:
```sh ```sh
docker run -p 5432:5432 -e POSTGRES_PASSWORD=claper -e POSTGRES_USER=claper -e POSTGRES_DB=claper --name claper-db -d postgres:9 docker run -p 5432:5432 -e POSTGRES_PASSWORD=claper -e POSTGRES_USER=claper -e POSTGRES_DB=claper --name claper-db -d postgres:15
``` ```
### Configuration ### Configuration
@@ -105,7 +104,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). 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 ### 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. 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.
@@ -116,19 +114,8 @@ cd Claper
docker compose up 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 --> <!-- CONTRIBUTING -->
## Contributing ## 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**. 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 +129,23 @@ Don't forget to give the project a star! Thanks again!
4. Push to the Branch (`git push origin feature/amazing_feature`) 4. Push to the Branch (`git push origin feature/amazing_feature`)
5. Open a Pull Request 5. Open a Pull Request
<!-- LICENSE --> <!-- LICENSE -->
## License ## License
Distributed under the GPLv3 License. See `LICENSE.txt` for more information. Distributed under the GPLv3 License. See `LICENSE.txt` for more information.
<!-- CONTACT --> <!-- CONTACT -->
## Contact ## Contact
[![](https://img.shields.io/badge/@alxlion__-000000?style=for-the-badge&logo=x&logoColor=white)](https://x.com/alxlion_) [![](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) Project Link: [https://github.com/ClaperCo/Claper](https://github.com/ClaperCo/Claper)
<!-- MARKDOWN LINKS & IMAGES --> <!-- MARKDOWN LINKS & IMAGES -->
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links --> <!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
[contributors-shield]: https://img.shields.io/github/contributors/ClaperCo/Claper.svg?style=for-the-badge [contributors-shield]: https://img.shields.io/github/contributors/ClaperCo/Claper.svg?style=for-the-badge
[contributors-url]: https://github.com/ClaperCo/Claper/graphs/contributors [contributors-url]: https://github.com/ClaperCo/Claper/graphs/contributors
[forks-shield]: https://img.shields.io/github/forks/ClaperCo/Claper.svg?style=for-the-badge [forks-shield]: https://img.shields.io/github/forks/ClaperCo/Claper.svg?style=for-the-badge

View File

@@ -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"); @import url("animate.css/animate.min.css");
@tailwind base; @tailwind base;
@@ -428,3 +428,20 @@
-o-transform:rotate(-20deg); -o-transform:rotate(-20deg);
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;
}

View File

@@ -2,6 +2,8 @@
@import "../node_modules/tiny-slider/src/tiny-slider.scss"; @import "../node_modules/tiny-slider/src/tiny-slider.scss";
@import "../node_modules/@sjmc11/tourguidejs/src/scss/tour.scss";
$particleSize: 20vmin; $particleSize: 20vmin;
$animationDuration: 6s; $animationDuration: 6s;
$amount: 20; $amount: 20;

View File

@@ -1,20 +1,3 @@
// 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. // Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html" import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration. // Establish Phoenix Socket and LiveView configuration.
@@ -22,13 +5,18 @@ import {Socket, Presence} from "phoenix"
import {LiveSocket} from "phoenix_live_view" import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar" import topbar from "../vendor/topbar"
import Alpine from 'alpinejs' import Alpine from 'alpinejs'
import flatpickr from "flatpickr"
import moment from "moment-timezone" 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 'moment/locale/de' import 'moment/locale/de'
import 'moment/locale/fr' import 'moment/locale/fr'
import QRCodeStyling from "qr-code-styling" import QRCodeStyling from "qr-code-styling"
import { Presenter } from "./presenter" import { Presenter } from "./presenter"
import { Manager } from "./manager" import { Manager } from "./manager"
import Split from "split-grid"
import { TourGuideClient } from "@sjmc11/tourguidejs/src/Tour"
window.moment = moment window.moment = moment
window.moment.locale("en") window.moment.locale("en")
@@ -36,32 +24,125 @@ window.moment.locale(navigator.language.split('-')[0])
window.Alpine = Alpine window.Alpine = Alpine
Alpine.start() Alpine.start()
let airdatepickerLocale = {
en: airdatepickerLocaleEn,
fr: airdatepickerLocaleFr,
de: airdatepickerLocaleDe
}
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let Hooks = {} 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 = { Hooks.Scroll = {
mounted() { mounted() {
if (this.el.dataset.postsNb > 4) window.scrollTo({top: document.querySelector(this.el.dataset.target).scrollHeight, behavior: 'smooth'}); if (this.el.dataset.postsNb > 4) window.scrollTo({top: document.querySelector(this.el.dataset.target).scrollHeight, behavior: 'smooth'});
this.handleEvent("scroll", () => { this.handleEvent("scroll", () => {
})
},
updated() {
let t = document.querySelector(this.el.dataset.target) let t = document.querySelector(this.el.dataset.target)
if (this.el.childElementCount > 4 && (window.scrollY + window.innerHeight >= t.offsetHeight - 100)) { if (this.el.childElementCount > 4 && (window.scrollY + window.innerHeight >= t.offsetHeight - 300)) {
window.scrollTo({top: t.scrollHeight, behavior: 'smooth'}); window.scrollTo({top: t.scrollHeight, behavior: 'smooth'});
} }
})
} }
} }
Hooks.ScrollIntoDiv = { Hooks.ScrollIntoDiv = {
mounted() { mounted() {
let t = document.querySelector(this.el.dataset.target) this.scrollElement(true);
if (this.el.dataset.postsNb > 4) t.scrollTo({top: t.scrollHeight, behavior: 'smooth'}); this.handleEvent("scroll", this.scrollElement.bind(this));
},
this.handleEvent("scroll", () => { scrollElement(firstScroll) {
let t = document.querySelector(this.el.dataset.target); let t = this.el.parentElement;
if (this.el.childElementCount > 4 && (t.scrollHeight - t.scrollTop < t.clientHeight + 100)) { if (firstScroll === true || (t.scrollHeight - t.scrollTop - t.clientHeight) <= 100) {
t.scrollTo({top: t.scrollHeight, behavior: 'smooth'}); t.scrollTo({top: t.scrollHeight, behavior: 'smooth'})
} }
})
} }
} }
@@ -74,7 +155,7 @@ Hooks.NicknamePicker = {
this.el.addEventListener("click", (e) => this.clicked(e)) this.el.addEventListener("click", (e) => this.clicked(e))
}, },
destroy() { destroyed() {
this.el.removeEventListener("click", (e) => this.clicked(e)) this.el.removeEventListener("click", (e) => this.clicked(e))
}, },
clicked(e) { clicked(e) {
@@ -91,7 +172,7 @@ Hooks.EmptyNickname = {
mounted() { mounted() {
this.el.addEventListener("click", (e) => this.clicked(e)) this.el.addEventListener("click", (e) => this.clicked(e))
}, },
destroy() { destroyed() {
this.el.removeEventListener("click", (e) => this.clicked(e)) this.el.removeEventListener("click", (e) => this.clicked(e))
}, },
clicked(e) { clicked(e) {
@@ -170,33 +251,21 @@ Hooks.CalendarLocalDate = {
} }
Hooks.Pickr = { Hooks.Pickr = {
mounted() { mounted() {
const getDefaultDate = (dateStart, dateEnd, mode) => { const localTime = this.el.querySelector("input[type=text]")
if (mode == "range") { const utcTime = this.el.querySelector("input[type=hidden]")
return moment.utc(dateStart).format('Y-MM-DD HH:mm') + " - " + moment.utc(dateEnd).format('Y-MM-DD HH:mm') localTime.value = moment.utc(utcTime.value).local().format("DD-MM-YYYY HH:mm")
} else { this.pickr = new AirDatepicker(localTime, {
return moment.utc(dateStart).format('Y-MM-DD HH:mm') dateFormat: "dd-MM-yyyy",
} timepicker: true,
}; minutesStep: 5,
this.pickr = flatpickr(this.el, { minDate: moment(),
wrap: true, timeFormat: "HH:mm",
inline: false, selectedDates: [moment(localTime.value, "DD-MM-YYYY HH:mm").toDate()],
enableTime: true, onSelect: ({date}) => {
enable: JSON.parse(this.el.dataset.enable), const utc = moment(date).utc().format("YYYY-MM-DDTHH:mm:ss")
time_24hr: true, utcTime.value = utc
formatDate: (date, format, locale) => {
return moment(date).utc().format('Y-MM-DD HH:mm');
}, },
parseDate: (datestr, format) => { locale: airdatepickerLocale[navigator.language.split('-')[0]]
return moment.utc(datestr).local().toDate();
},
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() { updated() {
@@ -298,11 +367,6 @@ Hooks.WelcomeEarly = {
}) })
} }
} }
Hooks.DefaultValue = {
mounted() {
this.el.value = moment(this.el.dataset.defaultValue ? this.el.dataset.defaultValue : undefined).utc().format();
}
}
Hooks.ClickFeedback = { Hooks.ClickFeedback = {
clicked(e) { clicked(e) {
this.el.className = "animate__animated animate__rubberBand animate__faster"; this.el.className = "animate__animated animate__rubberBand animate__faster";
@@ -313,7 +377,7 @@ Hooks.ClickFeedback = {
mounted() { mounted() {
this.el.addEventListener("click", (e) => this.clicked(e)) this.el.addEventListener("click", (e) => this.clicked(e))
}, },
destroy() { destroyed() {
this.el.removeEventListener("click", (e) => this.clicked(e)) this.el.removeEventListener("click", (e) => this.clicked(e))
} }
} }
@@ -370,6 +434,15 @@ Hooks.QRCode = {
} }
} }
Hooks.Dropdown = {
mounted() {
this.el.addEventListener("click", (e) => {
e.preventDefault()
this.el.classList.toggle("hidden")
})
}
}
let Uploaders = {} let Uploaders = {}
Uploaders.S3 = function(entries, onViewError){ Uploaders.S3 = function(entries, onViewError){

View File

@@ -15,8 +15,9 @@ export class Manager {
if (el) { if (el) {
setTimeout(() => { setTimeout(() => {
document.getElementById("slide-preview-" + data.current_page).scrollIntoView({ document.getElementById("slides").scrollTo({
block: 'center', top: el.offsetTop - el.scrollHeight,
left: 0,
behavior: 'smooth' behavior: 'smooth'
}); });
}, data.timeout ? data.timeout : 0) }, data.timeout ? data.timeout : 0)
@@ -51,10 +52,13 @@ export class Manager {
var el = document.getElementById("slide-preview-" + this.currentPage) var el = document.getElementById("slide-preview-" + this.currentPage)
if (el) { if (el) {
document.getElementById("slide-preview-" + this.currentPage).scrollIntoView({ setTimeout(() => {
block: 'center', document.getElementById("slides").scrollTo({
top: el.offsetTop - el.scrollHeight,
left: 0,
behavior: 'smooth' behavior: 'smooth'
}); });
}, 50)
} }
} }

2011
assets/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,12 +6,13 @@
"alpinejs": "^3.13.1", "alpinejs": "^3.13.1",
"autoprefixer": "^10.4.15", "autoprefixer": "^10.4.15",
"esbuild": "^0.14.54", "esbuild": "^0.14.54",
"flatpickr": "^4.6.13",
"postcss": "^8.4.29", "postcss": "^8.4.29",
"postcss-import": "^15.1.0", "postcss-import": "^15.1.0",
"tailwindcss": "^3.3.3" "tailwindcss": "^3.3.3"
}, },
"dependencies": { "dependencies": {
"@sjmc11/tourguidejs": "^0.0.16",
"air-datepicker": "^3.5.0",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"moment": "^2.29.4", "moment": "^2.29.4",
"moment-timezone": "^0.5.43", "moment-timezone": "^0.5.43",
@@ -19,6 +20,8 @@
"phoenix_html": "file:../deps/phoenix_html", "phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view", "phoenix_live_view": "file:../deps/phoenix_live_view",
"qr-code-styling": "^1.6.0-rc.1", "qr-code-styling": "^1.6.0-rc.1",
"split-grid": "^1.0.11",
"split.js": "^1.6.5",
"tiny-slider": "^2.9.4" "tiny-slider": "^2.9.4"
} }
} }

View File

@@ -30,7 +30,8 @@ port = get_int_from_path_or_env(config_dir, "PORT", "4000")
secret_key_base = get_var_from_path_or_env(config_dir, "SECRET_KEY_BASE", nil) secret_key_base = get_var_from_path_or_env(config_dir, "SECRET_KEY_BASE", nil)
case secret_key_base do if Mix.env() == :prod do
case secret_key_base do
nil -> nil ->
raise "SECRET_KEY_BASE configuration option is required. See https://docs.claper.co/configuration.html#production-docker" raise "SECRET_KEY_BASE configuration option is required. See https://docs.claper.co/configuration.html#production-docker"
@@ -39,6 +40,7 @@ case secret_key_base do
_ -> _ ->
nil nil
end
end end
endpoint_host = get_var_from_path_or_env(config_dir, "ENDPOINT_HOST", "localhost") endpoint_host = get_var_from_path_or_env(config_dir, "ENDPOINT_HOST", "localhost")

View File

@@ -11,7 +11,7 @@ config :claper, Claper.Repo,
database: "claper_test#{System.get_env("MIX_TEST_PARTITION")}", database: "claper_test#{System.get_env("MIX_TEST_PARTITION")}",
hostname: "localhost", hostname: "localhost",
pool: Ecto.Adapters.SQL.Sandbox, pool: Ecto.Adapters.SQL.Sandbox,
pool_size: 10 pool_size: 1
# We don't run a server during test. If one is required, # We don't run a server during test. If one is required,
# you can enable the server option below. # you can enable the server option below.
@@ -24,7 +24,7 @@ config :claper, ClaperWeb.Endpoint,
config :claper, Claper.Mailer, adapter: Swoosh.Adapters.Test config :claper, Claper.Mailer, adapter: Swoosh.Adapters.Test
# Print only warnings and errors during test # Print only warnings and errors during test
config :logger, level: :warn config :logger, level: :warning
# Initialize plugs at runtime for faster test compilation # Initialize plugs at runtime for faster test compilation
config :phoenix, :plug_init_mode, :runtime config :phoenix, :plug_init_mode, :runtime

View File

@@ -1,38 +0,0 @@
version: "3.0"
services:
db:
image: postgres:9
ports:
- 5432:5432
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: claper
POSTGRES_USER: claper
POSTGRES_DB: claper
healthcheck:
test: ["CMD-SHELL", "pg_isready -U claper"]
interval: 5s
timeout: 5s
retries: 10
app:
build: .
user: 0:0
ports:
- 4000:4000
volumes:
- uploads:/app/uploads
environment:
DATABASE_URL: postgres://claper:claper@db:5432/claper
SECRET_KEY_BASE: 0LZiQBLw4WvqPlz4cz8RsHJlxNiSqM9B48y4ChyJ5v1oA0L/TPIqRjQNdPZN3iEG
MAIL_TRANSPORT: local
ENDPOINT_PORT: 4000
PRESENTATION_STORAGE: local
MAX_FILE_SIZE_MB: 15
ENABLE_ACCOUNT_CREATION: true
depends_on:
db:
condition: service_healthy
volumes:
postgres-data:
uploads:

View File

@@ -1,37 +1,50 @@
version: "3.0"
services: services:
db: db:
image: postgres:9 image: postgres:15
ports: ports:
- 5432:5432 - 5432:5432
volumes: volumes:
- ./postgres-data:/var/lib/postgresql/data - "claper-db:/var/lib/postgresql/data"
healthcheck:
test:
- CMD
- pg_isready
- "-q"
- "-d"
- "claper"
- "-U"
- "claper"
retries: 3
timeout: 5s
environment: environment:
POSTGRES_PASSWORD: claper POSTGRES_PASSWORD: claper
POSTGRES_USER: claper POSTGRES_USER: claper
POSTGRES_DB: claper POSTGRES_DB: claper
healthcheck: networks:
test: ["CMD-SHELL", "pg_isready -U claper"] - claper-net
interval: 5s
timeout: 5s
retries: 10
app: app:
image: ghcr.io/claperco/claper:latest image: ghcr.io/claperco/claper:latest # or build: .
user: 0:0 user: 0:0
ports: ports:
- 4000:4000 - 4000:4000
volumes: volumes:
- uploads:/app/uploads - "claper-uploads:/app/uploads"
environment: healthcheck:
DATABASE_URL: postgres://claper:claper@db:5432/claper test: curl --fail http://localhost:4000 || exit 1
SECRET_KEY_BASE: 0LZiQBLw4WvqPlz4cz8RsHJlxNiSqM9B48y4ChyJ5v1oA0L/TPIqRjQNdPZN3iEG retries: 3
MAIL_TRANSPORT: local start_period: 20s
ENDPOINT_PORT: 4000 timeout: 5s
PRESENTATION_STORAGE: local env_file: .env
MAX_FILE_SIZE_MB: 15
ENABLE_ACCOUNT_CREATION: true
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
volumes: volumes:
uploads: claper-db:
driver: local
claper-uploads:
driver: local
networks:
claper-net:
driver: bridge

View File

@@ -3,14 +3,14 @@
## Prerequisites ## Prerequisites
To run Claper on your production environment you need to have: To run Claper on your production 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)
- 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)
## Steps (without docker) ## Steps (without docker)
@@ -71,43 +71,83 @@ server {
Here is a docker-compose example to run Claper behind Traefik. Here is a docker-compose example to run Claper behind Traefik.
```yaml ```yaml
version: "3.0"
services: services:
db: db:
image: postgres:9 image: postgres:15
ports:
- 5432:5432
volumes:
- "claper-db:/var/lib/postgresql/data"
healthcheck:
test:
- CMD
- pg_isready
- "-q"
- "-d"
- "claper"
- "-U"
- "claper"
retries: 3
timeout: 5s
environment: environment:
POSTGRES_PASSWORD: claper POSTGRES_PASSWORD: claper
POSTGRES_USER: claper POSTGRES_USER: claper
POSTGRES_DB: claper POSTGRES_DB: claper
networks:
- claper-net
app: app:
build: . build: .
environment: healthcheck:
DATABASE_URL: postgres://claper:claper@db:5432/claper test: curl --fail http://localhost:4000 || exit 1
SECRET_KEY_BASE: 0LZiQBLw4WvqPlz4cz8RsHJlxNiSqM9B48y4ChyJ5v1oA0L/TPIqRjQNdPZN3iEG retries: 3
MAIL_TRANSPORT: local start_period: 20s
ENDPOINT_HOST: claper.local timeout: 5s
ENDPOINT_PORT: 4000 volumes:
- "claper-uploads:/app/uploads"
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.claper.rule=Host(`claper.local`)" - "traefik.http.routers.app.rule=Host(`app.claper.co`)" # change to your domain
- "traefik.http.routers.claper.entrypoints=web" - "traefik.http.routers.app.tls.certresolver=myresolver"
- "traefik.http.routers.app.entrypoints=web"
- "traefik.http.services.app.loadbalancer.server.port=4000"
env_file: .env
depends_on: depends_on:
- db - db
- traefik - traefik
networks:
- claper-net
traefik: traefik:
image: traefik image: traefik
command: command:
#- "--log.level=DEBUG" #- "--log.level=DEBUG"
#- "--api.dashboard=true"
- "--accesslog.filepath=/var/log/traefik/access.log"
- "--api.insecure=true" - "--api.insecure=true"
- "--providers.docker=true" - "--providers.docker=true"
- "--providers.docker.exposedbydefault=false" - "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80" - "--entrypoints.web.address=:443"
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
- "--certificatesresolvers.myresolver.acme.email=yourmail@example.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
volumes: volumes:
- "../letsencrypt:/letsencrypt"
- "/var/run/docker.sock:/var/run/docker.sock:ro" - "/var/run/docker.sock:/var/run/docker.sock:ro"
- "/var/log/traefik:/var/log/traefik/"
ports: ports:
- "80:80" - "443:443"
- "8080:8080" networks:
- claper-net
volumes:
claper-db:
driver: local
claper-uploads:
driver: local
networks:
claper-net:
driver: bridge
``` ```
## Behind Kubernetes ## Behind Kubernetes

View File

@@ -3,18 +3,20 @@
## Prerequisites ## Prerequisites
To run Claper on your local environment you need to have: To run Claper on your local environment you need to have:
* Postgres >= 9
* Elixir >= 1.13.2 - Postgres >= 9
* Erlang >= 24 - Elixir >= 1.13.2
* NPM >= 6.14.17 - Erlang >= 24
* NodeJS >= 14.19.2 - NPM >= 6.14.17
* Ghostscript >= 9.5.0 (for PDF support) - NodeJS >= 14.19.2
* Libreoffice >= 6.4 (for PPT/PPTX support) - 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: You can also use Docker to easily run a Postgres instance:
```sh ```sh
docker run -p 5432:5432 -e POSTGRES_PASSWORD=claper -e POSTGRES_USER=claper -e POSTGRES_DB=claper --name claper-db -d postgres:9 docker run -p 5432:5432 -e POSTGRES_PASSWORD=claper -e POSTGRES_USER=claper -e POSTGRES_DB=claper --name claper-db -d postgres:15
``` ```
1. Clone the repo 1. Clone the repo
```sh ```sh
@@ -45,7 +47,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). 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 ## 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. 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.
@@ -56,17 +57,6 @@ cd Claper
docker compose up 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
```
### ARM architecture ### ARM architecture
If you are using an ARM architecture (like Apple M1), the original Docker image won't work. You can build the image yourself by replacing the `BUILDER_IMAGE` argument in the `Dockerfile` with `ARG BUILDER_IMAGE="hexpm/elixir-arm64:1.13.2-erlang-24.2.1-debian-bullseye-20210902-slim"` and then build the image as described above. If you are using an ARM architecture (like Apple M1), the original Docker image won't work. You can build the image yourself by replacing the `BUILDER_IMAGE` argument in the `Dockerfile` with `ARG BUILDER_IMAGE="hexpm/elixir-arm64:1.13.2-erlang-24.2.1-debian-bullseye-20210902-slim"` and then build the image as described above.

View File

@@ -182,7 +182,7 @@ defmodule Claper.Accounts do
## Examples ## Examples
iex> deliver_magic_link(user, &Routes.user_confirmation_url(conn, :confirm_magic, &1)) iex> deliver_magic_link(user, &url(~p"/users/magic/&1"))
{:ok, %{to: ..., body: ...}} {:ok, %{to: ..., body: ...}}
""" """
@@ -423,4 +423,8 @@ defmodule Claper.Accounts do
UserToken.user_magic_and_contexts_query(token.sent_to, ["magic"]) UserToken.user_magic_and_contexts_query(token.sent_to, ["magic"])
) )
end end
def delete(user) do
Repo.delete(user)
end
end end

View File

@@ -20,7 +20,7 @@ defmodule Claper.Events do
""" """
def list_events(user_id, preload \\ []) do def list_events(user_id, preload \\ []) do
from(e in Event, where: e.user_id == ^user_id, order_by: [desc: e.expired_at]) from(e in Event, where: e.user_id == ^user_id, order_by: [desc: e.inserted_at])
|> Repo.all() |> Repo.all()
|> Repo.preload(preload) |> Repo.preload(preload)
end end
@@ -140,7 +140,7 @@ defmodule Claper.Events do
def get_event_with_code!(code, preload \\ []) do def get_event_with_code!(code, preload \\ []) do
now = NaiveDateTime.utc_now() now = NaiveDateTime.utc_now()
from(e in Event, where: e.code == ^code and e.expired_at > ^now) from(e in Event, where: e.code == ^code and (is_nil(e.expired_at) or e.expired_at > ^now))
|> Repo.one!() |> Repo.one!()
|> Repo.preload(preload) |> Repo.preload(preload)
end end
@@ -148,7 +148,7 @@ defmodule Claper.Events do
def get_event_with_code(code, preload \\ []) do def get_event_with_code(code, preload \\ []) do
now = DateTime.utc_now() now = DateTime.utc_now()
from(e in Event, where: e.code == ^code and e.expired_at > ^now) from(e in Event, where: e.code == ^code and (is_nil(e.expired_at) or e.expired_at > ^now))
|> Repo.one() |> Repo.one()
|> Repo.preload(preload) |> Repo.preload(preload)
end end
@@ -234,7 +234,7 @@ defmodule Claper.Events do
end end
@doc """ @doc """
Updates a event. Updates an event.
## Examples ## Examples
@@ -258,6 +258,28 @@ defmodule Claper.Events do
end end
end end
@doc """
Terminates an event.
## Examples
iex> terminate_event(event)
{:ok, %Event{}}
"""
def terminate_event(%Event{} = event) do
event
|> Event.update_changeset(%{expired_at: NaiveDateTime.utc_now()})
|> Repo.update()
|> case do
{:ok, event} ->
broadcast({:ok, event, event.uuid}, :event_terminated)
{:error, changeset} ->
{:error, %{changeset | action: :update}}
end
end
@doc """ @doc """
Import interactions from another event Import interactions from another event
@@ -421,4 +443,16 @@ defmodule Claper.Events do
def change_activity_leader(%ActivityLeader{} = activity_leader, attrs \\ %{}) do def change_activity_leader(%ActivityLeader{} = activity_leader, attrs \\ %{}) do
ActivityLeader.changeset(activity_leader, attrs) ActivityLeader.changeset(activity_leader, attrs)
end end
defp broadcast({:error, _reason} = error, _event), do: error
defp broadcast({:ok, e, event_uuid}, event) do
Phoenix.PubSub.broadcast(
Claper.PubSub,
"event:#{event_uuid}",
{event, event_uuid}
)
{:ok, e}
end
end end

View File

@@ -10,8 +10,6 @@ defmodule Claper.Events.Event do
field :started_at, :naive_datetime field :started_at, :naive_datetime
field :expired_at, :naive_datetime field :expired_at, :naive_datetime
field :date_range, :string, virtual: true
has_many :posts, Claper.Posts.Post has_many :posts, Claper.Posts.Post
has_many :leaders, Claper.Events.ActivityLeader, on_replace: :delete has_many :leaders, Claper.Events.ActivityLeader, on_replace: :delete
@@ -30,21 +28,19 @@ defmodule Claper.Events.Event do
:code, :code,
:started_at, :started_at,
:expired_at, :expired_at,
:date_range,
:audience_peak :audience_peak
]) ])
|> cast_assoc(:presentation_file) |> cast_assoc(:presentation_file)
|> cast_assoc(:leaders) |> cast_assoc(:leaders)
|> validate_required([:code]) |> validate_required([:code, :name])
|> validate_date_range
end end
def create_changeset(event, attrs) do def create_changeset(event, attrs) do
event event
|> cast(attrs, [:name, :code, :user_id, :started_at, :expired_at, :date_range]) |> cast(attrs, [:name, :code, :user_id, :started_at, :expired_at])
|> cast_assoc(:presentation_file) |> cast_assoc(:presentation_file)
|> cast_assoc(:leaders) |> cast_assoc(:leaders)
|> validate_required([:code, :started_at, :expired_at]) |> validate_required([:code, :started_at])
|> downcase_code |> downcase_code
end end
@@ -56,38 +52,12 @@ defmodule Claper.Events.Event do
) )
end end
defp validate_date_range(changeset) do
date_range = get_change(changeset, :date_range)
if date_range != nil do
splited = date_range |> String.split(" - ")
if splited |> Enum.count() == 2 do
changeset
|> put_change(:started_at, Enum.at(splited, 0))
|> put_change(:expired_at, Enum.at(splited, 1))
else
add_error(changeset, :date_range, "invalid date range")
end
else
start_date = get_change(changeset, :started_at)
end_date = get_change(changeset, :expired_at)
if start_date != nil && end_date != nil do
changeset
|> put_change(:date_range, "#{start_date} - #{end_date}")
else
changeset
end
end
end
def update_changeset(event, attrs) do def update_changeset(event, attrs) do
event event
|> cast(attrs, [:name, :code, :started_at, :expired_at, :date_range, :audience_peak]) |> cast(attrs, [:name, :code, :started_at, :expired_at, :audience_peak])
|> cast_assoc(:presentation_file) |> cast_assoc(:presentation_file)
|> cast_assoc(:leaders) |> cast_assoc(:leaders)
|> validate_required([:code, :started_at, :expired_at]) |> validate_required([:code, :started_at])
|> downcase_code |> downcase_code
end end
@@ -105,4 +75,8 @@ defmodule Claper.Events.Event do
def started?(event) do def started?(event) do
NaiveDateTime.compare(NaiveDateTime.utc_now(), event.started_at) == :gt NaiveDateTime.compare(NaiveDateTime.utc_now(), event.started_at) == :gt
end end
def finished?(event) do
event.expired_at && NaiveDateTime.compare(NaiveDateTime.utc_now(), event.expired_at) == :gt
end
end end

View File

@@ -238,13 +238,14 @@ defmodule Claper.Forms do
[%FormSubmit{}, ...] [%FormSubmit{}, ...]
""" """
def list_form_submits(presentation_file_id) do def list_form_submits(presentation_file_id, preload \\ []) do
from(fs in FormSubmit, from(fs in FormSubmit,
join: f in Form, join: f in Form,
on: f.id == fs.form_id, on: f.id == fs.form_id,
where: f.presentation_file_id == ^presentation_file_id where: f.presentation_file_id == ^presentation_file_id
) )
|> Repo.all() |> Repo.all()
|> Repo.preload(preload)
end end
@doc """ @doc """

View File

@@ -9,6 +9,8 @@ defmodule Claper.Presentations.PresentationState do
field :join_screen_visible, :boolean field :join_screen_visible, :boolean
field :chat_enabled, :boolean field :chat_enabled, :boolean
field :anonymous_chat_enabled, :boolean field :anonymous_chat_enabled, :boolean
field :message_reaction_enabled, :boolean, default: true
field :show_poll_results_enabled, :boolean, default: true
field :banned, {:array, :string}, default: [] field :banned, {:array, :string}, default: []
field :show_only_pinned, :boolean, default: false field :show_only_pinned, :boolean, default: false
@@ -29,7 +31,9 @@ defmodule Claper.Presentations.PresentationState do
:presentation_file_id, :presentation_file_id,
:chat_enabled, :chat_enabled,
:anonymous_chat_enabled, :anonymous_chat_enabled,
:show_only_pinned :show_only_pinned,
:message_reaction_enabled,
:show_poll_results_enabled
]) ])
|> validate_required([]) |> validate_required([])
end end

View File

@@ -16,11 +16,14 @@ defmodule Claper.Stats do
def total_vote_count(presentation_file_id) do def total_vote_count(presentation_file_id) do
from(p in Claper.Polls.Poll, from(p in Claper.Polls.Poll,
join: o in Claper.Polls.PollOpt, join: pv in Claper.Polls.PollVote,
on: o.poll_id == p.id, on: pv.poll_id == p.id,
where: p.presentation_file_id == ^presentation_file_id, where: p.presentation_file_id == ^presentation_file_id,
group_by: o.poll_id, group_by: p.presentation_file_id,
select: sum(o.vote_count) select:
count(
fragment("DISTINCT COALESCE(?, CAST(? AS varchar))", pv.attendee_identifier, pv.user_id)
)
) )
|> Repo.all() |> Repo.all()
end end

View File

@@ -43,8 +43,6 @@ defmodule Claper.Tasks.Converter do
Remove the presentation files directory Remove the presentation files directory
""" """
def clear(hash) do def clear(hash) do
IO.puts("Clearing #{hash}...")
if get_presentation_storage() == "local" do if get_presentation_storage() == "local" do
File.rm_rf( File.rm_rf(
Path.join([ Path.join([

View File

@@ -17,13 +17,16 @@ defmodule ClaperWeb do
and import those modules here. and import those modules here.
""" """
def static_paths, do: ~w(assets fonts .well-known images favicon.ico robots.txt)
def controller do def controller do
quote do quote do
use Phoenix.Controller, namespace: ClaperWeb use Phoenix.Controller, namespace: ClaperWeb
import Plug.Conn import Plug.Conn
import ClaperWeb.Gettext import ClaperWeb.Gettext
alias ClaperWeb.Router.Helpers, as: Routes
unquote(verified_routes())
end end
end end
@@ -71,7 +74,9 @@ defmodule ClaperWeb do
def view_component do def view_component do
quote do quote do
use Phoenix.HTML import Phoenix.HTML
import Phoenix.HTML.Form
use PhoenixHTMLHelpers
use Phoenix.Component use Phoenix.Component
import ClaperWeb.ErrorHelpers import ClaperWeb.ErrorHelpers
alias Phoenix.LiveView.JS alias Phoenix.LiveView.JS
@@ -89,7 +94,9 @@ defmodule ClaperWeb do
defp view_helpers do defp view_helpers do
quote do quote do
# Use all HTML functionality (forms, tags, etc) # Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML import Phoenix.HTML
import Phoenix.HTML.Form
use PhoenixHTMLHelpers
# Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc) # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
import Phoenix.LiveView.Helpers import Phoenix.LiveView.Helpers
@@ -101,7 +108,17 @@ defmodule ClaperWeb do
import ClaperWeb.ErrorHelpers import ClaperWeb.ErrorHelpers
import ClaperWeb.Gettext import ClaperWeb.Gettext
alias ClaperWeb.Router.Helpers, as: Routes
unquote(verified_routes())
end
end
def verified_routes do
quote do
use Phoenix.VerifiedRoutes,
endpoint: ClaperWeb.Endpoint,
router: ClaperWeb.Router,
statics: ClaperWeb.static_paths()
end end
end end

View File

@@ -5,22 +5,22 @@ defmodule ClaperWeb.StatController do
def export(conn, %{"form_id" => form_id}) do def export(conn, %{"form_id" => form_id}) do
form = Forms.get_form!(form_id, [:form_submits]) form = Forms.get_form!(form_id, [:form_submits])
csv_data = csv_content(form.form_submits |> Enum.map(& &1.response)) headers = form.fields |> Enum.map(& &1.name)
csv_data = headers |> csv_content(form.form_submits |> Enum.map(& &1.response))
conn conn
|> put_resp_content_type("text/csv") |> put_resp_content_type("text/csv")
|> put_resp_header("content-disposition", "attachment; filename=\"export.csv\"") |> put_resp_header("content-disposition", "attachment; filename=\"#{form.title}.csv\"")
|> put_root_layout(false) |> put_root_layout(false)
|> send_resp(200, csv_data) |> send_resp(200, csv_data)
end end
defp csv_content(records) do defp csv_content(headers, records) do
data =
records records
|> Enum.map(fn record -> |> Enum.map(&(&1 |> Map.values()))
record
|> Map.new(fn {k, v} -> {String.to_atom(k), v} end) ([headers] ++ data)
|> Map.values()
end)
|> CSV.encode() |> CSV.encode()
|> Enum.to_list() |> Enum.to_list()
|> to_string() |> to_string()

View File

@@ -2,12 +2,12 @@ defmodule ClaperWeb.UserAuth do
@moduledoc """ @moduledoc """
Plug for user authentication. Plug for user authentication.
""" """
use ClaperWeb, :controller
import Plug.Conn import Plug.Conn
import Phoenix.Controller import Phoenix.Controller
alias Claper.Accounts alias Claper.Accounts
alias ClaperWeb.Router.Helpers, as: Routes
# Make the remember me cookie valid for 60 days. # Make the remember me cookie valid for 60 days.
# If you want bump or reduce this value, also change # If you want bump or reduce this value, also change
@@ -137,13 +137,13 @@ defmodule ClaperWeb.UserAuth do
conn conn
else else
conn conn
# |> redirect(to: Routes.user_registration_path(conn, :confirm)) # |> redirect(to: ~p"/users/register/confirm")
end end
else else
conn conn
|> put_flash(:error, "You must log in to access this page.") |> put_flash(:error, "You must log in to access this page.")
|> maybe_store_return_to() |> maybe_store_return_to()
|> redirect(to: Routes.user_session_path(conn, :new)) |> redirect(to: ~p"/users/log_in")
|> halt() |> halt()
end end
end end

View File

@@ -11,7 +11,7 @@ defmodule ClaperWeb.UserConfirmationController do
if user = Accounts.get_user_by_email(email) do if user = Accounts.get_user_by_email(email) do
Accounts.deliver_user_confirmation_instructions( Accounts.deliver_user_confirmation_instructions(
user, user,
&Routes.user_confirmation_url(conn, :update, &1) &url(~p"/users/confirm/#{&1}")
) )
end end

View File

@@ -26,7 +26,7 @@ defmodule ClaperWeb.UserRegistrationController do
# {:ok, _} = # {:ok, _} =
# Accounts.deliver_user_confirmation_instructions( # Accounts.deliver_user_confirmation_instructions(
# user, # user,
# &Routes.user_confirmation_url(conn, :update, &1) # &url(~p"/users/confirm/#{&1}")
# ) # )
conn conn

View File

@@ -15,7 +15,7 @@ defmodule ClaperWeb.UserResetPasswordController do
if user = Accounts.get_user_by_email(email) do if user = Accounts.get_user_by_email(email) do
Accounts.deliver_user_reset_password_instructions( Accounts.deliver_user_reset_password_instructions(
user, user,
&Routes.user_reset_password_url(conn, :edit, &1) &url(~p"/users/reset_password/#{&1}")
) )
end end

View File

@@ -10,10 +10,10 @@ defmodule ClaperWeb.UserSessionController do
end end
# def create(conn, %{"user" => %{"email" => email}} = _user_params) do # def create(conn, %{"user" => %{"email" => email}} = _user_params) do
# Accounts.deliver_magic_link(email, &Routes.user_confirmation_url(conn, :confirm_magic, &1)) # Accounts.deliver_magic_link(email, &url(~p"/users/magic/#{&1}"))
# conn # conn
# |> redirect(to: Routes.user_registration_path(conn, :confirm, %{email: email})) # |> redirect(to: ~p"/users/register/confirm?#{[%{email: email}]}")
# end # end
def create(conn, %{"user" => user_params}) do def create(conn, %{"user" => user_params}) do
%{"email" => email, "password" => password} = user_params %{"email" => email, "password" => password} = user_params

View File

@@ -18,7 +18,7 @@ defmodule ClaperWeb.UserSettingsController do
Accounts.deliver_update_email_instructions( Accounts.deliver_update_email_instructions(
applied_user, applied_user,
user.email, user.email,
&Routes.user_settings_url(conn, :confirm_email, &1) &url(~p"/users/settings/confirm_email/#{&1}")
) )
conn conn
@@ -26,7 +26,7 @@ defmodule ClaperWeb.UserSettingsController do
:info, :info,
"A link to confirm your email change has been sent to the new address." "A link to confirm your email change has been sent to the new address."
) )
|> redirect(to: Routes.user_settings_show_path(conn, :show)) |> redirect(to: ~p"/users/settings")
{:error, changeset} -> {:error, changeset} ->
render(conn, "edit.html", email_changeset: changeset) render(conn, "edit.html", email_changeset: changeset)
@@ -38,12 +38,12 @@ defmodule ClaperWeb.UserSettingsController do
:ok -> :ok ->
conn conn
|> put_flash(:info, "Email changed successfully.") |> put_flash(:info, "Email changed successfully.")
|> redirect(to: Routes.user_settings_show_path(conn, :show)) |> redirect(to: ~p"/users/settings")
:error -> :error ->
conn conn
|> put_flash(:error, "Email change link is invalid or it has expired.") |> put_flash(:error, "Email change link is invalid or it has expired.")
|> redirect(to: Routes.user_settings_show_path(conn, :show)) |> redirect(to: ~p"/users/settings")
end end
end end

View File

@@ -4,11 +4,23 @@ defmodule ClaperWeb.Endpoint do
# The session will be stored in the cookie and signed, # The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with. # this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it. # Set :encryption_salt if you would also like to encrypt it.
@session_options [ @session_options (case Mix.env() do
:dev ->
[
store: :cookie,
key: "_claper_key",
signing_salt: "Tg18Y2zU",
same_site: "None",
secure: true
]
_ ->
[
store: :cookie, store: :cookie,
key: "_claper_key", key: "_claper_key",
signing_salt: "Tg18Y2zU" signing_salt: "Tg18Y2zU"
] ]
end)
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
@@ -20,8 +32,7 @@ defmodule ClaperWeb.Endpoint do
at: "/", at: "/",
from: :claper, from: :claper,
gzip: false, gzip: false,
only: only: ClaperWeb.static_paths()
~w(assets fonts .well-known images favicon.ico robots.txt loaderio-eb3b956a176cdd4f54eb8570ce8bbb06.txt)
plug Plug.Static, plug Plug.Static,
at: "/uploads", at: "/uploads",

View File

@@ -1,6 +1,8 @@
defmodule ClaperWeb.EventLive.EventCardComponent do defmodule ClaperWeb.EventLive.EventCardComponent do
use ClaperWeb, :live_component use ClaperWeb, :live_component
alias Claper.Events.Event
def render(assigns) do def render(assigns) do
assigns = assigns =
assigns assigns
@@ -15,17 +17,19 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
<%= @event.name %> <%= @event.name %>
</p> </p>
<div class="ml-2 flex-shrink-0 flex"> <div class="ml-2 flex-shrink-0 flex">
<%= if NaiveDateTime.compare(@current_time, @event.started_at) == :gt and NaiveDateTime.compare(@current_time, @event.expired_at) == :lt do %> <%= if Event.started?(@event) && !Event.finished?(@event) do %>
<p class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800"> <div class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-500 text-white items-center gap-x-1">
<%= gettext("In progress") %> <span class="h-2 w-2 bg-white rounded-full animate__animated animate__flash animate__infinite animate__slow_slow">
</p> </span>
<%= gettext("Live") %>
</div>
<% end %> <% end %>
<%= if NaiveDateTime.compare(@current_time, @event.started_at) == :lt do %> <%= if !Event.started?(@event) && !Event.finished?(@event) do %>
<p class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"> <p class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
<%= gettext("Incoming") %> <%= gettext("Incoming") %>
</p> </p>
<% end %> <% end %>
<%= if NaiveDateTime.compare(@current_time, @event.expired_at) == :gt do %> <%= if Event.finished?(@event) do %>
<p class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"> <p class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">
<%= gettext("Finished") %> <%= gettext("Finished") %>
</p> </p>
@@ -44,25 +48,22 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
class="flex items-center text-sm text-gray-500 space-x-1" class="flex items-center text-sm text-gray-500 space-x-1"
phx-update="ignore" phx-update="ignore"
> >
<img src="/images/icons/calendar-clear-outline.svg" class="h-5 w-5" /> <img
<%= if NaiveDateTime.compare(@current_time, @event.started_at) == :gt and NaiveDateTime.compare(@current_time, @event.expired_at) == :lt do %> :if={
<p> Event.finished?(@event) ||
<%= gettext("Finish on") %> !Event.started?(@event)
<span x-text={"moment.utc('#{@event.expired_at}').local().format('lll')"}></span> }
</p> src="/images/icons/calendar-clear-outline.svg"
<% end %> class="h-5 w-5"
<%= if NaiveDateTime.compare(@current_time, @event.started_at) == :lt do %> />
<p> <p :if={!Event.finished?(@event) && !Event.started?(@event)}>
<%= gettext("Starting on") %> <%= gettext("Starting on") %>
<span x-text={"moment.utc('#{@event.started_at}').local().format('lll')"}></span> <span x-text={"moment.utc('#{@event.started_at}').local().format('lll')"}></span>
</p> </p>
<% end %> <p :if={Event.finished?(@event)}>
<%= if NaiveDateTime.compare(@current_time, @event.expired_at) == :gt do %>
<p>
<%= gettext("Finished on") %> <%= gettext("Finished on") %>
<span x-text={"moment.utc('#{@event.expired_at}').local().format('lll')"}></span> <span x-text={"moment.utc('#{@event.expired_at}').local().format('lll')"}></span>
</p> </p>
<% end %>
</div> </div>
</div> </div>
@@ -72,38 +73,118 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
</p> </p>
<% end %> <% end %>
<%= if NaiveDateTime.compare(@current_time, @event.expired_at) == :lt do %> <%= if !Event.finished?(@event) do %>
<%= if @event.presentation_file.status == "done" || (@event.presentation_file.status == "fail" && @event.presentation_file.hash) do %> <%= if @event.presentation_file.status != "progress" do %>
<div class="mt-2 flex flex-col space-y-2 sm:space-y-0 justify-between sm:flex-row items-center"> <div class="mt-2 flex flex-col space-y-2 sm:space-y-0 justify-between sm:flex-row items-center">
<div <div
id={"event-infos-0-#{@event.uuid}"} id={"event-infos-#{@event.uuid}"}
class="text-sm w-full space-y-2 sm:w-auto font-medium text-gray-700 sm:flex sm:justify-center sm:space-x-1 sm:space-y-0 sm:items-center" class="text-sm w-full sm:w-auto font-medium text-gray-700 flex justify-center space-x-1 sm:space-y-0 items-center relative"
phx-update="ignore"
> >
<button
phx-click-away={JS.hide(to: "#dropdown-#{@event.uuid}")}
phx-click={JS.toggle(to: "#dropdown-#{@event.uuid}")}
phx-target={@myself}
class="flex w-full lg:w-auto pl-3 pr-4 text-white items-center justify-between py-2 rounded-md tracking-wide font-bold focus:outline-none focus:shadow-outline hover:bg-primary-600 bg-primary-500"
>
<span class="mr-2"><%= gettext("Access") %></span>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m19.5 8.25-7.5 7.5-7.5-7.5"
/>
</svg>
</button>
<div
phx-hook="Dropdown"
id={"dropdown-#{@event.uuid}"}
class="hidden rounded shadow-lg bg-white border px-2 py-1 absolute -left-1 top-9 w-max"
>
<ul>
<li>
<a <a
data-phx-link="patch" data-phx-link="patch"
data-phx-link-state="push" data-phx-link-state="push"
href={Routes.event_manage_path(@socket, :show, @event.code)} class="py-2 px-2 rounded text-gray-600 hover:bg-gray-100 flex items-center gap-x-2"
class="flex w-full lg:w-auto px-6 text-white py-2 justify-center rounded-md tracking-wide font-bold focus:outline-none focus:shadow-outline hover:bg-primary-600 bg-primary-500 space-x-2" href={~p"/e/#{@event.code}/manage"}
> >
<img src="/images/icons/easel.svg" class="h-5" /> <svg
<span><%= gettext("Present/Customize") %></span> xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-6 h-6"
>
<path
fill-rule="evenodd"
d="M2.25 2.25a.75.75 0 0 0 0 1.5H3v10.5a3 3 0 0 0 3 3h1.21l-1.172 3.513a.75.75 0 0 0 1.424.474l.329-.987h8.418l.33.987a.75.75 0 0 0 1.422-.474l-1.17-3.513H18a3 3 0 0 0 3-3V3.75h.75a.75.75 0 0 0 0-1.5H2.25Zm6.04 16.5.5-1.5h6.42l.5 1.5H8.29Zm7.46-12a.75.75 0 0 0-1.5 0v6a.75.75 0 0 0 1.5 0v-6Zm-3 2.25a.75.75 0 0 0-1.5 0v3.75a.75.75 0 0 0 1.5 0V9Zm-3 2.25a.75.75 0 0 0-1.5 0v1.5a.75.75 0 0 0 1.5 0v-1.5Z"
clip-rule="evenodd"
/>
</svg>
<span><%= gettext("Presentation manager") %></span>
</a> </a>
</li>
<li>
<a <a
target="_blank" data-phx-link="patch"
href={Routes.event_show_path(@socket, :show, @event.code)} data-phx-link-state="push"
class="flex w-full lg:w-auto px-6 text-primary-500 py-2 justify-center rounded-md tracking-wide focus:outline-none focus:shadow-outline bg-white items-center space-x-2" class="py-2 px-2 rounded text-gray-600 hover:bg-gray-100 flex items-center gap-x-2"
href={~p"/e/#{@event.code}"}
> >
<img src="/images/icons/eye.svg" class="h-5" /> <svg
<span><%= gettext("Join") %></span> xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-6 h-6"
>
<path
fill-rule="evenodd"
d="M8.25 6.75a3.75 3.75 0 1 1 7.5 0 3.75 3.75 0 0 1-7.5 0ZM15.75 9.75a3 3 0 1 1 6 0 3 3 0 0 1-6 0ZM2.25 9.75a3 3 0 1 1 6 0 3 3 0 0 1-6 0ZM6.31 15.117A6.745 6.745 0 0 1 12 12a6.745 6.745 0 0 1 6.709 7.498.75.75 0 0 1-.372.568A12.696 12.696 0 0 1 12 21.75c-2.305 0-4.47-.612-6.337-1.684a.75.75 0 0 1-.372-.568 6.787 6.787 0 0 1 1.019-4.38Z"
clip-rule="evenodd"
/>
<path d="M5.082 14.254a8.287 8.287 0 0 0-1.308 5.135 9.687 9.687 0 0 1-1.764-.44l-.115-.04a.563.563 0 0 1-.373-.487l-.01-.121a3.75 3.75 0 0 1 3.57-4.047ZM20.226 19.389a8.287 8.287 0 0 0-1.308-5.135 3.75 3.75 0 0 1 3.57 4.047l-.01.121a.563.563 0 0 1-.373.486l-.115.04c-.567.2-1.156.349-1.764.441Z" />
</svg>
<span><%= gettext("Attendees room") %></span>
</a> </a>
</li>
</ul>
</div>
<.link
:if={Event.started?(@event)}
data-confirm={
gettext(
"Are you sure you want to terminate this event? This action cannot be undone."
)
}
phx-value-id={@event.uuid}
phx-click="terminate"
class="flex w-full lg:w-auto pl-3 pr-4 text-white items-center justify-between py-2 rounded-md tracking-wide font-bold focus:outline-none focus:shadow-outline bg-red-500 hover:bg-red-600 transition"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2.5"
stroke="currentColor"
class="w-5 h-5 mr-2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
<span><%= gettext("Terminate") %></span>
</.link>
</div> </div>
<div> <div>
<%= if not @is_leader do %> <%= if not @is_leader do %>
<a <a
data-phx-link="patch" data-phx-link="patch"
data-phx-link-state="push" data-phx-link-state="push"
href={Routes.event_index_path(@socket, :edit, @event.uuid)} href={~p"/events/#{@event.uuid}/edit"}
class="flex w-full lg:w-auto rounded-md tracking-wide focus:outline-none focus:shadow-outline text-primary-500 text-sm items-center" class="flex w-full lg:w-auto rounded-md tracking-wide focus:outline-none focus:shadow-outline text-primary-500 text-sm items-center"
> >
<span><%= gettext("Edit") %></span> <span><%= gettext("Edit") %></span>
@@ -113,8 +194,10 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
</div> </div>
<% end %> <% end %>
<%= if @event.presentation_file.status == "fail" && is_nil(@event.presentation_file.hash) do %> <div
<div class="mt-2 flex flex-col space-y-2 sm:space-y-0 justify-between sm:flex-row items-center"> :if={@event.presentation_file.status == "fail" && is_nil(@event.presentation_file.hash)}
class="mt-2 flex flex-col space-y-2 sm:space-y-0 justify-between sm:flex-row items-center"
>
<span class="text-sm text-supporting-red-500"> <span class="text-sm text-supporting-red-500">
<%= gettext("Error when processing the file") %> <%= gettext("Error when processing the file") %>
</span> </span>
@@ -123,7 +206,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
<a <a
data-phx-link="patch" data-phx-link="patch"
data-phx-link-state="push" data-phx-link-state="push"
href={Routes.event_index_path(@socket, :edit, @event.uuid)} href={~p"/events/#{@event.uuid}/edit"}
class="flex w-full lg:w-auto rounded-md tracking-wide focus:outline-none focus:shadow-outline text-primary-500 text-sm items-center" class="flex w-full lg:w-auto rounded-md tracking-wide focus:outline-none focus:shadow-outline text-primary-500 text-sm items-center"
> >
<span><%= gettext("Edit") %></span> <span><%= gettext("Edit") %></span>
@@ -131,28 +214,28 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
<% end %> <% end %>
</div> </div>
</div> </div>
<% end %>
<%= if @event.presentation_file.status == "progress" do %> <div
<div class="flex space-x-1 items-center"> :if={@event.presentation_file.status == "progress"}
class="flex space-x-1 items-center"
>
<img src="/images/loading.gif" class="h-8" /> <img src="/images/loading.gif" class="h-8" />
<span class="text-sm text-gray-500"><%= gettext("Processing your file...") %></span> <span class="text-sm text-gray-500"><%= gettext("Processing your file...") %></span>
</div> </div>
<% end %> <% end %>
<% end %>
<%= if NaiveDateTime.compare(@current_time, @event.expired_at) == :gt do %> <div
<div class="mt-2 flex flex-col space-y-2 sm:space-y-0 justify-between sm:flex-row items-center"> :if={Event.finished?(@event)}
class="mt-2 flex flex-col space-y-2 sm:space-y-0 justify-between sm:flex-row items-center"
>
<div <div
id={"event-infos-1-#{@event.uuid}"} id={"event-infos-1-#{@event.uuid}"}
class="text-sm w-full space-y-2 sm:w-auto font-medium text-gray-700 sm:flex sm:justify-center sm:space-x-1 sm:space-y-0 sm:items-center" class="text-sm w-full space-y-2 sm:w-auto font-medium text-gray-700 sm:flex sm:justify-center sm:space-x-1 sm:space-y-0 sm:items-center"
phx-update="ignore" phx-update="ignore"
> >
<a <a
data-phx-link="patch" href={~p"/events/#{@event.uuid}/stats"}
data-phx-link-state="push" class="flex w-full lg:w-auto px-3 text-white py-2 justify-center rounded-md tracking-wide font-bold focus:outline-none focus:shadow-outline hover:bg-primary-600 bg-primary-500 space-x-2"
href={Routes.stat_index_path(@socket, :index, @event.uuid)}
class="flex w-full lg:w-auto px-6 text-white py-2 justify-center rounded-md tracking-wide font-bold focus:outline-none focus:shadow-outline hover:bg-primary-600 bg-primary-500 space-x-2"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -163,7 +246,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
<path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z" /> <path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z" />
<path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z" /> <path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z" />
</svg> </svg>
<span><%= gettext("Report") %></span> <span><%= gettext("View report") %></span>
</a> </a>
</div> </div>
<div> <div>
@@ -184,10 +267,13 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
<% end %> <% end %>
</div> </div>
</div> </div>
<% end %>
</div> </div>
</div> </div>
</li> </li>
""" """
end end
def handle_event("open", _params, socket) do
{:noreply, socket |> assign(:dropdown, true)}
end
end end

View File

@@ -1,4 +1,5 @@
defmodule ClaperWeb.EventLive.EventFormComponent do defmodule ClaperWeb.EventLive.EventFormComponent do
alias Claper.Presentations.PresentationFile
use ClaperWeb, :live_component use ClaperWeb, :live_component
alias Claper.Events alias Claper.Events
@@ -118,7 +119,7 @@ defmodule ClaperWeb.EventLive.EventFormComponent do
File.cp!(path, dest) File.cp!(path, dest)
{:ok, Routes.static_path(socket, "/uploads/#{hash}/#{Path.basename(dest)}")} {:ok, "/uploads/#{hash}/#{Path.basename(dest)}"}
end) end)
[ext | _] = MIME.extensions(MIME.from_path(dest)) [ext | _] = MIME.extensions(MIME.from_path(dest))
@@ -151,10 +152,53 @@ defmodule ClaperWeb.EventLive.EventFormComponent do
save_file(socket, event_params, &edit_event/4) save_file(socket, event_params, &edit_event/4)
end end
defp save_event(socket, :new, event_params) do defp save_event(
%{assigns: %{event: %{:presentation_file => %PresentationFile{}}}} = socket,
:new,
event_params
) do
save_file(socket, event_params, &create_event/4) save_file(socket, event_params, &create_event/4)
end end
defp save_event(
%{assigns: %{event: %{:presentation_file => %Ecto.Association.NotLoaded{}}}} = socket,
:new,
event_params
) do
create_event(socket, event_params)
end
defp create_event(socket, event_params) do
case Events.create_event(
event_params
|> Map.put("user_id", socket.assigns.current_user.id)
|> Map.put("presentation_file", %{
"status" => "done",
"length" => 0,
"presentation_state" => %{}
})
) do
{:ok, event} ->
with e <- Events.get_event!(event.uuid, [:leaders]) do
Enum.each(e.leaders, fn leader ->
Claper.Accounts.LeaderNotifier.deliver_event_invitation(
e.name,
leader.email,
url(~p"/events")
)
end)
end
{:noreply,
socket
|> put_flash(:info, gettext("Created successfully"))
|> push_redirect(to: socket.assigns.return_to)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end
defp create_event(socket, event_params, hash, ext) do defp create_event(socket, event_params, hash, ext) do
case Events.create_event( case Events.create_event(
event_params event_params
@@ -176,7 +220,7 @@ defmodule ClaperWeb.EventLive.EventFormComponent do
Claper.Accounts.LeaderNotifier.deliver_event_invitation( Claper.Accounts.LeaderNotifier.deliver_event_invitation(
e.name, e.name,
leader.email, leader.email,
Routes.event_index_url(socket, :index) url(~p"/events")
) )
end) end)
end end
@@ -246,7 +290,7 @@ defmodule ClaperWeb.EventLive.EventFormComponent do
Claper.Accounts.LeaderNotifier.deliver_event_invitation( Claper.Accounts.LeaderNotifier.deliver_event_invitation(
e.name, e.name,
leader.email, leader.email,
Routes.event_index_url(socket, :index) url(~p"/events")
) )
end end
end) end)

View File

@@ -1,4 +1,11 @@
<div> <div
id="wrapper"
phx-hook="TourGuide"
data-next-label={gettext("Next")}
data-prev-label={gettext("Back")}
data-finish-label={gettext("Finish")}
data-group="create-event"
>
<div class="border-b border-gray-200 py-4 flex flex-col sm:flex-row sm:items-center justify-between"> <div class="border-b border-gray-200 py-4 flex flex-col sm:flex-row sm:items-center justify-between">
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<h1 class="text-2xl font-medium leading-6 text-gray-900 sm:truncate"> <h1 class="text-2xl font-medium leading-6 text-gray-900 sm:truncate">
@@ -6,7 +13,7 @@
</h1> </h1>
</div> </div>
<div class="flex mt-4 space-x-5 sm:mt-0"> <div class="flex mt-4 space-x-5 sm:mt-0">
<%= if @uploads.presentation_file.entries |> Enum.at(0, %{}) |> Map.get(:progress, 0) >= 100 || Map.has_key?(@event.presentation_file, :id) do %> <%= if (@uploads.presentation_file.entries |> Enum.at(0, %{}) |> Map.get(:progress, 0) == 0 || @uploads.presentation_file.entries |> Enum.at(0, %{}) |> Map.get(:progress, 0) == 100) && @changeset.valid? do %>
<button <button
type="submit" type="submit"
form="event-form" form="event-form"
@@ -26,7 +33,7 @@
end %> end %>
</div> </div>
<% end %> <% end %>
<%= if @action == :edit && NaiveDateTime.compare(NaiveDateTime.utc_now(), @event.expired_at) == :lt do %> <%= if @action == :edit && !@event.expired_at do %>
<%= link(gettext("Delete"), <%= link(gettext("Delete"),
to: "#", to: "#",
phx_click: "delete", phx_click: "delete",
@@ -39,10 +46,16 @@
</div> </div>
</div> </div>
<%= if Map.get(@event, :presentation_file) == nil || Map.get(@event.presentation_file, :id) == nil do %> <%= if Map.get(@event, :presentation_file) == nil || Map.get(@event.presentation_file, :length) == 0 || Map.get(@event.presentation_file, :id) == nil do %>
<div class="mt-12 mb-3"> <div
class="mb-3 mt-12"
data-tg-group="create-event"
data-tg-order="1"
data-tg-tour={"<p class='mb-3'>#{gettext("Select your presentation file. Accepted formats are PDF, PPT, or PPTX. Ensure the file size does not exceed the maximum limit.")}</p><p class='opacity-50 text-xs'>#{gettext("Animations in PPT/PPTX files are not supported, which is why we recommend exporting your presentation to PDF to ensure it displays correctly.")}</p>"}
data-tg-title={"📄 #{gettext("Presentation file (optional)")}"}
>
<label class="block text-sm font-medium text-gray-700 mb-2"> <label class="block text-sm font-medium text-gray-700 mb-2">
<%= gettext("Select your presentation") %> <%= gettext("Select your presentation (optional)") %>
</label> </label>
<div class="max-w-lg flex flex-col justify-center items-center px-6 pt-5 pb-6 border-2 bg-white shadow-base border-gray-300 border-dashed rounded-md"> <div class="max-w-lg flex flex-col justify-center items-center px-6 pt-5 pb-6 border-2 bg-white shadow-base border-gray-300 border-dashed rounded-md">
<%= if @uploads.presentation_file.entries |> Enum.at(0, %{}) |> Map.get(:progress, 0) < 100 do %> <%= if @uploads.presentation_file.entries |> Enum.at(0, %{}) |> Map.get(:progress, 0) < 100 do %>
@@ -70,7 +83,7 @@
phx-target={@myself} phx-target={@myself}
> >
<span><%= gettext("Upload a file") %></span> <span><%= gettext("Upload a file") %></span>
<%= live_file_input(@uploads.presentation_file, class: "sr-only") %> <.live_file_input upload={@uploads.presentation_file} class="sr-only" />
</form> </form>
</label> </label>
<p class="pl-1"><%= gettext("or drag and drop") %></p> <p class="pl-1"><%= gettext("or drag and drop") %></p>
@@ -157,7 +170,7 @@
phx-target={@myself} phx-target={@myself}
> >
<span><%= gettext("Change file") %></span> <span><%= gettext("Change file") %></span>
<%= live_file_input(@uploads.presentation_file, class: "sr-only") %> <.live_file_input upload={@uploads.presentation_file} class="sr-only" />
</form> </form>
</label> </label>
<p class="text-supporting-red-500 text-sm italic text-center hidden"> <p class="text-supporting-red-500 text-sm italic text-center hidden">
@@ -225,15 +238,20 @@
<ClaperWeb.Component.Input.text <ClaperWeb.Component.Input.text
form={f} form={f}
key={:name} key={:name}
name={gettext("Name of your presentation")} name={gettext("Name of your event")}
autofocus="true" autofocus="true"
required="true" required="true"
/> />
</div> </div>
<div class="my-3"> <div
class="my-3"
data-tg-title={"🔑 #{gettext("Access code")}"}
data-tg-tour={"<p>#{gettext("This code will be used by your attendees to access the event. You have the option to create a custom code.")}</p>"}
data-tg-group="create-event"
data-tg-order="2"
>
<ClaperWeb.Component.Input.code <ClaperWeb.Component.Input.code
readonly={NaiveDateTime.compare(NaiveDateTime.utc_now(), @event.expired_at) == :gt}
form={f} form={f}
key={:code} key={:code}
name={gettext("Code")} name={gettext("Code")}
@@ -241,15 +259,20 @@
/> />
</div> </div>
<div class="my-3"> <div
<ClaperWeb.Component.Input.date_range class="my-3"
readonly={NaiveDateTime.compare(NaiveDateTime.utc_now(), @event.expired_at) == :gt} data-tg-title={"🗓️ #{gettext("Event start date")}"}
data-tg-tour={"<p class='mb-3'>#{gettext("Select the start date for your event. Future dates are permissible.")}</p><p class='opacity-50 text-xs'>#{gettext("Attendees attempting to access the event prior to this date will be directed to a waiting room.")}</p>"}
data-tg-group="create-event"
data-tg-order="3"
phx-update="ignore"
id="date-picker"
>
<ClaperWeb.Component.Input.date
form={f} form={f}
key={:date_range} key={:started_at}
name={gettext("When your presentation will be available ?")} name={gettext("When your event will start?")}
required="true" required="true"
start_date_field={:started_at}
end_date_field={:expired_at}
from={Date.add(Date.utc_today(), -1)} from={Date.add(Date.utc_today(), -1)}
to={Date.add(Date.utc_today(), 365)} to={Date.add(Date.utc_today(), 365)}
/> />
@@ -270,7 +293,13 @@
<% end %> <% end %>
</div> </div>
<div class="mt-20 mb-3"> <div
class="mt-7 mb-3"
data-tg-title={"🧑‍💻 #{gettext("Facilitators")}"}
data-tg-tour={"<p class='mb-3'>#{gettext("If you require assistance in managing your event, you can grant access to others. Simply enter their email addresses; once they register an account with these emails, they will be able to manage the event.")}</p><p class='opacity-50 text-xs'>#{gettext("Note: Facilitators do not have the ability to delete your event.")}</p>"}
data-tg-group="create-event"
data-tg-order="4"
>
<span class="text-lg block font-medium text-gray-700"> <span class="text-lg block font-medium text-gray-700">
<%= gettext("Facilitators can present and manage interactions") %> <%= gettext("Facilitators can present and manage interactions") %>
</span> </span>
@@ -278,7 +307,7 @@
type="button" type="button"
phx-click="add-leader" phx-click="add-leader"
phx-target={@myself} 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"
> >
<svg <svg
class="text-white h-6 transform" class="text-white h-6 transform"
@@ -292,8 +321,8 @@
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
clip-rule="evenodd" clip-rule="evenodd"
/> />
<span><%= gettext("Add facilitator") %></span>
</svg> </svg>
<span><%= gettext("Add facilitator") %></span>
</button> </button>
</div> </div>

View File

@@ -12,16 +12,24 @@ defmodule ClaperWeb.EventLive.Index do
Gettext.put_locale(ClaperWeb.Gettext, locale) Gettext.put_locale(ClaperWeb.Gettext, locale)
end end
changeset =
Events.change_event(%Event{}, %{
started_at: NaiveDateTime.utc_now(),
code: Enum.random(1000..9999),
leaders: []
})
if connected?(socket) do if connected?(socket) do
Phoenix.PubSub.subscribe(Claper.PubSub, "events:#{socket.assigns.current_user.id}") Phoenix.PubSub.subscribe(Claper.PubSub, "events:#{socket.assigns.current_user.id}")
end end
socket = socket =
socket socket
|> assign(:events, list_events(socket)) |> stream(:events, list_events(socket))
|> assign(:managed_events, list_managed_events(socket)) |> assign(:managed_events, list_managed_events(socket))
|> assign(:quick_event_changeset, changeset)
{:ok, socket, temporary_assigns: [events: []]} {:ok, socket}
end end
@impl true @impl true
@@ -33,26 +41,92 @@ defmodule ClaperWeb.EventLive.Index do
def handle_info({:presentation_file_process_done, presentation}, socket) do def handle_info({:presentation_file_process_done, presentation}, socket) do
event = Claper.Events.get_event!(presentation.event.uuid, [:presentation_file]) event = Claper.Events.get_event!(presentation.event.uuid, [:presentation_file])
{:noreply, {:noreply, socket |> stream_insert(:events, event) |> put_flash(:info, nil)}
socket |> update(:events, fn events -> [event | events] end) |> put_flash(:info, nil)}
end end
@impl true @impl true
def handle_event("delete", %{"id" => id}, socket) do def handle_event("validate", %{"event" => event_params}, socket) do
event = Events.get_event!(id, [:presentation_file]) changeset =
%Event{}
|> Claper.Events.change_event(event_params)
|> Map.put(:action, :validate)
{:noreply, socket |> assign(:quick_event_changeset, changeset)}
end
@impl true
def handle_event("save", %{"event" => event_params}, socket) do
case Claper.Events.create_event(
event_params
|> Map.put("user_id", socket.assigns.current_user.id)
|> Map.put("presentation_file", %{
"status" => "done",
"length" => 0,
"presentation_state" => %{}
})
|> Map.put("started_at", NaiveDateTime.utc_now())
|> Map.put("code", "#{Enum.random(1000..9999)}")
) do
{:ok, _event} ->
{:noreply,
socket
|> put_flash(:info, gettext("Quick event created successfully"))
|> push_redirect(to: ~p"/events")}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, quick_event_changeset: changeset)}
end
end
@impl true
def handle_event("delete", %{"id" => id}, %{assigns: %{current_user: current_user}} = socket) do
event = Events.get_user_event!(current_user.id, id, [:presentation_file])
{:ok, _} = Events.delete_event(event) {:ok, _} = Events.delete_event(event)
Task.Supervisor.async_nolink(Claper.TaskSupervisor, fn -> Task.Supervisor.async_nolink(Claper.TaskSupervisor, fn ->
Claper.Tasks.Converter.clear(event.presentation_file.hash) Claper.Tasks.Converter.clear(event.presentation_file.hash)
end) end)
{:noreply, redirect(socket, to: Routes.event_index_path(socket, :index))} {:noreply, redirect(socket, to: ~p"/events")}
end
@impl true
def handle_event(
"checked",
%{"key" => "no_file", "value" => value},
%{assigns: %{event: event}} = socket
) do
{:noreply, socket |> assign(:event, %{event | no_file: value})}
end
@impl true
def handle_event("terminate", %{"id" => id}, %{assigns: %{current_user: current_user}} = socket) do
event = Events.get_user_event!(current_user.id, id)
{:ok, _} = Events.terminate_event(event)
{:noreply, redirect(socket, to: ~p"/events")}
end
@impl true
def handle_event(
"toggle-quick-create",
_params,
%{assigns: %{:live_action => :quick_create}} = socket
) do
{:noreply, assign(socket, :live_action, :index)}
end
@impl true
def handle_event("toggle-quick-create", _params, %{assigns: %{:live_action => :index}} = socket) do
{:noreply, assign(socket, :live_action, :quick_create)}
end end
defp apply_action(socket, :edit, %{"id" => id}) do defp apply_action(socket, :edit, %{"id" => id}) do
event = event =
Events.get_user_event!(socket.assigns.current_user.id, id, [:presentation_file, :leaders]) Events.get_user_event!(socket.assigns.current_user.id, id, [:presentation_file, :leaders])
if event.expired_at && NaiveDateTime.compare(NaiveDateTime.utc_now(), event.expired_at) == :gt do
redirect(socket, to: ~p"/events")
else
if event.presentation_file.status == "fail" && event.presentation_file.hash do if event.presentation_file.status == "fail" && event.presentation_file.hash do
Claper.Presentations.update_presentation_file(event.presentation_file, %{ Claper.Presentations.update_presentation_file(event.presentation_file, %{
"status" => "done" "status" => "done"
@@ -65,13 +139,13 @@ defmodule ClaperWeb.EventLive.Index do
|> assign(:page_title, gettext("Edit")) |> assign(:page_title, gettext("Edit"))
|> assign(:event, event) |> assign(:event, event)
end end
end
defp apply_action(socket, :new, _params) do defp apply_action(socket, :new, _params) do
socket socket
|> assign(:page_title, gettext("Create")) |> assign(:page_title, gettext("Create"))
|> assign(:event, %Event{ |> assign(:event, %Event{
started_at: NaiveDateTime.utc_now(), started_at: NaiveDateTime.utc_now(),
expired_at: NaiveDateTime.utc_now() |> NaiveDateTime.add(3600 * 2, :second),
code: Enum.random(1000..9999), code: Enum.random(1000..9999),
leaders: [] leaders: []
}) })

View File

@@ -1,4 +1,4 @@
<div class="mx-3 max-w-7xl sm:mx-auto"> <div class="mx-3 md:max-w-3xl lg:max-w-5xl md:mx-auto">
<%= if @live_action in [:new, :edit] do %> <%= if @live_action in [:new, :edit] do %>
<.live_component <.live_component
module={ClaperWeb.EventLive.EventFormComponent} module={ClaperWeb.EventLive.EventFormComponent}
@@ -6,21 +6,117 @@
event={@event} event={@event}
page_title={@page_title} page_title={@page_title}
action={@live_action} action={@live_action}
return_to={Routes.event_index_path(@socket, :index)} return_to={~p"/events"}
current_user={@current_user} current_user={@current_user}
/> />
<% else %> <% else %>
<div class="border-b border-gray-200 py-4 flex items-center justify-between"> <div
id="quick-create-modal"
class={"#{if @live_action != :quick_create, do: 'hidden' } fixed z-30 inset-0 overflow-y-auto p-4 sm:p-6 md:p-24
transform transition-all duration-150"}
role="dialog"
aria-modal="true"
>
<div
phx-click="toggle-quick-create"
class="fixed inset-0 bg-gray-800 bg-opacity-75 transition-opacity w-full h-full"
aria-hidden="true"
>
</div>
<div class="mx-auto max-w-xl transform divide-y divide-gray-100 overflow-hidden rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
<button phx-click="toggle-quick-create" class="absolute right-0 top-0">
<svg
class="text-gray-500 h-9 transform rotate-45"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
clip-rule="evenodd"
/>
</svg>
</button>
<div id="modal-content" class="bg-gray-100 pt-7 pb-5 px-3">
<.form
:let={f}
:if={@live_action == :quick_create}
for={@quick_event_changeset}
phx-change="validate"
phx-submit="save"
>
<ClaperWeb.Component.Input.text
form={f}
key={:name}
name=""
readonly={false}
class="h-12"
placeholder={gettext("Name of your event")}
/>
<button
type="submit"
phx_disable_with="Loading..."
class="mt-5 w-full lg:w-auto px-6 text-white py-2 rounded-md tracking-wide font-bold focus:outline-none focus:shadow-outline bg-gradient-to-tl from-primary-500 to-secondary-500 bg-size-200 bg-pos-0 hover:bg-pos-100 transition-all duration-500"
>
<%= gettext("Create") %>
</button>
</.form>
</div>
</div>
</div>
<div
class="border-b border-gray-200 py-4 flex items-center justify-between relative"
id="events-header"
phx-hook="TourGuide"
data-group="welcome"
data-next-label={gettext("Next")}
data-prev-label={gettext("Back")}
data-finish-label={gettext("Finish")}
>
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<h1 class="text-2xl font-medium leading-6 text-gray-900 sm:truncate"> <h1 class="text-2xl font-medium leading-6 text-gray-900 sm:truncate">
<%= gettext("My presentations") %> <%= gettext("My events") %>
</h1> </h1>
</div> </div>
<div class="flex mt-0"> <div class="flex mt-0">
<a <.link
data-phx-link="patch" data-tg-group="welcome"
data-phx-link-state="push" data-tg-tour={
href={Routes.event_index_path(@socket, :new)} gettext(
"If you don't have time and just want interactions without a presentation file, you can create a new event here."
)
}
data-tg-order="2"
data-tg-title={"#{gettext("In a hurry ?")} 🏃‍♂️"}
phx-click="toggle-quick-create"
class="relative inline-flex items-center px-5 py-2 text-sm rounded-md text-gray-500"
>
<svg
class="-ml-1 mr-1 h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
clip-rule="evenodd"
/>
</svg>
<span>
<%= gettext("Quick event") %>
</span>
</.link>
<.link
data-tg-group="welcome"
data-tg-order="1"
data-tg-tour={gettext("Welcome to Claper! You can create a new event here.")}
data-tg-title={"#{gettext("Your first steps with Claper")} 👋"}
href={~p"/events/new"}
class="relative inline-flex items-center px-5 py-2 text-lg font-medium rounded-md text-white bg-gradient-to-tl from-primary-500 to-secondary-500 bg-size-200 bg-pos-0 hover:bg-pos-100 transition-all duration-500" class="relative inline-flex items-center px-5 py-2 text-lg font-medium rounded-md text-white bg-gradient-to-tl from-primary-500 to-secondary-500 bg-size-200 bg-pos-0 hover:bg-pos-100 transition-all duration-500"
> >
<svg <svg
@@ -37,28 +133,27 @@
/> />
</svg> </svg>
<span> <span>
<%= gettext("Create") %> <%= gettext("Create event") %>
</span> </span>
</a> </.link>
</div> </div>
</div> </div>
<div class="mt-10 relative"> <div class="mt-2 relative">
<ul role="event-list" class="divide-y divide-gray-200" phx-update="append" id="events"> <ul role="event-list" phx-update="stream" id="events">
<% current_time = NaiveDateTime.utc_now() %> <% current_time = NaiveDateTime.utc_now() %>
<%= for event <- @events do %>
<.live_component <.live_component
:for={{id, event} <- @streams.events}
module={ClaperWeb.EventLive.EventCardComponent} module={ClaperWeb.EventLive.EventCardComponent}
id={"event-#{event.uuid}"} id={id}
event={event} event={event}
current_time={current_time} current_time={current_time}
/> />
<% end %>
</ul> </ul>
<%= if Enum.count(@events) == 0 do %> <%= if Enum.count(@streams.events) == 0 do %>
<div class="w-full text-2xl text-black opacity-25 text-center"> <div class="w-full text-2xl text-black opacity-25 text-center">
<img src="/images/icons/arrow.svg" class="h-20 float-right mr-16 -mt-5" /> <img src="/images/icons/arrow.svg" class="h-20 float-right mr-16 -mt-5" />
<p class="pt-12 clear-both"><%= gettext("Create your first presentation") %></p> <p class="pt-12 clear-both"><%= gettext("Create your first event") %></p>
</div> </div>
<% end %> <% end %>
</div> </div>
@@ -67,18 +162,13 @@
<div class="border-b border-gray-200 py-4 flex items-center justify-between mt-12"> <div class="border-b border-gray-200 py-4 flex items-center justify-between mt-12">
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<h1 class="text-2xl font-medium leading-6 text-gray-900 sm:truncate"> <h1 class="text-2xl font-medium leading-6 text-gray-900 sm:truncate">
<%= gettext("Invited presentations") %> <%= gettext("Invited events") %>
</h1> </h1>
</div> </div>
</div> </div>
<div class="mt-10 relative"> <div class="mt-2 relative">
<ul <ul role="managed-event-list" id="event-cards" phx-update="replace">
role="managed-event-list"
class="divide-y divide-gray-200"
id="event-cards"
phx-update="replace"
>
<% current_time = NaiveDateTime.utc_now() %> <% current_time = NaiveDateTime.utc_now() %>
<%= for event <- @managed_events do %> <%= for event <- @managed_events do %>
<.live_component <.live_component

View File

@@ -28,8 +28,7 @@ defmodule ClaperWeb.EventLive.Join do
@impl true @impl true
def handle_event("join", %{"event" => %{"code" => code}}, socket) do def handle_event("join", %{"event" => %{"code" => code}}, socket) do
{:noreply, {:noreply, socket |> push_redirect(to: ~p"/e/#{String.downcase(code)}")}
socket |> push_redirect(to: Routes.event_show_path(socket, :show, String.downcase(code)))}
end end
defp apply_action(socket, :join, _params) do defp apply_action(socket, :join, _params) do

View File

@@ -3,6 +3,7 @@
background: linear-gradient(-45deg, #2C033A, #21033A, #053138, #053138); background: linear-gradient(-45deg, #2C033A, #21033A, #053138, #053138);
background-size: 400% 400%; background-size: 400% 400%;
animation: gradient 15s ease infinite; animation: gradient 15s ease infinite;
height: 100vh;
} }
</style> </style>
@@ -23,17 +24,19 @@
<%= gettext("About") %> <%= gettext("About") %>
</a> </a>
<%= if @current_user do %> <%= if @current_user do %>
<%= live_patch(gettext("Dashboard"), <.link
to: Routes.event_index_path(@socket, :index), href={~p"/events"}
class: class="relative inline-flex items-center px-4 py-1 text-base font-sm rounded-md text-white bg-gradient-to-tl from-primary-500 to-secondary-500 bg-size-200 bg-pos-0 hover:bg-pos-100 transition-all duration-500"
"relative inline-flex items-center px-4 py-1 text-base font-sm rounded-md text-white bg-gradient-to-tl from-primary-500 to-secondary-500 bg-size-200 bg-pos-0 hover:bg-pos-100 transition-all duration-500" >
) %> <%= gettext("Dashboard") %>
</.link>
<% else %> <% else %>
<%= live_patch(gettext("Login"), <.link
to: Routes.user_session_path(@socket, :new), href={~p"/users/log_in"}
class: class="relative inline-flex items-center px-4 py-1 text-base font-sm rounded-md text-white bg-gradient-to-tl from-primary-500 to-secondary-500 bg-size-200 bg-pos-0 hover:bg-pos-100 transition-all duration-500"
"relative inline-flex items-center px-4 py-1 text-base font-sm rounded-md text-white bg-gradient-to-tl from-primary-500 to-secondary-500 bg-size-200 bg-pos-0 hover:bg-pos-100 transition-all duration-500" >
) %> <%= gettext("Login") %>
</.link>
<% end %> <% end %>
</div> </div>
<button @click="open = true" class="md:hidden"> <button @click="open = true" class="md:hidden">
@@ -44,17 +47,19 @@
<%= gettext("About") %> <%= gettext("About") %>
</a> </a>
<%= if @current_user do %> <%= if @current_user do %>
<%= live_patch(gettext("Dashboard"), <.link
to: Routes.event_index_path(@socket, :index), href={~p"/events"}
class: class="relative inline-flex items-center px-4 py-1 text-base font-sm rounded-md text-white bg-gradient-to-tl from-primary-500 to-secondary-500 bg-size-200 bg-pos-0 hover:bg-pos-100 transition-all duration-500"
"relative inline-flex items-center px-4 py-1 text-base font-sm rounded-md text-white bg-gradient-to-tl from-primary-500 to-secondary-500 bg-size-200 bg-pos-0 hover:bg-pos-100 transition-all duration-500" >
) %> <%= gettext("Dashboard") %>
</.link>
<% else %> <% else %>
<%= live_patch(gettext("Login"), <.link
to: Routes.user_session_path(@socket, :new), href={~p"/users/log_in"}
class: class="relative inline-flex items-center px-4 py-1 text-base font-sm rounded-md text-white bg-gradient-to-tl from-primary-500 to-secondary-500 bg-size-200 bg-pos-0 hover:bg-pos-100 transition-all duration-500"
"relative inline-flex items-center px-4 py-1 text-base font-sm rounded-md text-white bg-gradient-to-tl from-primary-500 to-secondary-500 bg-size-200 bg-pos-0 hover:bg-pos-100 transition-all duration-500" >
) %> <%= gettext("Login") %>
</.link>
<% end %> <% end %>
</div> </div>
</div> </div>
@@ -64,24 +69,7 @@
<img src="/images/logo.svg" class="h-12 mx-auto mb-16" /> <img src="/images/logo.svg" class="h-12 mx-auto mb-16" />
</div> </div>
<%= if @last_event do %> <%= form_for :event, ~p"/join", ["phx-submit": "join", "phx-hook": "JoinEvent", id: "form"], fn f -> %>
<%= live_patch to: Routes.event_show_path(@socket, :show, @last_event.code) do %>
<div class="rounded-md bg-gray-600 p-4 mb-8">
<div class="flex justify-center items-center">
<p class="text-sm text-white">
<%= gettext("Return to your last presentation") %> (#<span class="uppercase"><%= @last_event.code %></span>)
</p>
<p class="text-base ml-3 mt-1">
<a href="#" class="whitespace-nowrap font-medium text-white">
<span aria-hidden="true">&rarr;</span>
</a>
</p>
</div>
</div>
<% end %>
<% end %>
<%= form_for :event, Routes.event_join_path(@socket, :join), ["phx-submit": "join", "phx-hook": "JoinEvent", id: "form"], fn f -> %>
<div class="relative"> <div class="relative">
<%= text_input(f, :code, <%= text_input(f, :code,
required: true, required: true,
@@ -107,6 +95,23 @@
</button> </button>
<img src="/images/loading.gif" id="loading" class="hidden h-12 mx-auto" /> <img src="/images/loading.gif" id="loading" class="hidden h-12 mx-auto" />
</div> </div>
<%= if @last_event do %>
<.link href={~p"/e/#{@last_event.code}"}>
<div class="rounded-md bg-gray-600 bg-opacity-50 p-4 mt-8">
<div class="flex justify-center items-center">
<p class="text-sm text-white">
<%= gettext("Return to your last event") %> (<%= @last_event.name %>)
</p>
<p class="text-base ml-3 mt-1">
<a href="#" class="whitespace-nowrap font-medium text-white">
<span aria-hidden="true">&rarr;</span>
</a>
</p>
</div>
</div>
</.link>
<% end %>
<% end %> <% end %>
</div> </div>
</div> </div>

View File

@@ -26,7 +26,6 @@ defmodule ClaperWeb.EventLive.Manage do
else else
if connected?(socket) do if connected?(socket) do
Claper.Events.Event.subscribe(event.uuid) Claper.Events.Event.subscribe(event.uuid)
# Claper.Presentations.subscribe(event.presentation_file.id)
Presence.track( Presence.track(
self(), self(),
@@ -41,9 +40,14 @@ defmodule ClaperWeb.EventLive.Manage do
|> assign(:attendees_nb, 1) |> assign(:attendees_nb, 1)
|> assign(:event, event) |> assign(:event, event)
|> assign(:state, event.presentation_file.presentation_state) |> assign(:state, event.presentation_file.presentation_state)
|> assign(:pinned_posts, list_pinned_posts(socket, event.uuid)) |> stream(:pinned_posts, list_pinned_posts(socket, event.uuid))
|> assign(:all_posts, list_all_posts(socket, event.uuid)) |> stream(:posts, list_all_posts(socket, event.uuid))
|> assign(:pinned_post_count, length(list_pinned_posts(socket, event.uuid))) |> assign(:pinned_post_count, length(list_pinned_posts(socket, event.uuid)))
|> assign(:post_count, length(list_all_posts(socket, event.uuid)))
|> assign(
:form_submit_count,
length(list_form_submits(socket, event.presentation_file.id))
)
|> assign(:polls, list_polls(socket, event.presentation_file.id)) |> assign(:polls, list_polls(socket, event.presentation_file.id))
|> assign(:forms, list_forms(socket, event.presentation_file.id)) |> assign(:forms, list_forms(socket, event.presentation_file.id))
|> assign(:embeds, list_embeds(socket, event.presentation_file.id)) |> assign(:embeds, list_embeds(socket, event.presentation_file.id))
@@ -58,7 +62,7 @@ defmodule ClaperWeb.EventLive.Manage do
|> form_at_position(false) |> form_at_position(false)
|> embed_at_position(false) |> embed_at_position(false)
{:ok, socket, temporary_assigns: [all_posts: [], pinned_posts: [], form_submits: []]} {:ok, socket}
end end
end end
@@ -78,7 +82,8 @@ defmodule ClaperWeb.EventLive.Manage do
def handle_info({:post_created, post}, socket) do def handle_info({:post_created, post}, socket) do
{:noreply, {:noreply,
socket socket
|> assign(:all_posts, [post | socket.assigns.all_posts]) |> stream_insert(:posts, post)
|> update(:post_count, fn post_count -> post_count + 1 end)
|> push_event("scroll", %{})} |> push_event("scroll", %{})}
end end
@@ -86,27 +91,28 @@ defmodule ClaperWeb.EventLive.Manage do
def handle_info({:post_updated, updated_post}, socket) do def handle_info({:post_updated, updated_post}, socket) do
{:noreply, {:noreply,
socket socket
|> update(:all_posts, fn posts -> [updated_post | posts] end) |> stream_insert(:posts, updated_post)
|> update(:pinned_posts, fn posts -> [updated_post | posts] end)} |> stream_insert(:pinned_posts, updated_post)}
end end
@impl true @impl true
def handle_info({:post_deleted, deleted_post}, socket) do def handle_info({:post_deleted, deleted_post}, socket) do
{:noreply, {:noreply,
socket socket
|> update(:all_posts, fn posts -> [deleted_post | posts] end) |> stream_delete(:posts, deleted_post)
|> update(:pinned_posts, fn posts -> [deleted_post | posts] end) |> stream_delete(:pinned_posts, deleted_post)
|> update(:pinned_post_count, fn pinned_post_count -> |> update(:pinned_post_count, fn pinned_post_count ->
pinned_post_count - if deleted_post.pinned, do: 1, else: 0 pinned_post_count - if deleted_post.pinned, do: 1, else: 0
end)} end)
|> update(:post_count, fn post_count -> post_count - 1 end)}
end end
@impl true @impl true
def handle_info({:post_pinned, post}, socket) do def handle_info({:post_pinned, post}, socket) do
updated_socket = updated_socket =
socket socket
|> update(:all_posts, fn all_posts -> [post | all_posts] end) |> stream_insert(:posts, post)
|> update(:pinned_posts, fn pinned_posts -> [post | pinned_posts] end) |> stream_insert(:pinned_posts, post)
|> assign(:pinned_post_count, socket.assigns.pinned_post_count + 1) |> assign(:pinned_post_count, socket.assigns.pinned_post_count + 1)
{:noreply, updated_socket} {:noreply, updated_socket}
@@ -116,8 +122,8 @@ defmodule ClaperWeb.EventLive.Manage do
def handle_info({:post_unpinned, post}, socket) do def handle_info({:post_unpinned, post}, socket) do
updated_socket = updated_socket =
socket socket
|> update(:all_posts, fn all_posts -> [post | all_posts] end) |> stream_insert(:posts, post)
|> update(:pinned_posts, fn pinned_posts -> [post | pinned_posts] end) |> stream_delete(:pinned_posts, post)
|> assign(:pinned_post_count, socket.assigns.pinned_post_count - 1) |> assign(:pinned_post_count, socket.assigns.pinned_post_count - 1)
{:noreply, updated_socket} {:noreply, updated_socket}
@@ -127,18 +133,22 @@ defmodule ClaperWeb.EventLive.Manage do
def handle_info({:form_submit_created, fs}, socket) do def handle_info({:form_submit_created, fs}, socket) do
{:noreply, {:noreply,
socket socket
|> update(:form_submits, fn form_submits -> [fs | form_submits] end) |> stream_insert(:form_submits, fs)
|> update(:form_submit_count, fn form_submit_count -> form_submit_count + 1 end)
|> push_event("scroll", %{})} |> push_event("scroll", %{})}
end end
@impl true @impl true
def handle_info({:form_submit_updated, fs}, socket) do def handle_info({:form_submit_updated, fs}, socket) do
{:noreply, socket |> update(:form_submits, fn form_submits -> [fs | form_submits] end)} {:noreply, socket |> stream_insert(:form_submits, fs)}
end end
@impl true @impl true
def handle_info({:form_submit_deleted, fs}, socket) do def handle_info({:form_submit_deleted, fs}, socket) do
{:noreply, socket |> update(:form_submits, fn form_submits -> [fs | form_submits] end)} {:noreply,
socket
|> stream_delete(:form_submits, fs)
|> update(:form_submit_count, fn form_submit_count -> form_submit_count - 1 end)}
end end
@impl true @impl true
@@ -218,14 +228,14 @@ defmodule ClaperWeb.EventLive.Manage do
{:noreply, {:noreply,
socket socket
|> put_flash(:info, gettext("Interactions imported successfully")) |> put_flash(:info, gettext("Interactions imported successfully"))
|> redirect(to: Routes.event_manage_path(socket, :show, current_event.code))} |> redirect(to: ~p"/e/#{current_event.code}/manage")}
end end
rescue rescue
Ecto.NoResultsError -> Ecto.NoResultsError ->
{:noreply, {:noreply,
socket socket
|> put_flash(:error, gettext("Interactions import failed")) |> put_flash(:error, gettext("Interactions import failed"))
|> redirect(to: Routes.event_manage_path(socket, :show, current_event.code))} |> redirect(to: ~p"/e/#{current_event.code}/manage")}
end end
end end
@@ -496,6 +506,40 @@ defmodule ClaperWeb.EventLive.Manage do
{:noreply, socket |> assign(:state, new_state)} {:noreply, socket |> assign(:state, new_state)}
end end
@impl true
def handle_event(
"checked",
%{"key" => "message_reaction_enabled", "value" => value},
%{assigns: %{state: state}} = socket
) do
{:ok, new_state} =
Claper.Presentations.update_presentation_state(
state,
%{
:message_reaction_enabled => value
}
)
{:noreply, socket |> assign(:state, new_state)}
end
@impl true
def handle_event(
"checked",
%{"key" => "show_poll_results_enabled", "value" => value},
%{assigns: %{state: state}} = socket
) do
{:ok, new_state} =
Claper.Presentations.update_presentation_state(
state,
%{
:show_poll_results_enabled => value
}
)
{:noreply, socket |> assign(:state, new_state)}
end
@impl true @impl true
def handle_event( def handle_event(
"checked", "checked",
@@ -537,10 +581,13 @@ defmodule ClaperWeb.EventLive.Manage do
updated_socket = updated_socket =
if post.pinned do if post.pinned do
assign(socket, :pinned_posts, list_pinned_posts(socket, socket.assigns.event.uuid)) stream(socket, :pinned_posts, list_pinned_posts(socket, socket.assigns.event.uuid),
assign(socket, :all_posts, list_all_posts(socket, socket.assigns.event.uuid)) reset: true
)
stream(socket, :posts, list_all_posts(socket, socket.assigns.event.uuid), reset: true)
else else
assign(socket, :all_posts, list_all_posts(socket, socket.assigns.event.uuid)) stream(socket, :posts, list_all_posts(socket, socket.assigns.event.uuid), reset: true)
end end
{:noreply, updated_socket} {:noreply, updated_socket}
@@ -567,19 +614,24 @@ defmodule ClaperWeb.EventLive.Manage do
case tab do case tab do
"posts" -> "posts" ->
socket socket
|> assign(:pinned_posts, list_pinned_posts(socket, socket.assigns.event.uuid)) |> stream(:pinned_posts, list_pinned_posts(socket, socket.assigns.event.uuid),
|> assign(:all_posts, list_all_posts(socket, socket.assigns.event.uuid)) reset: true
)
|> stream(:posts, list_all_posts(socket, socket.assigns.event.uuid), reset: true)
"forms" -> "forms" ->
assign( stream(
socket, socket,
:form_submits, :form_submits,
list_form_submits(socket, socket.assigns.event.presentation_file.id) list_form_submits(socket, socket.assigns.event.presentation_file.id),
reset: true
) )
"pinned_posts" -> "pinned_posts" ->
socket socket
|> assign(:pinned_posts, list_pinned_posts(socket, socket.assigns.event.uuid)) |> stream(:pinned_posts, list_pinned_posts(socket, socket.assigns.event.uuid),
reset: true
)
end end
{:noreply, socket} {:noreply, socket}
@@ -590,7 +642,7 @@ defmodule ClaperWeb.EventLive.Manage do
if socket.assigns.create != nil do if socket.assigns.create != nil do
{:noreply, {:noreply,
socket socket
|> push_redirect(to: Routes.event_manage_path(socket, :show, socket.assigns.event.code))} |> push_redirect(to: ~p"/e/#{socket.assigns.event.code}/manage")}
else else
{:noreply, socket} {:noreply, socket}
end end
@@ -788,6 +840,6 @@ defmodule ClaperWeb.EventLive.Manage do
end end
defp list_form_submits(_socket, presentation_file_id) do defp list_form_submits(_socket, presentation_file_id) do
Claper.Forms.list_form_submits(presentation_file_id) Claper.Forms.list_form_submits(presentation_file_id, [:form])
end end
end end

View File

@@ -1,121 +1,11 @@
<div <div
id="manager" id="manager"
class="h-screen max-h-screen flex flex-col"
x-data={"{date: x-data={"{date:
moment.utc('#{@event.expired_at}').local().format('lll')}"} moment.utc('#{@event.expired_at}').local().format('lll')}"}
phx-hook="Manager" phx-hook="Manager"
data-max-page={@event.presentation_file.length} data-max-page={@event.presentation_file.length}
data-current-page={@state.position} data-current-page={@state.position}
> >
<div class="md:flex md:items-center md:justify-between px-6 pb-4 pt-2 h-12 md:h-20 shadow-base absolute top-0 left-0 w-full z-20 bg-white">
<div class="flex-1 min-w-0">
<div class="flex space-x-2">
<a
data-phx-link="patch"
data-phx-link-state="push"
href={Routes.event_index_path(@socket, :index)}
class="md:px-3 pt-0.5 md:pt-1.5 rounded-md hover:bg-gray-200"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
</svg>
</a>
<h2 class="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">
<%= @event.name %>
</h2>
</div>
<div class="hidden mt-1 md:flex flex-col sm:flex-row sm:flex-wrap sm:mt-0 sm:space-x-6">
<div class="mt-2 flex items-center text-sm text-gray-500">
<svg
xmlns="http://www.w3.org/2000/svg"
class="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14"
/>
</svg>
<%= @event.code %>
</div>
<div class="hidden mt-2 md:flex items-center text-sm text-gray-500">
<svg
xmlns="http://www.w3.org/2000/svg"
class="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
clip-rule="evenodd"
/>
</svg>
<%= @attendees_nb %>
</div>
<div class="hidden mt-2 md:flex items-center text-sm text-gray-500">
<svg
class="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"
clip-rule="evenodd"
/>
</svg>
<%= gettext("Finish on") %> <span class="ml-1" x-text="date"></span>
</div>
</div>
</div>
<div class="hidden mt-5 md:flex lg:mt-0 lg:ml-4">
<span class="md:ml-3">
<span class="italic text-gray-400 text-sm mr-5">
<%= raw(
gettext("Press <strong>F</strong> in the presentation window to enable fullscreen")
) %>
</span>
<button
phx-hook="OpenPresenter"
id={"openPresenter-#{@event.uuid}"}
data-url={Routes.event_presenter_path(@socket, :show, @event.code)}
type="button"
class="inline-flex items-center px-5 py-4 border border-transparent rounded-md shadow-sm text-base font-medium text-white bg-gradient-to-tl from-primary-500 to-secondary-500 bg-size-200 bg-pos-0 hover:bg-pos-100 transition-all duration-500"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="-ml-1 mr-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"
/>
</svg>
<%= gettext("Start") %>
</button>
</span>
</div>
</div>
<div <div
id="add-modal" id="add-modal"
class={"#{if !@create, do: 'hidden' } fixed z-30 inset-0 overflow-y-auto p-4 sm:p-6 md:p-24 class={"#{if !@create, do: 'hidden' } fixed z-30 inset-0 overflow-y-auto p-4 sm:p-6 md:p-24
@@ -153,17 +43,17 @@
<a <a
data-phx-link="patch" data-phx-link="patch"
data-phx-link-state="push" data-phx-link-state="push"
href={Routes.event_manage_path(@socket, :add_poll, @event.code)} href={~p"/e/#{@event.code}/manage/add/poll"}
class="group flex select-none rounded-xl p-3 w-full hover:bg-gray-200 cursor-pointer" class="group flex select-none rounded-xl p-3 w-full hover:bg-gray-200 cursor-pointer"
> >
<div class="flex h-12 w-12 flex-none text-white items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-secondary-500"> <div class="flex h-12 w-12 flex-none text-white items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-secondary-500">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-9 w-9" class="h-6 w-6"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
stroke-width="2" stroke-width="1.5"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"
@@ -186,7 +76,7 @@
<a <a
data-phx-link="patch" data-phx-link="patch"
data-phx-link-state="push" data-phx-link-state="push"
href={Routes.event_manage_path(@socket, :add_form, @event.code)} href={~p"/e/#{@event.code}/manage/add/form"}
class="group flex select-none rounded-xl p-3 w-full hover:bg-gray-200 cursor-pointer" class="group flex select-none rounded-xl p-3 w-full hover:bg-gray-200 cursor-pointer"
> >
<div class="flex h-12 w-12 flex-none text-white items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-secondary-500"> <div class="flex h-12 w-12 flex-none text-white items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-secondary-500">
@@ -225,7 +115,7 @@
<a <a
data-phx-link="patch" data-phx-link="patch"
data-phx-link-state="push" data-phx-link-state="push"
href={Routes.event_manage_path(@socket, :add_embed, @event.code)} href={~p"/e/#{@event.code}/manage/add/embed"}
class="group flex select-none rounded-xl p-3 w-full hover:bg-gray-200 cursor-pointer" class="group flex select-none rounded-xl p-3 w-full hover:bg-gray-200 cursor-pointer"
> >
<div class="flex h-12 w-12 flex-none text-white items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-secondary-500"> <div class="flex h-12 w-12 flex-none text-white items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-secondary-500">
@@ -254,12 +144,12 @@
</li> </li>
</ul> </ul>
<%= if (length @polls)==0 && (length @forms)==0 do %> <%= if (length @polls)==0 && (length @forms)==0 && @event.presentation_file.length > 0 do %>
<div class="mt-10"> <div class="mt-10">
<a <a
data-phx-link="patch" data-phx-link="patch"
data-phx-link-state="push" data-phx-link-state="push"
href={Routes.event_manage_path(@socket, :import, @event.code)} href={~p"/e/#{@event.code}/manage/import"}
class="group flex gap-x-2 select-none rounded-xl p-3 w-full hover:bg-gray-200 cursor-pointer" class="group flex gap-x-2 select-none rounded-xl p-3 w-full hover:bg-gray-200 cursor-pointer"
> >
<svg <svg
@@ -301,7 +191,7 @@
poll={@poll} poll={@poll}
live_action={@create_action} live_action={@create_action}
position={@state.position} position={@state.position}
return_to={Routes.event_manage_path(@socket, :show, @event.code)} return_to={~p"/e/#{@event.code}/manage"}
/> />
</div> </div>
<% end %> <% end %>
@@ -322,7 +212,7 @@
form={@form} form={@form}
live_action={@create_action} live_action={@create_action}
position={@state.position} position={@state.position}
return_to={Routes.event_manage_path(@socket, :show, @event.code)} return_to={~p"/e/#{@event.code}/manage"}
/> />
</div> </div>
<% end %> <% end %>
@@ -343,7 +233,7 @@
embed={@embed} embed={@embed}
live_action={@create_action} live_action={@create_action}
position={@state.position} position={@state.position}
return_to={Routes.event_manage_path(@socket, :show, @event.code)} return_to={~p"/e/#{@event.code}/manage"}
/> />
</div> </div>
<% end %> <% end %>
@@ -388,10 +278,129 @@
</div> </div>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 grid-rows-2 md:grid-rows-1 w-full h-screen pt-12 md:pt-20"> <div
<div class="bg-gray-100 pb-10 md:col-span-2 overflow-y-auto"> id="wrapper"
<div class="flex flex-col justify-center items-center text-center"> class="grid grid-rows-[3.2em_1fr] h-screen max-h-screen overflow-hidden"
<%= for index <- 0..@event.presentation_file.length-1 do %> data-next-label={gettext("Next")}
data-prev-label={gettext("Back")}
data-finish-label={gettext("Finish")}
data-group="manage"
phx-hook="TourGuide"
>
<div class="flex items-center justify-between px-3 py-2 border-b-2 border-gray-200 w-full bg-white">
<div class="flex-1 flex gap-x-2">
<a
data-phx-link="patch"
data-phx-link-state="push"
href={~p"/events"}
class="md:px-3 rounded-md hover:bg-gray-200 flex items-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
</svg>
</a>
<div class="flex items-center justify-start gap-x-2">
<h2 class="text-xl font-bold leading-7 text-gray-900 sm:text-2xl sm:truncate">
<%= @event.name %>
</h2>
<div class="flex gap-x-3 items-center">
<div class="flex items-center text-sm text-gray-500">
<svg
xmlns="http://www.w3.org/2000/svg"
class="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14"
/>
</svg>
<%= @event.code %>
</div>
<div class="flex items-center text-sm text-gray-500">
<svg
xmlns="http://www.w3.org/2000/svg"
class="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
clip-rule="evenodd"
/>
</svg>
<%= @attendees_nb %>
</div>
</div>
</div>
</div>
<div>
<div class="flex items-center gap-x-3 justify-end">
<span class="hidden lg:block italic text-gray-400 text-sm">
<%= raw(
gettext("Press <strong>F</strong> in the presentation window to enable fullscreen")
) %>
</span>
<button
phx-hook="OpenPresenter"
id={"openPresenter-#{@event.uuid}"}
data-url={~p"/e/#{@event.code}/presenter"}
type="button"
data-tg-title={"🙌 #{gettext("Time to launch your presentation!")}"}
data-tg-order="4"
data-tg-tour={"<p>#{gettext("Click here to open the presentation window.")}</p>"}
data-tg-group="manage"
class="inline-flex items-center px-5 py-1 border border-transparent rounded-md shadow-sm text-base font-medium text-white bg-gradient-to-tl from-primary-500 to-secondary-500 bg-size-200 bg-pos-0 hover:bg-pos-100 transition-all duration-500"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="-ml-1 mr-2 h-5 w-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
/>
</svg>
<%= gettext("Open presentation") %>
</button>
</div>
</div>
</div>
<div
phx-hook="Split"
data-type="column"
data-gutter=".gutter-1"
id="layout"
class="grid grid-cols-[1fr_10px_1fr] overflow-y-auto"
>
<div
id="slides"
class="bg-gray-100 overflow-y-auto"
data-tg-order="1"
data-tg-title={"#{gettext("Your slides and/or interactions")}"}
data-tg-tour={"<p class='mb-3'>#{gettext("This section contains all your presentation slides (if you have upload one). You have the option to add interactions to each slide.")}</p><p class='opacity-50 text-xs'>#{gettext("If you have slides, you can navigate through the slides with ease using the arrow keys on your keyboard.")}</p>"}
data-tg-group="manage"
>
<div class="flex flex-col items-center text-center">
<%= for index <- 0..max(0, @event.presentation_file.length-1) do %>
<%= if @state.position==index && @state.position> 0 do %> <%= if @state.position==index && @state.position> 0 do %>
<button <button
phx-click="current-page" phx-click="current-page"
@@ -421,6 +430,7 @@
phx-value-page={index} phx-value-page={index}
class="py-4 focus:outline-none" class="py-4 focus:outline-none"
> >
<%= if @event.presentation_file.length > 0 do %>
<%= if Application.get_env(:claper, :presentations) |> Keyword.get(:storage) == "local" do %> <%= if Application.get_env(:claper, :presentations) |> Keyword.get(:storage) == "local" do %>
<img <img
class="w-1/3 mx-auto" class="w-1/3 mx-auto"
@@ -434,15 +444,18 @@
:region)}.amazonaws.com/presentations/#{@event.presentation_file.hash}/#{index+1}.jpg"} :region)}.amazonaws.com/presentations/#{@event.presentation_file.hash}/#{index+1}.jpg"}
/> />
<% end %> <% end %>
<% else %>
<div class="w-screen bg-white h-4"></div>
<% end %>
</button> </button>
<div class="flex flex-col space-y-3 w-full lg:w-1/2 mx-auto justify-start items-center"> <div class="flex flex-col space-y-3 w-full lg:w-1/2 mx-auto justify-start items-center">
<%= for poll <- Enum.filter(@polls, fn poll -> poll.position == index end) do %> <%= for poll <- Enum.filter(@polls, fn poll -> poll.position == index end) do %>
<div class="flex space-x-2 items-center"> <div class="flex space-x-2 items-center">
<div class="flex h-10 w-10 flex-none text-white items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-secondary-500"> <div class="flex h-8 w-8 flex-none text-white items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-secondary-500">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-5 w-5"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -490,7 +503,7 @@
<a <a
data-phx-link="patch" data-phx-link="patch"
data-phx-link-state="push" data-phx-link-state="push"
href={Routes.event_manage_path(@socket, :edit_poll, @event.code, poll.id)} href={~p"/e/#{@event.code}/manage/edit/poll/#{poll.id}"}
class="text-xs text-primary-500" class="text-xs text-primary-500"
> >
<svg <svg
@@ -511,16 +524,16 @@
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="h-10 border border-gray-300"></div> <div class="h-4 border border-gray-300"></div>
<% end %> <% end %>
<%= for form <- Enum.filter(@forms, fn form -> form.position == index end) do %> <%= for form <- Enum.filter(@forms, fn form -> form.position == index end) do %>
<div class="flex space-x-2 items-center"> <div class="flex space-x-2 items-center">
<div class="flex h-10 w-10 flex-none text-white items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-secondary-500"> <div class="flex h-8 w-8 flex-none text-white items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-secondary-500">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-forms" class="icon icon-tabler icon-tabler-forms"
width="24" width="20"
height="24" height="20"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@@ -537,7 +550,7 @@
<path d="M13 12h.01"></path> <path d="M13 12h.01"></path>
</svg> </svg>
</div> </div>
<div class="flex space-x-2"> <div class="flex space-x-2 items-center">
<span> <span>
<span class="font-semibold"> <span class="font-semibold">
<%= gettext "Form" %> <%= gettext "Form" %>
@@ -572,7 +585,7 @@
<a <a
data-phx-link="patch" data-phx-link="patch"
data-phx-link-state="push" data-phx-link-state="push"
href={Routes.event_manage_path(@socket, :edit_form, @event.code, form.id)} href={~p"/e/#{@event.code}/manage/edit/form/#{form.id}"}
class="text-xs text-primary-500" class="text-xs text-primary-500"
> >
<svg <svg
@@ -593,14 +606,14 @@
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="h-10 border border-gray-300"></div> <div class="h-4 border border-gray-300"></div>
<% end %> <% end %>
<%= for embed<- Enum.filter(@embeds, fn embed -> embed.position == index end) do %> <%= for embed<- Enum.filter(@embeds, fn embed -> embed.position == index end) do %>
<div class="flex space-x-2 items-center"> <div class="flex space-x-2 items-center">
<div class="flex h-10 w-10 flex-none text-white items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-secondary-500"> <div class="flex h-8 w-8 flex-none text-white items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-secondary-500">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-5 w-5"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -646,9 +659,7 @@
<a <a
data-phx-link="patch" data-phx-link="patch"
data-phx-link-state="push" data-phx-link-state="push"
href={ href={~p"/e/#{@event.code}/manage/edit/embed/#{embed.id}"}
Routes.event_manage_path(@socket, :edit_embed, @event.code, embed.id)
}
class="text-xs text-primary-500" class="text-xs text-primary-500"
> >
<svg <svg
@@ -669,13 +680,24 @@
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="h-10 border border-gray-300"></div> <div class="h-4 border border-gray-300"></div>
<% end %> <% end %>
</div> </div>
<%= if @state.position==index do %> <%= if @state.position==index do %>
<button class="underline" phx-click={toggle_add_modal()}> <button
<%= gettext("Add interaction") %> class="flex items-center justify-center px-3 py-2 text-white bg-primary-500 hover:bg-primary-600 rounded-md my-5 mx-auto text-xs"
phx-click={toggle_add_modal()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" />
</svg>
<span><%= gettext("Add interaction") %></span>
</button> </button>
<% end %> <% end %>
</div> </div>
@@ -702,16 +724,39 @@
</div> </div>
</div> </div>
<div class="grid grid-cols-1 grid-rows-2 md:grid-rows-3" style="height: 100%;"> <div
<div class="bg-gray-200 md:row-span-2 border-2"> class="gutter-1 row-span-full cursor-col-resize col-[2] bg-gray-50 text-center text-gray-300 text-sm leading-3"
style="writing-mode: vertical-rl"
>
•••
</div>
<div
id="ugc"
phx-hook="Split"
data-gutter=".gutter-2"
data-type="row"
class="grid grid-rows-[1fr_10px_1fr] overflow-y-auto"
>
<div
class="bg-gray-200 border-2 overflow-y-auto relative"
data-tg-title={"#{gettext("Attendees interactions")}"}
data-tg-order="2"
data-tg-tour={"<p class='mb-3'>#{gettext("Here you'll find all interactions from your attendees. You can manage messages, pinned messages, and submitted forms.")}</p><p class='opacity-50 text-xs'>#{gettext("Identify users by their unique avatars.")}</p>"}
data-tg-group="manage"
>
<ul <ul
id="menu" id="menu"
phx-update="replace" phx-update="replace"
class="fixed z-20 flex items-center bg-gray-200 space-x-3 px-2 w-full py-2" class="fixed z-10 flex items-center bg-gray-200 space-x-3 px-2 w-full py-3"
> >
<li class={"rounded-md #{if @list_tab==:posts, do: 'bg-secondary-600 text-white' , <li class={"rounded-md #{if @list_tab==:posts, do: 'bg-secondary-600 text-white' ,
else: 'bg-white text-gray-600' } px-2 py-0.5 text-sm shadow-sm"}> else: 'bg-white text-gray-600' } px-2 py-0.5 text-sm shadow-sm"}>
<%= link(gettext("Messages"), to: "#", phx_click: "list-tab", phx_value_tab: :posts) %> <%= link(gettext("Messages") <> " (#{@post_count})",
to: "#",
phx_click: "list-tab",
phx_value_tab: :posts
) %>
</li> </li>
<li class={"rounded-md #{if @list_tab==:pinned_posts, do: 'bg-secondary-600 text-white' , <li class={"rounded-md #{if @list_tab==:pinned_posts, do: 'bg-secondary-600 text-white' ,
else: 'bg-white text-gray-600' } px-2 py-0.5 text-sm shadow-sm"}> else: 'bg-white text-gray-600' } px-2 py-0.5 text-sm shadow-sm"}>
@@ -723,7 +768,7 @@
</li> </li>
<li class={"rounded-md #{if @list_tab==:forms, do: 'bg-secondary-600 text-white' , <li class={"rounded-md #{if @list_tab==:forms, do: 'bg-secondary-600 text-white' ,
else: 'bg-white text-gray-600' } px-2 py-0.5 text-sm shadow-sm"}> else: 'bg-white text-gray-600' } px-2 py-0.5 text-sm shadow-sm"}>
<%= link(gettext("Form submissions"), <%= link(gettext("Form submissions") <> " (#{@form_submit_count})",
to: "#", to: "#",
phx_click: "list-tab", phx_click: "list-tab",
phx_value_tab: :forms phx_value_tab: :forms
@@ -731,8 +776,8 @@
</li> </li>
</ul> </ul>
<%= if @list_tab==:posts do %> <%= if @list_tab == :posts do %>
<%= if Enum.count(@all_posts)==0 && Enum.count(@pinned_posts)==0 do %> <%= if @post_count == 0 && @pinned_post_count == 0 do %>
<div <div
class="text-center flex flex-col space-y-5 items-center justify-center text-gray-400" class="text-center flex flex-col space-y-5 items-center justify-center text-gray-400"
style="height: 100%;" style="height: 100%;"
@@ -757,24 +802,15 @@
</p> </p>
</div> </div>
<% end %> <% end %>
<div <div id="x" class="overflow-y-auto max-h-full">
id="x"
class={"overflow-y-auto #{if Enum.any?(@pinned_posts) or Enum.any?(@all_posts), do: 'h-full', else: 'h-1/2'}"}
>
<div <div
id="post-list" id="post-list"
class={"overflow-y-auto #{if Enum.count(@all_posts)> 0, do: ''} pb-5 pt-8 px-5"} class="overflow-y-auto max-h-full pb-5 pt-8 px-5"
phx-update="append" phx-update="stream"
data-posts-nb={Enum.count(@all_posts)}
phx-hook="ScrollIntoDiv" phx-hook="ScrollIntoDiv"
data-target="#post-list"
> >
<%= for post <- @all_posts do %> <div :for={{id, post} <- @streams.posts} id={id}>
<div <div class="flex flex-col md:block px-4 pb-2 pt-3 rounded-b-lg rounded-tr-lg bg-white relative shadow-md text-black break-all mt-4">
class={if post.__meta__.state == :deleted, do: "hidden"}
id={"#{post.id}-post"}
>
<div class="px-4 pb-2 pt-3 rounded-b-lg rounded-tr-lg bg-white relative shadow-md text-black break-all mt-4">
<div class="float-right mr-1"> <div class="float-right mr-1">
<%= if post.attendee_identifier do %> <%= if post.attendee_identifier do %>
<span class="text-yellow-500"> <span class="text-yellow-500">
@@ -901,13 +937,12 @@
<% end %> <% end %>
</div> </div>
</div> </div>
<% end %>
</div> </div>
<!-- The div below encompasses the 2 post lists --> <!-- The div below encompasses the 2 post lists -->
</div> </div>
<% end %> <% end %>
<%= if @list_tab == :pinned_posts do %> <%= if @list_tab == :pinned_posts do %>
<%= if Enum.count(@pinned_posts) == 0 do %> <%= if @pinned_post_count == 0 do %>
<div <div
class="text-center flex flex-col space-y-5 items-center justify-center text-gray-400" class="text-center flex flex-col space-y-5 items-center justify-center text-gray-400"
style="height: 100%;" style="height: 100%;"
@@ -931,24 +966,15 @@
</div> </div>
<% end %> <% end %>
<div <div id="x-pinned" class="overflow-y-auto max-h-full">
id="x-pinned" <%= if @pinned_post_count > 0 do %>
class={"overflow-y-auto #{if Enum.any?(@pinned_posts), do: 'h-full', else: 'h-1/2'}"}
>
<%= if Enum.any?(@pinned_posts) do %>
<div <div
id="pinned-post-list" id="pinned-post-list"
class="overflow-y-auto pb-5 pt-8 px-5" class="overflow-y-auto pb-5 pt-8 px-5"
phx-update="append" phx-update="stream"
data-posts-nb={Enum.count(@pinned_posts)}
phx-hook="ScrollIntoDiv" phx-hook="ScrollIntoDiv"
data-target="#pinned-post-list"
>
<%= for post <- @pinned_posts do %>
<div
class={if post.__meta__.state == :deleted || !post.pinned, do: "hidden"}
id={"#{post.id}-post"}
> >
<div :for={{id, post} <- @streams.pinned_posts} id={id}>
<div class="px-4 pb-2 pt-3 rounded-b-lg rounded-tr-lg bg-white relative shadow-md text-black break-all mt-4"> <div class="px-4 pb-2 pt-3 rounded-b-lg rounded-tr-lg bg-white relative shadow-md text-black break-all mt-4">
<div class="float-right mr-1"> <div class="float-right mr-1">
<%= if post.attendee_identifier do %> <%= if post.attendee_identifier do %>
@@ -1076,14 +1102,13 @@
<% end %> <% end %>
</div> </div>
</div> </div>
<% end %>
</div> </div>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<%= if @list_tab==:forms do %> <%= if @list_tab == :forms do %>
<%= if Enum.count(@form_submits)==0 do %> <%= if @form_submit_count == 0 do %>
<div class="text-center h-full flex flex-col space-y-5 items-center justify-center text-gray-400"> <div class="text-center h-full flex flex-col space-y-5 items-center justify-center text-gray-400">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -1111,18 +1136,12 @@
<% end %> <% end %>
<div <div
id="form-list" id="form-list"
class={"overflow-y-auto #{if Enum.count(@form_submits)> 0, do: 'max-h-full'} class="overflow-y-auto max-h-full pb-5 pt-8 px-5"
pb-5 pt-8 px-5"} phx-update="stream"
phx-update="append" data-forms-nb={@form_submit_count}
data-forms-nb={Enum.count(@form_submits)}
phx-hook="ScrollIntoDiv" phx-hook="ScrollIntoDiv"
data-target="#form-list"
>
<%= for submission <- @form_submits do %>
<div
class={if submission.__meta__.state == :deleted, do: "hidden"}
id={"#{submission.id}-form"}
> >
<div :for={{id, submission} <- @streams.form_submits} id={id}>
<div class="px-4 pb-2 pt-3 rounded-b-lg rounded-tr-lg bg-white relative shadow-md text-black break-all mt-4"> <div class="px-4 pb-2 pt-3 rounded-b-lg rounded-tr-lg bg-white relative shadow-md text-black break-all mt-4">
<div class="float-right mr-1"> <div class="float-right mr-1">
<span class="text-red-500"> <span class="text-red-500">
@@ -1136,6 +1155,12 @@
</span> </span>
</div> </div>
<p>
<span class="font-semibold text-lg">
<%= gettext("Form") %>
</span>: <%= submission.form.title %>
</p>
<div class="flex space-x-3 items-center"> <div class="flex space-x-3 items-center">
<%= if submission.attendee_identifier do %> <%= if submission.attendee_identifier do %>
<img <img
@@ -1162,16 +1187,24 @@
</div> </div>
</div> </div>
</div> </div>
<% end %>
</div> </div>
<% end %> <% end %>
</div> </div>
<div class="w-full shadow-lg"> <div class="gutter-2 col-span-full cursor-row-resize z-20 row-[2] bg-gray-50 text-center text-gray-300 text-sm leading-3">
<div class="px-5 py-3 grid grid-cols-1 lg:grid-cols-2"> •••
</div>
<div
class="px-5 py-3 grid grid-cols-1 lg:grid-cols-2 z-20 bg-white"
data-tg-title={"#{gettext("Settings")}"}
data-tg-order="3"
data-tg-tour={"<p class='mb-3'>#{gettext("You can control each setting for the presentation (showing on the big screen) and on the attendee's room.")}</p><p class='opacity-50 text-xs'>#{gettext("Use the associated keyboard shortcuts for quick toggling of these settings.")}</p>"}
data-tg-group="manage"
>
<div> <div>
<span class="font-semibold text-lg"> <span class="font-semibold text-lg">
<%= gettext("On screen settings") %> <%= gettext("Presentation settings") %>
</span> </span>
<div class="flex space-x-2 items-center mt-3"> <div class="flex space-x-2 items-center mt-3">
@@ -1180,7 +1213,12 @@
checked={@state.join_screen_visible} checked={@state.join_screen_visible}
shortcut={if @create == nil, do: "Q", else: nil} shortcut={if @create == nil, do: "Q", else: nil}
/> />
<span><%= gettext("Show instructions") %><code class="pl-1">(Q)</code></span> <span>
<%= gettext("Show instructions") %>
<code class="px-2 py-1.5 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg">
q
</code>
</span>
</div> </div>
<div class="flex space-x-2 items-center mt-3"> <div class="flex space-x-2 items-center mt-3">
@@ -1189,7 +1227,12 @@
checked={@state.chat_visible} checked={@state.chat_visible}
shortcut={if @create == nil, do: "W", else: nil} shortcut={if @create == nil, do: "W", else: nil}
/> />
<span><%= gettext("Show messages") %><code class="pl-1">(W)</code></span> <span>
<%= gettext("Show messages") %>
<code class="px-2 py-1.5 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg">
w
</code>
</span>
</div> </div>
<div class="flex space-x-2 items-center mt-3"> <div class="flex space-x-2 items-center mt-3">
@@ -1199,7 +1242,10 @@
shortcut={if @create == nil, do: "E", else: nil} shortcut={if @create == nil, do: "E", else: nil}
/> />
<span> <span>
<%= gettext("Show only pinned messages") %><code class="pl-1">(E)</code> <%= gettext("Show only pinned messages") %>
<code class="px-2 py-1.5 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg">
e
</code>
</span> </span>
</div> </div>
@@ -1210,7 +1256,12 @@
checked={@state.poll_visible} checked={@state.poll_visible}
shortcut={if @create == nil, do: "R", else: nil} shortcut={if @create == nil, do: "R", else: nil}
/> />
<span><%= gettext("Show poll results") %><code class="pl-1">(R)</code></span> <span>
<%= gettext("Show poll results") %>
<code class="px-2 py-1.5 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg">
r
</code>
</span>
</div> </div>
</div> </div>
@@ -1225,7 +1276,12 @@
checked={@state.chat_enabled} checked={@state.chat_enabled}
shortcut={if @create == nil, do: "A", else: nil} shortcut={if @create == nil, do: "A", else: nil}
/> />
<span><%= gettext("Enable messages") %><code class="pl-1">(A)</code></span> <span>
<%= gettext("Enable messages") %>
<code class="px-2 py-1.5 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg">
a
</code>
</span>
</div> </div>
<div class="flex space-x-2 items-center mt-3"> <div class="flex space-x-2 items-center mt-3">
@@ -1235,7 +1291,38 @@
shortcut={if @create == nil, do: "S", else: nil} shortcut={if @create == nil, do: "S", else: nil}
/> />
<span> <span>
<%= gettext("Enable anonymous messages") %><code class="pl-1">(S)</code> <%= gettext("Enable anonymous messages") %>
<code class="px-2 py-1.5 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg">
s
</code>
</span>
</div>
<div class="flex space-x-2 items-center mt-3">
<ClaperWeb.Component.Input.check
key={:message_reaction_enabled}
checked={@state.message_reaction_enabled}
shortcut={if @create == nil, do: "D", else: nil}
/>
<span>
<%= gettext("Enable message reactions") %>
<code class="px-2 py-1.5 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg">
d
</code>
</span>
</div>
<div class="flex space-x-2 items-center mt-3">
<ClaperWeb.Component.Input.check
key={:show_poll_results_enabled}
checked={@state.show_poll_results_enabled}
shortcut={if @create == nil, do: "F", else: nil}
/>
<span>
<%= gettext("Show poll results") %>
<code class="px-2 py-1.5 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg">
f
</code>
</span> </span>
</div> </div>
</div> </div>

View File

@@ -59,7 +59,7 @@ defmodule ClaperWeb.EventLive.PollComponent do
<%= if (length @current_poll_vote) > 0 do %> <%= if (length @current_poll_vote) > 0 do %>
<button class="bg-gray-500 px-3 py-2 rounded-full flex justify-between items-center relative text-white"> <button class="bg-gray-500 px-3 py-2 rounded-full flex justify-between items-center relative text-white">
<div <div
style={"width: #{opt.percentage}%;"} style={"width: #{if @show_results, do: opt.percentage, else: 0}%;"}
class={"bg-gradient-to-r from-primary-500 to-secondary-500 h-full absolute left-0 transition-all rounded-l-full #{if opt.percentage == "100", do: 'rounded-r-full'}"} class={"bg-gradient-to-r from-primary-500 to-secondary-500 h-full absolute left-0 transition-all rounded-l-full #{if opt.percentage == "100", do: 'rounded-r-full'}"}
> >
</div> </div>
@@ -80,7 +80,9 @@ defmodule ClaperWeb.EventLive.PollComponent do
<% end %> <% end %>
<span class="flex-1"><%= opt.content %></span> <span class="flex-1"><%= opt.content %></span>
</div> </div>
<span class="text-sm z-10"><%= opt.percentage %>% (<%= opt.vote_count %>)</span> <span :if={@show_results} class="text-sm z-10">
<%= opt.percentage %>% (<%= opt.vote_count %>)
</span>
</button> </button>
<% else %> <% else %>
<button <button
@@ -90,7 +92,7 @@ defmodule ClaperWeb.EventLive.PollComponent do
class="bg-gray-500 px-3 py-2 rounded-full flex justify-between items-center relative text-white" class="bg-gray-500 px-3 py-2 rounded-full flex justify-between items-center relative text-white"
> >
<div <div
style={"width: #{opt.percentage}%;"} style={"width: #{if @show_results, do: opt.percentage, else: 0}%;"}
class={"bg-gradient-to-r from-primary-500 to-secondary-500 h-full absolute left-0 transition-all rounded-l-full #{if opt.percentage == "100", do: 'rounded-r-full'}"} class={"bg-gradient-to-r from-primary-500 to-secondary-500 h-full absolute left-0 transition-all rounded-l-full #{if opt.percentage == "100", do: 'rounded-r-full'}"}
> >
</div> </div>
@@ -111,7 +113,9 @@ defmodule ClaperWeb.EventLive.PollComponent do
<% end %> <% end %>
<span class="flex-1"><%= opt.content %></span> <span class="flex-1"><%= opt.content %></span>
</div> </div>
<span class="text-sm z-10"><%= opt.percentage %>% (<%= opt.vote_count %>)</span> <span :if={@show_results} class="text-sm z-10">
<%= opt.percentage %>% (<%= opt.vote_count %>)
</span>
</button> </button>
<% end %> <% end %>
<% end %> <% end %>

View File

@@ -3,7 +3,7 @@ defmodule ClaperWeb.EventLive.PostComponent do
def render(assigns) do def render(assigns) do
~H""" ~H"""
<div id={"post-#{@post.uuid}"} class={if @post.__meta__.state == :deleted, do: "hidden"}> <div id={@id}>
<%= if @post.attendee_identifier == @attendee_identifier || (not is_nil(@current_user) && @post.user_id == @current_user.id) do %> <%= if @post.attendee_identifier == @attendee_identifier || (not is_nil(@current_user) && @post.user_id == @current_user.id) do %>
<div class="px-4 pt-3 pb-8 rounded-b-lg rounded-tl-lg bg-gray-700 text-white relative z-0 break-word"> <div class="px-4 pt-3 pb-8 rounded-b-lg rounded-tl-lg bg-gray-700 text-white relative z-0 break-word">
<button <button
@@ -179,6 +179,7 @@ defmodule ClaperWeb.EventLive.PostComponent do
<p><%= @post.body %></p> <p><%= @post.body %></p>
<div class="flex h-6 text-xs float-right space-x-2"> <div class="flex h-6 text-xs float-right space-x-2">
<%= if @reaction_enabled do %>
<%= if not Enum.member?(@liked_posts, @post.id) do %> <%= if not Enum.member?(@liked_posts, @post.id) do %>
<button <button
phx-click="react" phx-click="react"
@@ -256,6 +257,7 @@ defmodule ClaperWeb.EventLive.PostComponent do
<% end %> <% end %>
</button> </button>
<% end %> <% end %>
<% end %>
</div> </div>
</div> </div>
<% end %> <% end %>

View File

@@ -88,7 +88,7 @@
class="w-full min-h-screen flex items-center justify-center relative bg-black" class="w-full min-h-screen flex items-center justify-center relative bg-black"
> >
<div <div
class={"#{if @state.chat_visible, do: 'opacity-100 w-3/12 px-4 showed', else: 'opacity-0 w-0 p-0'} transition-all duration-150 flex flex-col h-screen py-5 justify-end max-h-screen bg-black"} class={"#{if @state.chat_visible, do: (if @event.presentation_file.length > 0, do: 'opacity-100 w-3/12 px-4 showed', else: 'opacity-100 w-2/3 px-4 showed'), else: 'opacity-0 w-0 p-0'} transition-all duration-150 flex flex-col h-screen py-5 justify-end max-h-screen bg-black"}
id="post-list-wrapper" id="post-list-wrapper"
phx-update="replace" phx-update="replace"
> >
@@ -168,7 +168,8 @@
</div> </div>
<!-- SLIDES --> <!-- SLIDES -->
<div id="slider" phx-update="ignore"> <div id="slider" phx-update="ignore">
<%= for index <- 1..@event.presentation_file.length do %> <%= for index <- 1..max(1, @event.presentation_file.length) do %>
<%= if @event.presentation_file.length > 0 do %>
<%= if Application.get_env(:claper, :presentations) |> Keyword.get(:storage) == "local" do %> <%= if Application.get_env(:claper, :presentations) |> Keyword.get(:storage) == "local" do %>
<img <img
class="w-1/3 max-h-screen mx-auto" class="w-1/3 max-h-screen mx-auto"
@@ -181,6 +182,7 @@
/> />
<% end %> <% end %>
<% end %> <% end %>
<% end %>
</div> </div>
</div> </div>
<!-- ONLINE BADGE --> <!-- ONLINE BADGE -->

View File

@@ -70,11 +70,12 @@ defmodule ClaperWeb.EventLive.Show do
maybe_update_audience_peak(event, online) maybe_update_audience_peak(event, online)
posts = list_posts(socket, event.uuid)
socket = socket =
socket socket
|> assign(:attendees_nb, 1) |> assign(:attendees_nb, 1)
|> assign(:post_changeset, post_changeset) |> assign(:post_changeset, post_changeset)
|> assign(:posts, list_posts(socket, event.uuid))
|> assign(:liked_posts, reacted_posts(socket, event.id, "👍")) |> assign(:liked_posts, reacted_posts(socket, event.id, "👍"))
|> assign(:loved_posts, reacted_posts(socket, event.id, "❤️")) |> assign(:loved_posts, reacted_posts(socket, event.id, "❤️"))
|> assign(:loled_posts, reacted_posts(socket, event.id, "😂")) |> assign(:loled_posts, reacted_posts(socket, event.id, "😂"))
@@ -83,6 +84,8 @@ defmodule ClaperWeb.EventLive.Show do
|> assign(:event, event) |> assign(:event, event)
|> assign(:state, event.presentation_file.presentation_state) |> assign(:state, event.presentation_file.presentation_state)
|> assign(:nickname, "") |> assign(:nickname, "")
|> stream(:posts, posts)
|> assign(:post_count, Enum.count(posts))
|> starting_soon_assigns(event) |> starting_soon_assigns(event)
|> get_current_poll(event) |> get_current_poll(event)
|> get_current_form(event) |> get_current_form(event)
@@ -90,8 +93,7 @@ defmodule ClaperWeb.EventLive.Show do
|> check_leader(event) |> check_leader(event)
|> leader_list(event) |> leader_list(event)
{:ok, socket |> assign(:empty_room, Enum.empty?(socket.assigns.posts)), {:ok, socket}
temporary_assigns: [posts: []]}
end end
defp leader_list(socket, event) do defp leader_list(socket, event) do
@@ -145,9 +147,7 @@ defmodule ClaperWeb.EventLive.Show do
def handle_info(:tick, %{assigns: %{diff: 0}} = socket) do def handle_info(:tick, %{assigns: %{diff: 0}} = socket) do
{:noreply, {:noreply,
socket socket
|> redirect( |> redirect(to: ~p"/e/#{String.downcase(socket.assigns.event.code)}")}
to: Routes.event_show_path(socket, :show, String.downcase(socket.assigns.event.code))
)}
end end
@impl true @impl true
@@ -174,16 +174,24 @@ defmodule ClaperWeb.EventLive.Show do
def handle_info({:post_created, post}, socket) do def handle_info({:post_created, post}, socket) do
{:noreply, {:noreply,
socket socket
|> update(:posts, fn posts -> [post | posts] end) |> stream_insert(:posts, post)
|> push_event("scroll", %{}) |> update(:post_count, fn count -> count + 1 end)}
|> maybe_disable_empty_room}
end end
@impl true @impl true
def handle_info({:state_updated, presentation_state}, socket) do def handle_info({:state_updated, presentation_state}, socket) do
{:noreply, {:noreply,
socket socket
|> assign(:state, presentation_state)} |> assign(:state, presentation_state)
|> stream(:posts, list_posts(socket, socket.assigns.event.uuid), reset: true)}
end
@impl true
def handle_info({:event_terminated, _event}, socket) do
{:noreply,
socket
|> put_flash(:error, gettext("This event has been terminated"))
|> push_redirect(to: ~p"/")}
end end
@impl true @impl true
@@ -196,7 +204,7 @@ defmodule ClaperWeb.EventLive.Show do
{:noreply, {:noreply,
socket socket
|> put_flash(:error, gettext("You have been banned from this event")) |> put_flash(:error, gettext("You have been banned from this event"))
|> push_redirect(to: Routes.event_join_path(socket, :index))} |> push_redirect(to: ~p"/")}
else else
{:noreply, socket} {:noreply, socket}
end end
@@ -211,7 +219,7 @@ defmodule ClaperWeb.EventLive.Show do
{:noreply, {:noreply,
socket socket
|> put_flash(:error, gettext("You have been banned from this event")) |> put_flash(:error, gettext("You have been banned from this event"))
|> push_redirect(to: Routes.event_join_path(socket, :index))} |> push_redirect(to: ~p"/")}
else else
{:noreply, socket} {:noreply, socket}
end end
@@ -256,32 +264,35 @@ defmodule ClaperWeb.EventLive.Show do
@impl true @impl true
def handle_info({:post_updated, post}, socket) do def handle_info({:post_updated, post}, socket) do
{:noreply, socket |> update(:posts, fn posts -> [post | posts] end)} {:noreply, socket |> stream_insert(:posts, post)}
end end
@impl true @impl true
def handle_info({:post_pinned, post}, socket) do def handle_info({:post_pinned, post}, socket) do
{:noreply, socket |> update(:posts, fn posts -> [post | posts] end)} {:noreply, socket |> stream_insert(:posts, post)}
end end
@impl true @impl true
def handle_info({:post_unpinned, post}, socket) do def handle_info({:post_unpinned, post}, socket) do
{:noreply, socket |> update(:posts, fn posts -> [post | posts] end)} {:noreply, socket |> stream_insert(:posts, post)}
end end
@impl true @impl true
def handle_info({:reaction_added, post}, socket) do def handle_info({:reaction_added, post}, socket) do
{:noreply, socket |> update(:posts, fn posts -> [post | posts] end)} {:noreply, socket |> stream_insert(:posts, post)}
end end
@impl true @impl true
def handle_info({:reaction_removed, post}, socket) do def handle_info({:reaction_removed, post}, socket) do
{:noreply, socket |> update(:posts, fn posts -> [post | posts] end)} {:noreply, socket |> stream_insert(:posts, post)}
end end
@impl true @impl true
def handle_info({:post_deleted, post}, socket) do def handle_info({:post_deleted, post}, socket) do
{:noreply, socket |> update(:posts, fn posts -> [post | posts] end)} {:noreply,
socket
|> stream_delete(:posts, post)
|> update(:post_count, fn count -> count - 1 end)}
end end
@impl true @impl true
@@ -754,8 +765,4 @@ defmodule ClaperWeb.EventLive.Show do
socket socket
|> assign(:page_title, "##{socket.assigns.event.code} - #{socket.assigns.event.name}") |> assign(:page_title, "##{socket.assigns.event.code} - #{socket.assigns.event.name}")
end end
defp maybe_disable_empty_room(%{assigns: %{empty_room: empty_room}} = socket) do
if empty_room, do: assign(socket, :empty_room, false), else: socket
end
end end

View File

@@ -20,7 +20,7 @@
<a <a
class="flex items-center px-3 py-2 bg-gray-200 mb-15 rounded-lg mt-5" class="flex items-center px-3 py-2 bg-gray-200 mb-15 rounded-lg mt-5"
href={Routes.event_join_path(@socket, :index, %{disconnected_from: @event.uuid})} href={~p"/?disconnected_from=#{@event.uuid}"}
> >
<img src="/images/icons/exit-outline.svg" class="h-5 mr-3" /> <img src="/images/icons/exit-outline.svg" class="h-5 mr-3" />
<span><%= gettext("Leave") %></span> <span><%= gettext("Leave") %></span>
@@ -33,6 +33,12 @@
class="w-full bg-black fixed z-10 lg:w-1/3" class="w-full bg-black fixed z-10 lg:w-1/3"
style="box-shadow: 0px 15px 14px 1px rgba(0,0,0,0.75); -webkit-box-shadow: 0px 15px 14px 1px rgba(0,0,0,0.75); -moz-box-shadow: 0px 15px 14px 1px rgba(0,0,0,0.75);" style="box-shadow: 0px 15px 14px 1px rgba(0,0,0,0.75); -webkit-box-shadow: 0px 15px 14px 1px rgba(0,0,0,0.75); -moz-box-shadow: 0px 15px 14px 1px rgba(0,0,0,0.75);"
> >
<div id="banner" class="hidden w-full bg-gray-800 text-center" phx-hook="EmbeddedBanner">
<a href="https://claper.co" target="_blank" class="text-xs text-white py-3 w-full">
<%= gettext("Create your next presentation with") %>
<span class="underline">Claper</span>
</a>
</div>
<div class="flex justify-between items-center px-5 py-3"> <div class="flex justify-between items-center px-5 py-3">
<button <button
phx-click={toggle_side_menu()} phx-click={toggle_side_menu()}
@@ -64,6 +70,7 @@
event={@event} event={@event}
selected_poll_opt={@selected_poll_opt} selected_poll_opt={@selected_poll_opt}
current_poll_vote={@current_poll_vote} current_poll_vote={@current_poll_vote}
show_results={@state.show_poll_results_enabled}
/> />
</div> </div>
</div> </div>
@@ -109,29 +116,29 @@
<div <div
class="flex flex-col space-y-4 px-5 pt-20 pb-32 lg:w-1/3 bg-black min-h-screen" class="flex flex-col space-y-4 px-5 pt-20 pb-32 lg:w-1/3 bg-black min-h-screen"
id="post-list" id="post-list"
phx-update="append" phx-update="stream"
data-posts-nb={Enum.count(@posts)} data-posts-nb={Enum.count(@streams.posts)}
phx-hook="Scroll" phx-hook="Scroll"
data-target="body" data-target="body"
> >
<%= for post <- @posts do %>
<.live_component <.live_component
:for={{id, post} <- @streams.posts}
module={ClaperWeb.EventLive.PostComponent} module={ClaperWeb.EventLive.PostComponent}
id={"#{post.id}-post"} id={id}
post={post} post={post}
leaders={@leaders} leaders={@leaders}
is_leader={@is_leader} is_leader={@is_leader}
current_user={@current_user} current_user={@current_user}
attendee_identifier={@attendee_identifier} attendee_identifier={@attendee_identifier}
event={@event} event={@event}
reaction_enabled={@state.message_reaction_enabled}
liked_posts={@liked_posts} liked_posts={@liked_posts}
loved_posts={@loved_posts} loved_posts={@loved_posts}
loled_posts={@loled_posts} loled_posts={@loled_posts}
/> />
<% end %>
</div> </div>
<%= if @empty_room && @state.chat_enabled do %> <%= if @post_count == 0 && @state.chat_enabled do %>
<div class="text-2xl text-white block fixed bottom-32 left-0 w-full lg:w-1/3 lg:left-1/2 lg:transform lg:-translate-x-1/2 text-center opacity-30"> <div class="text-2xl text-white block fixed bottom-32 left-0 w-full lg:w-1/3 lg:left-1/2 lg:transform lg:-translate-x-1/2 text-center opacity-30">
<span><%= gettext("Be the first to react !") %></span> <span><%= gettext("Be the first to react !") %></span>
<img src="/images/icons/arrow-white.svg" class="h-24 rotate-180 ml-12 mt-8" /> <img src="/images/icons/arrow-white.svg" class="h-24 rotate-180 ml-12 mt-8" />

View File

@@ -1,23 +1,2 @@
defmodule ClaperWeb.LiveHelpers do defmodule ClaperWeb.LiveHelpers do
import Phoenix.LiveView.Helpers
@doc """
Renders a component inside the `ClaperWeb.ModalComponent` component.
The rendered modal receives a `:return_to` option to properly update
the URL when the modal is closed.
## Examples
<%= live_modal ClaperWeb.PostLive.FormComponent,
id: @post.id || :new,
action: @live_action,
post: @post,
return_to: Routes.post_index_path(@socket, :index) %>
"""
def live_modal(component, opts) do
path = Keyword.fetch!(opts, :return_to)
modal_opts = [id: :modal, return_to: path, component: component, opts: opts]
live_component(ClaperWeb.ModalComponent, modal_opts)
end
end end

View File

@@ -1,4 +1,4 @@
<div class="mx-3 max-w-7xl sm:mx-auto"> <div class="mx-3 md:max-w-3xl lg:max-w-5xl md:mx-auto">
<div class="border-b border-gray-200 py-4 flex flex-col sm:flex-row sm:items-center justify-between"> <div class="border-b border-gray-200 py-4 flex flex-col sm:flex-row sm:items-center justify-between">
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<h1 class="text-2xl font-medium leading-6 text-gray-900 sm:truncate"> <h1 class="text-2xl font-medium leading-6 text-gray-900 sm:truncate">
@@ -157,8 +157,9 @@
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4"> <h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
<%= gettext("Interactions history") %> <%= gettext("Interactions history") %>
</h3> </h3>
<%= for position <- 0..@event.presentation_file.length-1 do %> <%= for position <- 0..max(0, @event.presentation_file.length-1) do %>
<div class="my-10"> <div class="my-10">
<%= if @event.presentation_file.length > 0 do %>
<%= if Application.get_env(:claper, :presentations) |> Keyword.get(:storage) == "local" do %> <%= if Application.get_env(:claper, :presentations) |> Keyword.get(:storage) == "local" do %>
<img <img
class="w-1/3 mx-auto" class="w-1/3 mx-auto"
@@ -170,6 +171,7 @@
src={"https://#{Application.get_env(:claper, :presentations) |> Keyword.get(:aws_bucket)}.s3.#{Application.get_env(:ex_aws, :region)}.amazonaws.com/presentations/#{@event.presentation_file.hash}/#{position+1}.jpg"} src={"https://#{Application.get_env(:claper, :presentations) |> Keyword.get(:aws_bucket)}.s3.#{Application.get_env(:ex_aws, :region)}.amazonaws.com/presentations/#{@event.presentation_file.hash}/#{position+1}.jpg"}
/> />
<% end %> <% end %>
<% end %>
<%= for poll <- Enum.filter(@event.presentation_file.polls, fn p -> p.position == position end) do %> <%= for poll <- Enum.filter(@event.presentation_file.polls, fn p -> p.position == position end) do %>
<% total = Enum.map(poll.poll_opts, fn e -> e.vote_count end) |> Enum.sum() %> <% total = Enum.map(poll.poll_opts, fn e -> e.vote_count end) |> Enum.sum() %>
@@ -216,7 +218,7 @@
</span> </span>
<%= if length(form.form_submits) > 0 do %> <%= if length(form.form_submits) > 0 do %>
<%= link to: Routes.stat_path(@socket, :export, form.id), class: "text-xs text-white bg-primary-500 rounded-md px-2 py-0.5", method: :post do %> <%= link to: ~p"/export/#{form.id}", class: "text-xs text-white bg-primary-500 rounded-md px-2 py-0.5", method: :post do %>
<%= gettext("Export all submissions") %> <%= gettext("Export all submissions") %>
<% end %> <% end %>
<% end %> <% end %>
@@ -232,12 +234,12 @@
<%= if fs.attendee_identifier do %> <%= if fs.attendee_identifier do %>
<img <img
class="h-8 w-8" class="h-8 w-8"
src={"https://avatars.dicebear.com/api/identicon/#{fs.attendee_identifier}.svg"} src={"https://api.dicebear.com/7.x/personas/svg?seed=#{fs.attendee_identifier}.svg"}
/> />
<% else %> <% else %>
<img <img
class="h-8 w-8" class="h-8 w-8"
src={"https://avatars.dicebear.com/api/identicon/#{fs.user_id}.svg"} src={"https://api.dicebear.com/7.x/personas/svg?seed=#{fs.user_id}.svg"}
/> />
<% end %> <% end %>
@@ -279,12 +281,12 @@
<%= if post.attendee_identifier do %> <%= if post.attendee_identifier do %>
<img <img
class="h-8 w-8" class="h-8 w-8"
src={"https://avatars.dicebear.com/api/identicon/#{post.attendee_identifier}.svg"} src={"https://api.dicebear.com/7.x/personas/svg?seed=#{post.attendee_identifier}.svg"}
/> />
<% else %> <% else %>
<img <img
class="h-8 w-8" class="h-8 w-8"
src={"https://avatars.dicebear.com/api/identicon/#{post.user_id}.svg"} src={"https://api.dicebear.com/7.x/personas/svg?seed=#{post.user_id}.svg"}
/> />
<% end %> <% end %>

View File

@@ -1,7 +1,10 @@
defmodule ClaperWeb.UserLiveAuth do defmodule ClaperWeb.UserLiveAuth do
import Phoenix.LiveView import Phoenix.LiveView
import Phoenix.Component import Phoenix.Component
alias ClaperWeb.Router.Helpers, as: Routes
use Phoenix.VerifiedRoutes,
endpoint: ClaperWeb.Endpoint,
router: ClaperWeb.Router
def on_mount(:default, _params, %{"current_user" => current_user} = _session, socket) do def on_mount(:default, _params, %{"current_user" => current_user} = _session, socket) do
socket = socket =
@@ -19,11 +22,11 @@ defmodule ClaperWeb.UserLiveAuth do
# else # else
# {:halt, # {:halt,
# redirect(socket, # redirect(socket,
# to: Routes.user_registration_path(socket, :confirm, %{email: current_user.email}) # to: ~p"/users/register/confirm?#{[%{email: current_user.email}]}"
# )} # )}
# end # end
end end
def on_mount(:default, _params, _session, socket), def on_mount(:default, _params, _session, socket),
do: {:halt, redirect(socket, to: Routes.user_registration_path(socket, :confirm))} do: {:halt, redirect(socket, to: ~p"/users/register/confirm")}
end end

View File

@@ -59,7 +59,7 @@ defmodule ClaperWeb.UserSettingsLive.Show do
Accounts.deliver_update_email_instructions( Accounts.deliver_update_email_instructions(
applied_user, applied_user,
user.email, user.email,
&Routes.user_settings_url(socket, :confirm_email, &1) &url(~p"/users/settings/confirm_email/#{&1}")
) )
{:noreply, {:noreply,
@@ -68,7 +68,7 @@ defmodule ClaperWeb.UserSettingsLive.Show do
:info, :info,
gettext("A link to confirm your email change has been sent to the new address.") gettext("A link to confirm your email change has been sent to the new address.")
) )
|> push_redirect(to: Routes.user_settings_show_path(socket, :show))} |> push_redirect(to: ~p"/users/settings")}
{:error, changeset} -> {:error, changeset} ->
{:noreply, assign(socket, :email_changeset, changeset)} {:noreply, assign(socket, :email_changeset, changeset)}
@@ -90,13 +90,23 @@ defmodule ClaperWeb.UserSettingsLive.Show do
:info, :info,
gettext("Your password has been updated.") gettext("Your password has been updated.")
) )
|> push_redirect(to: Routes.user_settings_show_path(socket, :show))} |> push_redirect(to: ~p"/users/settings")}
{:error, changeset} -> {:error, changeset} ->
{:noreply, assign(socket, :password_changeset, changeset)} {:noreply, assign(socket, :password_changeset, changeset)}
end end
end end
@impl true
def handle_event("delete_account", _params, %{assigns: %{current_user: user}} = socket) do
Accounts.delete(user)
{:noreply,
socket
|> put_flash(:info, gettext("Your account has been deleted."))
|> redirect(to: ~p"/users/log_in")}
end
@impl true @impl true
def handle_event("validate", _params, socket) do def handle_event("validate", _params, socket) do
{:noreply, socket} {:noreply, socket}

View File

@@ -1,4 +1,4 @@
<div class="mx-3 max-w-7xl sm:mx-auto"> <div class="mx-3 md:max-w-3xl lg:max-w-5xl md:mx-auto">
<div class="border-b border-gray-200 py-4 sm:flex sm:items-center sm:justify-between"> <div class="border-b border-gray-200 py-4 sm:flex sm:items-center sm:justify-between">
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<h1 class="text-2xl font-medium leading-6 text-gray-900 sm:truncate"> <h1 class="text-2xl font-medium leading-6 text-gray-900 sm:truncate">
@@ -16,7 +16,7 @@
id="modal-wrapper" id="modal-wrapper"
title={@page_title} title={@page_title}
description={@page_description} description={@page_description}
return_to={Routes.user_settings_show_path(@socket, :show)} return_to={~p"/users/settings"}
> >
<div> <div>
<.form <.form
@@ -52,7 +52,7 @@
id="modal-wrapper" id="modal-wrapper"
title={@page_title} title={@page_title}
description={@page_description} description={@page_description}
return_to={Routes.user_settings_show_path(@socket, :show)} return_to={~p"/users/settings"}
> >
<div> <div>
<.form <.form
@@ -105,10 +105,12 @@
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2"> <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
<span class="flex-grow"><%= @current_user.email %></span> <span class="flex-grow"><%= @current_user.email %></span>
<span class="ml-4 flex-shrink-0"> <span class="ml-4 flex-shrink-0">
<%= live_patch(gettext("Change"), <.link
to: Routes.user_settings_show_path(@socket, :edit_email), patch={~p"/users/settings/edit/email"}
class: "rounded-md font-medium text-purple-600 hover:text-purple-500" class="rounded-md font-medium text-purple-600 hover:text-purple-500"
) %> >
<%= gettext("Change") %>
</.link>
</span> </span>
</dd> </dd>
@@ -118,15 +120,42 @@
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2"> <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
<span class="flex-grow">********</span> <span class="flex-grow">********</span>
<span class="ml-4 flex-shrink-0"> <span class="ml-4 flex-shrink-0">
<%= live_patch(gettext("Change"), <.link
to: Routes.user_settings_show_path(@socket, :edit_password), patch={~p"/users/settings/edit/password"}
class: "rounded-md font-medium text-purple-600 hover:text-purple-500" class="rounded-md font-medium text-purple-600 hover:text-purple-500"
) %> >
<%= gettext("Change") %>
</.link>
</span> </span>
</dd> </dd>
</div> </div>
</dl> </dl>
</div> </div>
<div>
<div class="py-5">
<h3 class="text-lg leading-6 font-medium text-gray-900">
<%= gettext("Danger zone") %>
</h3>
<p class="mt-1 max-w-2xl text-sm text-gray-500">
<%= gettext("Be careful, these actions are irreversible") %>
</p>
</div>
<div class="border-t border-gray-200 py-5 sm:p-0">
<dl class="sm:divide-y sm:divide-gray-200">
<div class="mt-5">
<button
data-confirm={
gettext("All your events and files will be permanently deleted, are you sure?")
}
phx-click="delete_account"
class="w-full lg:w-auto px-6 text-center text-white py-2 rounded-md tracking-wide font-bold focus:outline-none focus:shadow-outline bg-gradient-to-tl from-supporting-red-600 to-supporting-red-400 bg-size-200 bg-pos-0 hover:bg-pos-100 transition-all duration-500"
>
<%= gettext("Delete account") %>
</button>
</div>
</dl>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,12 @@
defmodule ClaperWeb.Plugs.Iframe do
import Plug.Conn
def init(_), do: %{}
def call(conn, _opts) do
conn
|> put_resp_header(
"x-frame-options",
"ALLOWALL"
)
end
end

View File

@@ -29,7 +29,7 @@ defmodule ClaperWeb.Router do
live_session :attendee do live_session :attendee do
scope "/", ClaperWeb do scope "/", ClaperWeb do
pipe_through([:browser, :attendee_registration]) pipe_through([:browser, :attendee_registration, ClaperWeb.Plugs.Iframe])
live("/", EventLive.Join, :index) live("/", EventLive.Join, :index)
live("/join", EventLive.Join, :join) live("/join", EventLive.Join, :join)

View File

@@ -5,20 +5,11 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<%= csrf_meta_tag() %> <%= csrf_meta_tag() %>
<%= live_title_tag(assigns[:page_title] || "Claper", suffix: " · Claper") %> <title>Not found - Claper</title>
<link rel="icon" type="image/png" href={Routes.static_path(@conn, "/images/favicon.png")} /> <link rel="icon" type="image/png" href="/images/favicon.png" />
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")} /> <link phx-track-static rel="stylesheet" href="/assets/app.css" />
<link <link phx-track-static rel="stylesheet" href="/assets/custom.css" />
phx-track-static <script defer phx-track-static type="text/javascript" src="/assets/app.js">
rel="stylesheet"
href={Routes.static_path(@conn, "/assets/custom.css")}
/>
<script
defer
phx-track-static
type="text/javascript"
src={Routes.static_path(@conn, "/assets/app.js")}
>
</script> </script>
</head> </head>
<body class=""> <body class="">
@@ -41,10 +32,9 @@
</p> </p>
<div class="mt-10"> <div class="mt-10">
<%= live_patch(gettext("Return to home"), <a href="/" class="text-sm text-white underline">
to: Routes.event_join_path(@conn, :index), <%= gettext("Return to home") %>
class: "text-sm text-white underline" </a>
) %>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -5,20 +5,11 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<%= csrf_meta_tag() %> <%= csrf_meta_tag() %>
<%= live_title_tag(assigns[:page_title] || "Claper", suffix: " · Claper.co") %> <title>Not found - Claper</title>
<link rel="icon" type="image/png" href={Routes.static_path(@conn, "/images/favicon.png")} /> <link rel="icon" type="image/png" href="/images/favicon.png" />
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")} /> <link phx-track-static rel="stylesheet" href="/assets/app.css" />
<link <link phx-track-static rel="stylesheet" href="/assets/custom.css" />
phx-track-static <script defer phx-track-static type="text/javascript" src="/assets/app.js">
rel="stylesheet"
href={Routes.static_path(@conn, "/assets/custom.css")}
/>
<script
defer
phx-track-static
type="text/javascript"
src={Routes.static_path(@conn, "/assets/app.js")}
>
</script> </script>
</head> </head>
<body class=""> <body class="">
@@ -42,10 +33,9 @@
<div class="mt-10"> <div class="mt-10">
<p class="mt-5"> <p class="mt-5">
<%= live_patch(gettext("Return to home"), <a href="/" class="text-sm text-white underline">
to: Routes.event_join_path(@conn, :index), <%= gettext("Return to home") %>
class: "text-sm text-white underline" </a>
) %>
</p> </p>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
<div class="sticky top-0 z-20 h-16 bg-black"> <div class="sticky top-0 z-20 h-16 bg-black">
<div class="max-w-7xl sm:mx-auto"> <div class="mx-3 md:max-w-3xl lg:max-w-5xl md:mx-auto">
<!-- Sidebar toggle, controls the 'sidebarOpen' sidebar state. --> <!-- Sidebar toggle, controls the 'sidebarOpen' sidebar state. -->
<a href={Routes.event_index_path(@conn, :index)} class="mt-3 float-left"> <a href={~p"/events"} class="mt-3 float-left">
<img src="/images/logo.svg" class="h-8" /> <img src="/images/logo.svg" class="h-8" />
</a> </a>
@@ -11,21 +11,8 @@
<div class="ml-3 relative"> <div class="ml-3 relative">
<div> <div>
<button <button
phx-click-away={ phx-click-away={JS.hide(to: "#profile-dropdown")}
JS.hide( phx-click={JS.toggle(to: "#profile-dropdown")}
to: "#profile-dropdown",
transition: "animate__animated animate__fadeOut",
time: 300
)
}
phx-click={
JS.toggle(
to: "#profile-dropdown",
out: "animate__animated animate__fadeOut",
in: "animate__animated animate__fadeIn",
time: 800
)
}
type="button" type="button"
class="max-w-xs bg-gray-800 text-white px-3 py-2 flex items-center text-sm rounded-md" class="max-w-xs bg-gray-800 text-white px-3 py-2 flex items-center text-sm rounded-md"
id="user-menu-button" id="user-menu-button"

View File

@@ -1,12 +1,14 @@
<div class="py-1" role="none"> <div class="py-1" role="none">
<%= live_patch(gettext("Settings"), <a
to: Routes.user_settings_show_path(@conn, :show), href={~p"/users/settings"}
class: "text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900" class="text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"
) %> >
<%= gettext("Settings") %>
</a>
</div> </div>
<div class="py-1" role="none"> <div class="py-1" role="none">
<%= link(gettext("Logout"), <%= link(gettext("Logout"),
to: Routes.user_session_path(@conn, :delete), to: ~p"/users/log_out",
method: :delete, method: :delete,
class: "text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900" class: "text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"
) %> ) %>

View File

@@ -5,20 +5,11 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<%= csrf_meta_tag() %> <%= csrf_meta_tag() %>
<%= live_title_tag(assigns[:page_title] || "Claper", suffix: " · Claper") %> <.live_title suffix=" · Claper" )><%= assigns[:page_title] || "Claper" %></.live_title>
<link rel="icon" type="image/png" href={Routes.static_path(@conn, "/images/favicon.png")} /> <link rel="icon" type="image/png" href="/images/favicon.png" />
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")} /> <link phx-track-static rel="stylesheet" href="/assets/app.css" />
<link <link phx-track-static rel="stylesheet" href="/assets/custom.css" />
phx-track-static <script defer phx-track-static type="text/javascript" src="/assets/app.js">
rel="stylesheet"
href={Routes.static_path(@conn, "/assets/custom.css")}
/>
<script
defer
phx-track-static
type="text/javascript"
src={Routes.static_path(@conn, "/assets/app.js")}
>
</script> </script>
</head> </head>
<body class=""> <body class="">

View File

@@ -5,20 +5,11 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<%= csrf_meta_tag() %> <%= csrf_meta_tag() %>
<%= live_title_tag(assigns[:page_title] || "Claper", suffix: " · Claper") %> <.live_title suffix=" · Claper" )><%= assigns[:page_title] || "Claper" %></.live_title>
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")} /> <link phx-track-static rel="stylesheet" href="/assets/app.css" />
<link rel="icon" type="image/png" href={Routes.static_path(@conn, "/images/favicon.png")} /> <link rel="icon" type="image/png" href="/images/favicon.png" />
<link <link phx-track-static rel="stylesheet" href="/assets/custom.css" />
phx-track-static <script defer phx-track-static type="text/javascript" src="/assets/app.js">
rel="stylesheet"
href={Routes.static_path(@conn, "/assets/custom.css")}
/>
<script
defer
phx-track-static
type="text/javascript"
src={Routes.static_path(@conn, "/assets/app.js")}
>
</script> </script>
</head> </head>
<body class="bg-gray-100"> <body class="bg-gray-100">

View File

@@ -1,13 +1,13 @@
<h1>Confirm account</h1> <h1>Confirm account</h1>
<.form :let={_f} for={:user} action={Routes.user_confirmation_path(@conn, :update, @token)}> <.form :let={_f} for={:user} action={~p"/users/confirm/#{@token}"}>
<div> <div>
<%= submit("Confirm my account") %> <%= submit("Confirm my account") %>
</div> </div>
</.form> </.form>
<p> <p>
<%= link("Register", to: Routes.user_registration_path(@conn, :new)) %> | <%= link("Log in", <%= link("Register", to: ~p"/users/register") %> | <%= link("Log in",
to: Routes.user_session_path(@conn, :new) to: ~p"/users/log_in"
) %> ) %>
</p> </p>

View File

@@ -1,6 +1,6 @@
<h1>Resend confirmation instructions</h1> <h1>Resend confirmation instructions</h1>
<.form :let={f} for={:user} action={Routes.user_confirmation_path(@conn, :create)}> <.form :let={f} for={:user} action={~p"/users/confirm"}>
<%= label(f, :email) %> <%= label(f, :email) %>
<%= email_input(f, :email, required: true) %> <%= email_input(f, :email, required: true) %>
@@ -10,7 +10,7 @@
</.form> </.form>
<p> <p>
<%= link("Register", to: Routes.user_registration_path(@conn, :new)) %> | <%= link("Log in", <%= link("Register", to: ~p"/users/register") %> | <%= link("Log in",
to: Routes.user_session_path(@conn, :new) to: ~p"/users/log_in"
) %> ) %>
</p> </p>

View File

@@ -1,6 +1,6 @@
<h1>Resend confirmation instructions</h1> <h1>Resend confirmation instructions</h1>
<%= form_for :user, Routes.user_confirmation_path(@conn, :create), fn f -> %> <%= form_for :user, ~p"/users/confirm", fn f -> %>
<%= label(f, :email) %> <%= label(f, :email) %>
<%= email_input(f, :email, required: true) %> <%= email_input(f, :email, required: true) %>
@@ -10,7 +10,7 @@
<% end %> <% end %>
<p> <p>
<%= link("Register", to: Routes.user_registration_path(@conn, :new)) %> | <%= link("Log in", <%= link("Register", to: ~p"/users/register") %> | <%= link("Log in",
to: Routes.user_session_path(@conn, :new) to: ~p"/users/log_in"
) %> ) %>
</p> </p>

View File

@@ -25,10 +25,9 @@
</p> </p>
<div class="mt-10"> <div class="mt-10">
<%= live_patch(gettext("Return to home"), <.link href={~p"/"} class="text-sm text-white underline">
to: Routes.event_join_path(@conn, :index), <%= gettext("Return to home") %>
class: "text-sm text-white underline" </.link>
) %>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -13,12 +13,7 @@
</h2> </h2>
</div> </div>
<.form <.form :let={f} for={@changeset} action={~p"/users/register"} class="mt-8 space-y-6">
:let={f}
for={@changeset}
action={Routes.user_registration_path(@conn, :create)}
class="mt-8 space-y-6"
>
<%= if @changeset.action do %> <%= if @changeset.action do %>
<ClaperWeb.Component.Alert.error <ClaperWeb.Component.Alert.error
message={gettext("Oops, check that all fields are filled in correctly.")} message={gettext("Oops, check that all fields are filled in correctly.")}

View File

@@ -16,7 +16,7 @@
<.form <.form
:let={f} :let={f}
for={@changeset} for={@changeset}
action={Routes.user_reset_password_path(@conn, :update, @token)} action={~p"/users/reset_password/#{@token}"}
method="post" method="post"
class="mt-8 space-y-6" class="mt-8 space-y-6"
> >

View File

@@ -13,12 +13,7 @@
</h2> </h2>
</div> </div>
<.form <.form :let={f} for={@changeset} action={~p"/users/reset_password"} class="mt-8 space-y-6">
:let={f}
for={@changeset}
action={Routes.user_reset_password_path(@conn, :create)}
class="mt-8 space-y-6"
>
<%= if @changeset.action do %> <%= if @changeset.action do %>
<ClaperWeb.Component.Alert.error <ClaperWeb.Component.Alert.error
message={gettext("Oops, check that all fields are filled in correctly.")} message={gettext("Oops, check that all fields are filled in correctly.")}

View File

@@ -27,13 +27,7 @@
</div> </div>
<div class="flex flex-row justify-center items-center space-x-3"></div> <div class="flex flex-row justify-center items-center space-x-3"></div>
<.form <.form :let={f} for={@conn} action={~p"/users/log_in"} as={:user} class="mt-12 mb-4">
:let={f}
for={@conn}
action={Routes.user_session_path(@conn, :create)}
as={:user}
class="mt-12 mb-4"
>
<%= if @error_message do %> <%= if @error_message do %>
<ClaperWeb.Component.Alert.error message={@error_message} stick={true} /> <ClaperWeb.Component.Alert.error message={@error_message} stick={true} />
<% end %> <% end %>
@@ -70,12 +64,12 @@
<div class="mt-4 text-center flex gap-x-2 justify-center"> <div class="mt-4 text-center flex gap-x-2 justify-center">
<%= link(gettext("Forgot your password?"), <%= link(gettext("Forgot your password?"),
to: Routes.user_reset_password_path(@conn, :new), to: ~p"/users/reset_password",
class: "text-white text-sm text-center" class: "text-white text-sm text-center"
) %> ) %>
<%= if Application.get_env(:claper, :enable_account_creation) do %> <%= if Application.get_env(:claper, :enable_account_creation) do %>
<%= link(gettext("Create account"), <%= link(gettext("Create account"),
to: Routes.user_registration_path(@conn, :new), to: ~p"/users/register",
class: "text-white text-sm text-center" class: "text-white text-sm text-center"
) %> ) %>
<% end %> <% end %>

View File

@@ -106,45 +106,29 @@ defmodule ClaperWeb.Component.Input do
|> assign_new(:shortcut, fn -> nil end) |> assign_new(:shortcut, fn -> nil end)
~H""" ~H"""
<!-- Enabled: "bg-indigo-600", Not Enabled: "bg-gray-200" -->
<button <button
phx-click={checked(@checked, @key)} phx-click={checked(@checked, @key)}
disabled={@disabled} disabled={@disabled}
phx-value-key={@key} phx-value-key={@key}
id={"check-#{@key}"} id={"check-#{@key}"}
type="button" type="button"
class={"#{if @checked, do: 'bg-primary-600', else: 'bg-gray-200'} relative inline-flex flex-shrink-0 h-8 w-14 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200"} class="group relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center rounded-full"
role="switch" role="switch"
aria-checked="false" aria-checked="false"
phx-key={@shortcut} phx-key={@shortcut}
phx-window-keydown={if @shortcut && not @disabled, do: checked(@checked, @key)} phx-window-keydown={if @shortcut && not @disabled, do: checked(@checked, @key)}
> >
<!-- Enabled: "translate-x-5", Not Enabled: "translate-x-0" --> <span class="pointer-events-none absolute h-full w-full rounded-md bg-white" aria-hidden="true">
<span class={"#{if @checked, do: 'translate-x-6', else: 'translate-x-0'} pointer-events-none relative inline-block h-7 w-7 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"}> </span>
<!-- Enabled: "opacity-0 ease-out duration-100", Not Enabled: "opacity-100 ease-in duration-200" -->
<span <span
class={"#{if @checked, do: 'opacity-0 ease-out duration-100', else: 'opacity-100 ease-in duration-200'} absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"} aria-hidden="true"
class={"#{if @checked, do: 'bg-primary-500', else: 'bg-gray-200'} pointer-events-none absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out"}
>
</span>
<span
class={"#{if @checked, do: 'translate-x-5', else: 'translate-x-0'} pointer-events-none absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow ring-0 transition-transform duration-200 ease-in-out"}
aria-hidden="true" aria-hidden="true"
> >
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 12 12">
<path
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<!-- Enabled: "opacity-100 ease-in duration-200", Not Enabled: "opacity-0 ease-out duration-100" -->
<span
class={"#{if @checked, do: 'opacity-100 ease-in duration-200', else: 'opacity-0 ease-out duration-100'} absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"}
aria-hidden="true"
>
<svg class="h-5 w-5 text-primary-400" fill="currentColor" viewBox="0 0 12 12">
<path d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z" />
</svg>
</span>
</span> </span>
</button> </button>
""" """
@@ -154,24 +138,6 @@ defmodule ClaperWeb.Component.Input do
def checked(false, key, js) do def checked(false, key, js) do
js js
|> JS.remove_class("translate-x-0",
to: "#check-#{key} > span"
)
|> JS.add_class("translate-x-6",
to: "#check-#{key} > span"
)
|> JS.remove_class("opacity-100 ease-in duration-200",
to: "#check-#{key} > span > span"
)
|> JS.add_class("opacity-0 ease-out duration-100",
to: "#check-#{key} > span > span"
)
|> JS.remove_class("opacity-0 ease-out duration-100",
to: "#check-#{key} > span > span:nth-child(2)"
)
|> JS.add_class("opacity-100 ease-in duration-200",
to: "#check-#{key} > span > span:nth-child(2)"
)
|> JS.push("checked", value: %{key: key, value: true}) |> JS.push("checked", value: %{key: key, value: true})
end end
@@ -233,63 +199,6 @@ defmodule ClaperWeb.Component.Input do
end end
def date(assigns) do def date(assigns) do
assigns =
assigns
|> assign_new(:required, fn -> false end)
|> assign_new(:autofocus, fn -> false end)
|> assign_new(:value, fn -> Map.get(assigns.form.data, assigns.key) end)
assigns =
if Map.has_key?(assigns, :dark),
do: assign(assigns, :containerTheme, "text-white"),
else: assign(assigns, :containerTheme, "text-black")
~H"""
<div
class="relative flatpickr"
x-data={"{input: moment.utc(#{if assigns.value == nil, do: 'undefined', else: '\'#{assigns.value}\''}).local().format('Y-MM-DD HH:mm')}"}
data-default-date={"#{assigns.value}"}
x-on:click="$refs.input.focus()"
id="date"
phx-hook="Pickr"
data-enable={"[
{
\"from\": \"#{@from}\",
\"to\": \"#{@to}\"
}]"}
>
<%= hidden_input(@form, :utc_date,
required: @required,
"x-ref": "utc",
"phx-hook": "DefaultValue",
"data-default-value": "#{assigns.value}"
) %>
<%= text_input(@form, @key,
required: @required,
autofocus: @autofocus,
autocomplete: @key,
class:
"transition-all bg-transparent w-full #{@containerTheme} rounded px-3 border border-gray-500 focus:border-2 focus:border-primary-500 pt-5 pb-2 focus:outline-none input active:outline-none text-left",
"x-model": "input",
"x-ref": "input",
"data-input": "true",
"x-on:change": "$refs.utc.value = moment($refs.input.value).utc().format()"
) %>
<%= label(@form, @key, @name,
class:
"label absolute mb-0 -mt-2 pt-5 pl-3 leading-tighter text-gray-500 mt-2 cursor-text transition-all left-0",
"x-bind:class": "input.length > 0 ? 'text-sm -top-1.5' : 'top-1'",
"x-on:click": "$refs.input.focus()",
"x-on:click.away": "$refs.input.blur()"
) %>
<%= if Keyword.has_key?(@form.errors, @key) do %>
<p class="text-supporting-red-500 text-sm"><%= error_tag(@form, @key) %></p>
<% end %>
</div>
"""
end
def date_range(assigns) do
assigns = assigns =
assigns assigns
|> assign_new(:required, fn -> false end) |> assign_new(:required, fn -> false end)
@@ -298,47 +207,20 @@ defmodule ClaperWeb.Component.Input do
|> assign_new(:readonly, fn -> false end) |> assign_new(:readonly, fn -> false end)
~H""" ~H"""
<div x-data="{getDate (start, end) { <div>
s = start == undefined || start.length === 0 ? moment().format('Y-MM-DD HH:mm') : moment.utc(start).local().format('Y-MM-DD HH:mm') <div class="relative" id="date" phx-hook="Pickr">
e = end == undefined || end.length === 0 ? moment().add(2, 'hours').format('Y-MM-DD HH:mm') : moment.utc(end).local().format('Y-MM-DD HH:mm')
return s + ' - ' + e }
}">
<div
x-effect="date = getDate($refs.startDate.value, $refs.endDate.value)"
class="relative flatpickr"
x-data="{date: getDate($refs.startDate.value, $refs.endDate.value)}"
data-mode="range"
data-default-date-start={Map.get(assigns.form.data, assigns.start_date_field)}
data-default-date-end={Map.get(assigns.form.data, assigns.end_date_field)}
id="date-range"
phx-hook={"#{if not @readonly, do: 'Pickr'}"}
data-enable={"[
{
\"from\": \"#{@from}\",
\"to\": \"#{@to}\"
}]"}
>
<%= hidden_input(@form, @start_date_field, "x-ref": "startDate") %>
<%= hidden_input(@form, @end_date_field, "x-ref": "endDate") %>
<%= label(@form, @key, @name, class: "block text-sm font-medium text-gray-700") %> <%= label(@form, @key, @name, class: "block text-sm font-medium text-gray-700") %>
<div class="mt-1 relative"> <div class="mt-1 relative">
<%= hidden_input(@form, @key) %>
<%= text_input(@form, :local_date, <%= text_input(@form, :local_date,
required: @required,
readonly: @readonly,
class:
"absolute z-0 outline-none shadow-base focus:ring-primary-500 focus:border-primary-500 block w-full text-lg border-gray-300 rounded-md py-4 px-3 read-only:opacity-50",
"x-model": "date"
) %>
<%= text_input(@form, @key,
autofocus: @autofocus, autofocus: @autofocus,
placeholder: @placeholder, placeholder: @placeholder,
autocomplete: @key, autocomplete: false,
class: class:
"absolute z-10 bg-transparent text-transparent outline-none block w-full py-4 px-3", "outline-none shadow-base focus:ring-primary-500 focus:border-primary-500 block w-full text-lg border-gray-300 rounded-md py-4 px-3 read-only:opacity-50"
"data-input": "true"
) %> ) %>
</div> </div>
<%= if Keyword.has_key?(@form.errors, @key) do %> <%= if Keyword.has_key?(@form.errors, @key) do %>
<p class="text-supporting-red-500 text-sm"><%= error_tag(@form, @key) %></p> <p class="text-supporting-red-500 text-sm"><%= error_tag(@form, @key) %></p>
<% end %> <% end %>

View File

@@ -3,7 +3,8 @@ defmodule ClaperWeb.ErrorHelpers do
Conveniences for translating and building error messages. Conveniences for translating and building error messages.
""" """
use Phoenix.HTML import Phoenix.HTML.Form
use PhoenixHTMLHelpers
@doc """ @doc """
Generates tag for inlined form input errors. Generates tag for inlined form input errors.

View File

@@ -47,6 +47,6 @@ defmodule ClaperWeb.LayoutView do
opts opts
|> Keyword.put(:class, class) |> Keyword.put(:class, class)
live_patch(text, opts) link(text, opts)
end end
end end

View File

@@ -1,5 +1,5 @@
defmodule ClaperWeb.LeaderNotifierView do defmodule ClaperWeb.LeaderNotifierView do
use Phoenix.View, root: "lib/claper_web/templates" use Phoenix.View, root: "lib/claper_web/templates"
import ClaperWeb.Gettext import ClaperWeb.Gettext
use Phoenix.HTML use PhoenixHTMLHelpers
end end

View File

@@ -1,5 +1,6 @@
defmodule ClaperWeb.UserNotifierView do defmodule ClaperWeb.UserNotifierView do
use Phoenix.View, root: "lib/claper_web/templates" use Phoenix.View, root: "lib/claper_web/templates"
import ClaperWeb.Gettext import ClaperWeb.Gettext
use Phoenix.HTML import Phoenix.HTML
use PhoenixHTMLHelpers
end end

19
mix.exs
View File

@@ -1,7 +1,7 @@
defmodule Claper.MixProject do defmodule Claper.MixProject do
use Mix.Project use Mix.Project
@version "1.7.0" @version "2.0.0"
def project do def project do
[ [
@@ -90,16 +90,17 @@ defmodule Claper.MixProject do
{:ex_doc, "~> 0.27", only: :dev, runtime: false}, {:ex_doc, "~> 0.27", only: :dev, runtime: false},
{:bcrypt_elixir, "~> 2.0"}, {:bcrypt_elixir, "~> 2.0"},
{:phoenix, "~> 1.7"}, {:phoenix, "~> 1.7"},
{:phoenix_ecto, "~> 4.4"}, {:phoenix_ecto, "~> 4.5"},
{:ecto_sql, "~> 3.10"}, {:ecto_sql, "~> 3.11"},
{:postgrex, ">= 0.0.0"}, {:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 3.0"}, {:phoenix_html, "~> 4.1"},
{:phoenix_live_reload, "~> 1.2", only: :dev}, {:phoenix_html_helpers, "~> 1.0"},
{:phoenix_live_view, "~> 0.18.3"}, {:phoenix_live_reload, "~> 1.5.2", only: :dev},
{:phoenix_swoosh, "~> 1.0"}, {:phoenix_live_view, "~> 0.20.14"},
{:phoenix_swoosh, "~> 1.2.1"},
{:phoenix_view, "~> 2.0"}, {:phoenix_view, "~> 2.0"},
{:floki, ">= 0.30.0", only: :test}, {:floki, ">= 0.36.1", only: :test},
{:phoenix_live_dashboard, "~> 0.7"}, {:phoenix_live_dashboard, "~> 0.8"},
{:esbuild, "~> 0.2", runtime: Mix.env() == :dev}, {:esbuild, "~> 0.2", runtime: Mix.env() == :dev},
{:dart_sass, "~> 0.5", runtime: Mix.env() == :dev}, {:dart_sass, "~> 0.5", runtime: Mix.env() == :dev},
{:swoosh, "~> 1.12"}, {:swoosh, "~> 1.12"},

View File

@@ -1,30 +1,30 @@
%{ %{
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.1", "5114d780459a04f2b4aeef52307de23de961b69e13a5cd98a911e39fda13f420", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "42182d5f46764def15bf9af83739e3bf4ad22661b1c34fc3e88558efced07279"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.1", "5114d780459a04f2b4aeef52307de23de961b69e13a5cd98a911e39fda13f420", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "42182d5f46764def15bf9af83739e3bf4ad22661b1c34fc3e88558efced07279"},
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"}, "castore": {:hex, :castore, "1.0.6", "ffc42f110ebfdafab0ea159cd43d31365fa0af0ce4a02ecebf1707ae619ee727", [:mix], [], "hexpm", "374c6e7ca752296be3d6780a6d5b922854ffcc74123da90f2f328996b962d33a"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"}, "comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"},
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
"credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, "credo": {:hex, :credo, "1.7.5", "643213503b1c766ec0496d828c90c424471ea54da77c8a168c725686377b9545", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f799e9b5cd1891577d8c773d245668aa74a2fcd15eb277f51a0131690ebfb3fd"},
"csv": {:hex, :csv, "3.0.5", "3c1455127e92de8845806db89554ad7d45e0212974be41dd9c38a5c881861713", [:mix], [], "hexpm", "cbbe5455c93df5f3f2943e995e28b7a8808361ba34cf3e44267d77a01eaf1609"}, "csv": {:hex, :csv, "3.0.5", "3c1455127e92de8845806db89554ad7d45e0212974be41dd9c38a5c881861713", [:mix], [], "hexpm", "cbbe5455c93df5f3f2943e995e28b7a8808361ba34cf3e44267d77a01eaf1609"},
"dart_sass": {:hex, :dart_sass, "0.6.0", "1fe560c3ed5c577b6b9cf97134a0e05c82b69645d313b1ef0ffb4d659c3d0300", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "41f7bb065b5c30c3ea05e8b41aa3f9b5c62817079b94f70e2a22d133828475bb"}, "dart_sass": {:hex, :dart_sass, "0.6.0", "1fe560c3ed5c577b6b9cf97134a0e05c82b69645d313b1ef0ffb4d659c3d0300", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "41f7bb065b5c30c3ea05e8b41aa3f9b5c62817079b94f70e2a22d133828475bb"},
"db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"}, "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"earmark_parser": {:hex, :earmark_parser, "1.4.31", "a93921cdc6b9b869f519213d5bc79d9e218ba768d7270d46fdcf1c01bacff9e2", [:mix], [], "hexpm", "317d367ee0335ef037a87e46c91a2269fef6306413f731e8ec11fc45a7efd059"}, "earmark_parser": {:hex, :earmark_parser, "1.4.31", "a93921cdc6b9b869f519213d5bc79d9e218ba768d7270d46fdcf1c01bacff9e2", [:mix], [], "hexpm", "317d367ee0335ef037a87e46c91a2269fef6306413f731e8ec11fc45a7efd059"},
"ecto": {:hex, :ecto, "3.10.1", "c6757101880e90acc6125b095853176a02da8f1afe056f91f1f90b80c9389822", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d2ac4255f1601bdf7ac74c0ed971102c6829dc158719b94bd30041bbad77f87a"}, "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"},
"ecto_sql": {:hex, :ecto_sql, "3.10.1", "6ea6b3036a0b0ca94c2a02613fd9f742614b5cfe494c41af2e6571bb034dd94c", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6a25bdbbd695f12c8171eaff0851fa4c8e72eec1e98c7364402dda9ce11c56b"}, "ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"},
"elixir_make": {:hex, :elixir_make, "0.7.6", "67716309dc5d43e16b5abbd00c01b8df6a0c2ab54a8f595468035a50189f9169", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5a0569756b0f7873a77687800c164cca6dfc03a09418e6fcf853d78991f49940"}, "elixir_make": {:hex, :elixir_make, "0.7.6", "67716309dc5d43e16b5abbd00c01b8df6a0c2ab54a8f595468035a50189f9169", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5a0569756b0f7873a77687800c164cca6dfc03a09418e6fcf853d78991f49940"},
"esbuild": {:hex, :esbuild, "0.7.0", "ce3afb13cd2c5fd63e13c0e2d0e0831487a97a7696cfa563707342bb825d122a", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "4ae9f4f237c5ebcb001390b8ada65a12fb2bb04f3fe3d1f1692b7a06fbfe8752"}, "esbuild": {:hex, :esbuild, "0.7.0", "ce3afb13cd2c5fd63e13c0e2d0e0831487a97a7696cfa563707342bb825d122a", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "4ae9f4f237c5ebcb001390b8ada65a12fb2bb04f3fe3d1f1692b7a06fbfe8752"},
"ex_aws": {:hex, :ex_aws, "2.5.0", "1785e69350b16514c1049330537c7da10039b1a53e1d253bbd703b135174aec3", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "971b86e5495fc0ae1c318e35e23f389e74cf322f2c02d34037c6fc6d405006f1"}, "ex_aws": {:hex, :ex_aws, "2.5.3", "9c2d05ba0c057395b12c7b5ca6267d14cdaec1d8e65bdf6481fe1fd245accfb4", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "67115f1d399d7ec4d191812ee565c6106cb4b1bbf19a9d4db06f265fd87da97e"},
"ex_aws_s3": {:hex, :ex_aws_s3, "2.4.0", "ce8decb6b523381812798396bc0e3aaa62282e1b40520125d1f4eff4abdff0f4", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "85dda6e27754d94582869d39cba3241d9ea60b6aa4167f9c88e309dc687e56bb"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.4.0", "ce8decb6b523381812798396bc0e3aaa62282e1b40520125d1f4eff4abdff0f4", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "85dda6e27754d94582869d39cba3241d9ea60b6aa4167f9c88e309dc687e56bb"},
"ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"},
"expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"}, "expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
"finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"}, "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
"floki": {:hex, :floki, "0.34.2", "5fad07ef153b3b8ec110b6b155ec3780c4b2c4906297d0b4be1a7162d04a7e02", [:mix], [], "hexpm", "26b9d50f0f01796bc6be611ca815c5e0de034d2128e39cc9702eee6b66a4d1c8"}, "floki": {:hex, :floki, "0.36.1", "712b7f2ba19a4d5a47dfe3e74d81876c95bbcbee44fe551f0af3d2a388abb3da", [:mix], [], "hexpm", "21ba57abb8204bcc70c439b423fc0dd9f0286de67dc82773a14b0200ada0995f"},
"gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"}, "gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"},
"gettext": {:hex, :gettext, "0.22.3", "c8273e78db4a0bb6fba7e9f0fd881112f349a3117f7f7c598fa18c66c888e524", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "935f23447713954a6866f1bb28c3a878c4c011e802bcd68a726f5e558e4b64bd"}, "gettext": {:hex, :gettext, "0.22.3", "c8273e78db4a0bb6fba7e9f0fd881112f349a3117f7f7c598fa18c66c888e524", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "935f23447713954a6866f1bb28c3a878c4c011e802bcd68a726f5e558e4b64bd"},
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~>2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~>2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
@@ -41,37 +41,38 @@
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"}, "mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"},
"mogrify": {:hex, :mogrify, "0.9.2", "b360984adea7dd6a55f18028e6327973c58de7f548fdb86c9859848aa904d5b0", [:mix], [], "hexpm", "c18d10fd70ca20e2585301616c89f6e4f7159d92efc9cc8ee579e00c886f699d"}, "mogrify": {:hex, :mogrify, "0.9.2", "b360984adea7dd6a55f18028e6327973c58de7f548fdb86c9859848aa904d5b0", [:mix], [], "hexpm", "c18d10fd70ca20e2585301616c89f6e4f7159d92efc9cc8ee579e00c886f699d"},
"nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
"nimble_parsec": {:hex, :nimble_parsec, "1.3.0", "9e18a119d9efc3370a3ef2a937bf0b24c088d9c4bf0ba9d7c3751d49d347d035", [:mix], [], "hexpm", "7977f183127a7cbe9346981e2f480dc04c55ffddaef746bd58debd566070eef8"}, "nimble_parsec": {:hex, :nimble_parsec, "1.3.0", "9e18a119d9efc3370a3ef2a937bf0b24c088d9c4bf0ba9d7c3751d49d347d035", [:mix], [], "hexpm", "7977f183127a7cbe9346981e2f480dc04c55ffddaef746bd58debd566070eef8"},
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"phoenix": {:hex, :phoenix, "1.7.2", "c375ffb482beb4e3d20894f84dd7920442884f5f5b70b9f4528cbe0cedefec63", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "1ebca94b32b4d0e097ab2444a9742ed8ff3361acad17365e4e6b2e79b4792159"}, "phoenix": {:hex, :phoenix, "1.7.11", "1d88fc6b05ab0c735b250932c4e6e33bfa1c186f76dcf623d8dd52f07d6379c7", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "b1ec57f2e40316b306708fe59b92a16b9f6f4bf50ccfa41aa8c7feb79e0ec02a"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.5.1", "6fdbc334ea53620e71655664df6f33f670747b3a7a6c4041cdda3e2c32df6257", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ebe43aa580db129e54408e719fb9659b7f9e0d52b965c5be26cdca416ecead28"},
"phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"}, "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"}, "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.3", "7ff51c9b6609470f681fbea20578dede0e548302b0c8bdf338b5a753a4f045bf", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "f9470a0a8bae4f56430a23d42f977b5a6205fdba6559d76f932b876bfaec652d"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.18", "1f38fbd7c363723f19aad1a04b5490ff3a178e37daaf6999594d5f34796c47fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a5810d0472f3189ede6d2a95bda7f31c6113156b91784a3426cb0ab6a6d85214"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.2", "354460993a480656b71c3887f5565f612b3bdbdd8688c83f9e6f512307067dd4", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "2bb3722f327e14a7aa47b1acf27ed633c8cd27b167e18b8237954b9b4804af39"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.14", "70fa101aa0539e81bed4238777498f6215e9dda3461bdaa067cad6908110c364", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "82f6d006c5264f979ed5eb75593d808bbe39020f20df2e78426f4f2d570e2402"},
"phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.0", "a544d83fde4a767efb78f45404a74c9e37b2a9c5ea3339692e65a6966731f935", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "e88d117251e89a16b92222415a6d87b99a96747ddf674fc5c7631de734811dba"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
"phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"}, "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"},
"phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"plug": {:hex, :plug, "1.15.1", "b7efd81c1a1286f13efb3f769de343236bd8b7d23b4a9f40d3002fc39ad8f74c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "459497bd94d041d98d948054ec6c0b76feacd28eec38b219ca04c0de13c79d30"}, "phoenix_view": {:hex, :phoenix_view, "2.0.3", "4d32c4817fce933693741deeb99ef1392619f942633dde834a5163124813aad3", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "cd34049af41be2c627df99cd4eaa71fc52a328c0c3d8e7d4aa28f880c30e7f64"},
"plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, "plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"},
"plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, "plug_cowboy": {:hex, :plug_cowboy, "2.7.0", "3ae9369c60641084363b08fe90267cbdd316df57e3557ea522114b30b63256ea", [:mix], [{:cowboy, "~> 2.7.0 or ~> 2.8.0 or ~> 2.9.0 or ~> 2.10.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d85444fb8aa1f2fc62eabe83bbe387d81510d773886774ebdcb429b3da3c1a4a"},
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
"porcelain": {:hex, :porcelain, "2.0.3", "2d77b17d1f21fed875b8c5ecba72a01533db2013bd2e5e62c6d286c029150fdc", [:mix], [], "hexpm", "dc996ab8fadbc09912c787c7ab8673065e50ea1a6245177b0c24569013d23620"}, "porcelain": {:hex, :porcelain, "2.0.3", "2d77b17d1f21fed875b8c5ecba72a01533db2013bd2e5e62c6d286c029150fdc", [:mix], [], "hexpm", "dc996ab8fadbc09912c787c7ab8673065e50ea1a6245177b0c24569013d23620"},
"postgrex": {:hex, :postgrex, "0.17.1", "01c29fd1205940ee55f7addb8f1dc25618ca63a8817e56fac4f6846fc2cddcbe", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "14b057b488e73be2beee508fb1955d8db90d6485c6466428fe9ccf1d6692a555"}, "postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"stripity_stripe": {:hex, :stripity_stripe, "2.13.0", "b9ea806fcf46e85232b75f2145c34770b17faa44c59cdd13ff493aaa6e84b4a9", [:mix], [{:hackney, "~> 1.15", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:uri_query, "~> 0.1.2", [hex: :uri_query, repo: "hexpm", optional: false]}], "hexpm", "d6931ed9816552320f95428fd997edf15e99a913ca78fc4342d5516b98f42476"}, "stripity_stripe": {:hex, :stripity_stripe, "2.13.0", "b9ea806fcf46e85232b75f2145c34770b17faa44c59cdd13ff493aaa6e84b4a9", [:mix], [{:hackney, "~> 1.15", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:uri_query, "~> 0.1.2", [hex: :uri_query, repo: "hexpm", optional: false]}], "hexpm", "d6931ed9816552320f95428fd997edf15e99a913ca78fc4342d5516b98f42476"},
"sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"}, "sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"},
"swoosh": {:hex, :swoosh, "1.12.0", "ecc85ee12947932986243299b8d28e6cdfc192c8d9e24c4c64f6738efdf344cb", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87db7ab0f35e358ba5eac3afc7422ed0c8c168a2d219d2a83ad8cb7a424f6cc9"}, "swoosh": {:hex, :swoosh, "1.16.3", "4ab7dc429e84afaf8ffe1c7c06ce1acbc7ddde758d2cb9152dd2ac32289d5498", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.1.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ff70980087650a72951ebd109a286d83c270e2b6610aba447140562adff8cf0a"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"uri_query": {:hex, :uri_query, "0.1.2", "ae35b83b472f3568c2c159eee3f3ccf585375d8a94fb5382db1ea3589e75c3b4", [:mix], [], "hexpm", "e3bc81816c98502c36498b9b2f239b89c71ce5eadfff7ceb2d6c0a2e6ae2ea0c"}, "uri_query": {:hex, :uri_query, "0.1.2", "ae35b83b472f3568c2c159eee3f3ccf585375d8a94fb5382db1ea3589e75c3b4", [:mix], [], "hexpm", "e3bc81816c98502c36498b9b2f239b89c71ce5eadfff7ceb2d6c0a2e6ae2ea0c"},
"websock": {:hex, :websock, "0.5.0", "f6bbce90226121d62a0715bca7c986c5e43de0ccc9475d79c55381d1796368cc", [:mix], [], "hexpm", "b51ac706df8a7a48a2c622ee02d09d68be8c40418698ffa909d73ae207eb5fb8"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.0", "cea35d8bbf1a6964e32d4b02ceb561dfb769c04f16d60d743885587e7d2ca55b", [:mix], [{:bandit, "~> 0.6", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "16318b124effab8209b1eb7906c636374f623dc9511a8278ad09c083cea5bb83"}, "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"},
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
defmodule Claper.Repo.Migrations.AddMessageReactionEnabledToPresentationStates do
use Ecto.Migration
def change do
alter table(:presentation_states) do
add :message_reaction_enabled, :boolean, default: true
end
end
end

View File

@@ -0,0 +1,9 @@
defmodule Claper.Repo.Migrations.AddShowPollResultsEnabledToPresentationStates do
use Ecto.Migration
def change do
alter table(:presentation_states) do
add :show_poll_results_enabled, :boolean, default: true
end
end
end

View File

@@ -1,6 +1,6 @@
docker stop claper-db docker stop claper-db
docker rm claper-db docker rm claper-db
docker run -p 5432:5432 -e POSTGRES_PASSWORD=claper -e POSTGRES_USER=claper -e POSTGRES_DB=claper --name claper-db -d postgres:9 docker run -p 5432:5432 -e POSTGRES_PASSWORD=claper -e POSTGRES_USER=claper -e POSTGRES_DB=claper --name claper-db -d postgres:15
sleep 5 sleep 5
mix ecto.migrate mix ecto.migrate
mix run priv/repo/seeds.exs mix run priv/repo/seeds.exs

View File

@@ -191,7 +191,14 @@ defmodule Claper.AccountsTest do
end end
test "does not update email if token expired", %{user: user, token: token} do test "does not update email if token expired", %{user: user, token: token} do
{1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]]) context = "change:#{user.email}"
{1, nil} =
from(ut in UserToken,
where: ut.user_id == ^user.id and ut.context == ^context
)
|> Repo.update_all(set: [inserted_at: ~N[2020-01-01 00:00:00]])
assert Accounts.update_user_email(user, token) == :error assert Accounts.update_user_email(user, token) == :error
assert Repo.get!(User, user.id).email == user.email assert Repo.get!(User, user.id).email == user.email
assert Repo.get_by(UserToken, user_id: user.id) assert Repo.get_by(UserToken, user_id: user.id)
@@ -235,8 +242,11 @@ defmodule Claper.AccountsTest do
refute Accounts.get_user_by_session_token("oops") refute Accounts.get_user_by_session_token("oops")
end end
test "does not return user for expired token", %{token: token} do test "does not return user for expired token", %{user: user, token: token} do
{1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]]) {1, nil} =
from(ut in UserToken, where: ut.user_id == ^user.id and ut.context == "session")
|> Repo.update_all(set: [inserted_at: ~N[2020-01-01 00:00:00]])
refute Accounts.get_user_by_session_token(token) refute Accounts.get_user_by_session_token(token)
end end
end end
@@ -296,7 +306,10 @@ defmodule Claper.AccountsTest do
end end
test "does not confirm email if token expired", %{user: user, token: token} do test "does not confirm email if token expired", %{user: user, token: token} do
{1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]]) {1, nil} =
from(ut in UserToken, where: ut.user_id == ^user.id and ut.context == "confirm")
|> Repo.update_all(set: [inserted_at: ~N[2020-01-01 00:00:00]])
assert Accounts.confirm_user(token) == :error assert Accounts.confirm_user(token) == :error
refute Repo.get!(User, user.id).confirmed_at refute Repo.get!(User, user.id).confirmed_at
assert Repo.get_by(UserToken, user_id: user.id) assert Repo.get_by(UserToken, user_id: user.id)

View File

@@ -131,8 +131,10 @@ defmodule ClaperWeb.UserAuthTest do
test "redirects if user is not authenticated", %{conn: conn} do test "redirects if user is not authenticated", %{conn: conn} do
conn = conn |> fetch_flash() |> UserAuth.require_authenticated_user([]) conn = conn |> fetch_flash() |> UserAuth.require_authenticated_user([])
assert conn.halted assert conn.halted
assert redirected_to(conn) == Routes.user_session_path(conn, :new) assert redirected_to(conn) == ~p"/users/log_in"
assert get_flash(conn, :error) == "You must log in to access this page."
assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
"You must log in to access this page."
end end
test "stores the path to redirect to on GET", %{conn: conn} do test "stores the path to redirect to on GET", %{conn: conn} do

View File

@@ -13,12 +13,12 @@ defmodule ClaperWeb.UserConfirmationControllerTest do
@tag :capture_log @tag :capture_log
test "sends a new confirmation token", %{conn: conn, user: user} do test "sends a new confirmation token", %{conn: conn, user: user} do
conn = conn =
post(conn, Routes.user_confirmation_path(conn, :create), %{ post(conn, ~p"/users/confirm", %{
"user" => %{"email" => user.email} "user" => %{"email" => user.email}
}) })
assert redirected_to(conn) == "/" assert redirected_to(conn) == "/"
assert get_flash(conn, :info) =~ "If your email is in our system" assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "If your email is in our system"
assert Repo.get_by!(Accounts.UserToken, user_id: user.id).context == "confirm" assert Repo.get_by!(Accounts.UserToken, user_id: user.id).context == "confirm"
end end
@@ -26,24 +26,28 @@ defmodule ClaperWeb.UserConfirmationControllerTest do
Repo.update!(Accounts.User.confirm_changeset(user)) Repo.update!(Accounts.User.confirm_changeset(user))
conn = conn =
post(conn, Routes.user_confirmation_path(conn, :create), %{ post(conn, ~p"/users/confirm", %{
"user" => %{"email" => user.email} "user" => %{"email" => user.email}
}) })
assert redirected_to(conn) == "/" assert redirected_to(conn) == "/"
assert get_flash(conn, :info) =~ "If your email is in our system" assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "If your email is in our system"
refute Repo.get_by(Accounts.UserToken, user_id: user.id) refute Repo.get_by(Accounts.UserToken, user_id: user.id)
end end
test "does not send confirmation token if email is invalid", %{conn: conn} do test "does not send confirmation token if email is invalid", %{conn: conn} do
conn = conn =
post(conn, Routes.user_confirmation_path(conn, :create), %{ post(conn, ~p"/users/confirm", %{
"user" => %{"email" => "unknown@example.com"} "user" => %{"email" => "unknown@example.com"}
}) })
assert redirected_to(conn) == "/" assert redirected_to(conn) == "/"
assert get_flash(conn, :info) =~ "If your email is in our system" assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "If your email is in our system"
assert Repo.all(Accounts.UserToken) == []
assert from(ut in Accounts.UserToken,
where: ut.context == "confirm"
)
|> Repo.all() == []
end end
end end
@@ -54,32 +58,41 @@ defmodule ClaperWeb.UserConfirmationControllerTest do
Accounts.deliver_user_confirmation_instructions(user, url) Accounts.deliver_user_confirmation_instructions(user, url)
end) end)
conn = post(conn, Routes.user_confirmation_path(conn, :update, token)) conn = post(conn, ~p"/users/confirm/#{token}")
assert redirected_to(conn) == "/" assert redirected_to(conn) == "/"
assert get_flash(conn, :info) =~ "User confirmed successfully" assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "User confirmed successfully"
assert Accounts.get_user!(user.id).confirmed_at assert Accounts.get_user!(user.id).confirmed_at
refute get_session(conn, :user_token) refute get_session(conn, :user_token)
assert Repo.all(Accounts.UserToken) == []
assert from(ut in Accounts.UserToken,
where: ut.context == "confirm"
)
|> Repo.all() == []
# When not logged in # When not logged in
conn = post(conn, Routes.user_confirmation_path(conn, :update, token)) conn = post(conn, ~p"/users/confirm/#{token}")
assert redirected_to(conn) == "/" assert redirected_to(conn) == "/"
assert get_flash(conn, :error) =~ "User confirmation link is invalid or it has expired"
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
"User confirmation link is invalid or it has expired"
# When logged in # When logged in
conn = conn =
build_conn() build_conn()
|> log_in_user(user) |> log_in_user(user)
|> post(Routes.user_confirmation_path(conn, :update, token)) |> post(~p"/users/confirm/#{token}")
assert redirected_to(conn) == "/" assert redirected_to(conn) == "/"
refute get_flash(conn, :error) refute Phoenix.Flash.get(conn.assigns.flash, :error)
end end
test "does not confirm email with invalid token", %{conn: conn, user: user} do test "does not confirm email with invalid token", %{conn: conn, user: user} do
conn = post(conn, Routes.user_confirmation_path(conn, :update, "oops")) conn = post(conn, ~p"/users/confirm/#{"oops"}")
assert redirected_to(conn) == "/" assert redirected_to(conn) == "/"
assert get_flash(conn, :error) =~ "User confirmation link is invalid or it has expired"
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
"User confirmation link is invalid or it has expired"
refute Accounts.get_user!(user.id).confirmed_at refute Accounts.get_user!(user.id).confirmed_at
end end
end end

View File

@@ -9,26 +9,26 @@ defmodule ClaperWeb.UserSessionControllerTest do
describe "GET /users/log_in" do describe "GET /users/log_in" do
test "renders log in page", %{conn: conn} do test "renders log in page", %{conn: conn} do
conn = get(conn, Routes.user_session_path(conn, :new)) conn = get(conn, ~p"/users/log_in")
response = html_response(conn, 200) response = html_response(conn, 200)
assert response =~ "Your email address" assert response =~ "Your email address"
end end
test "redirects if already logged in", %{conn: conn, user: user} do test "redirects if already logged in", %{conn: conn, user: user} do
conn = conn |> log_in_user(user) |> get(Routes.user_session_path(conn, :new)) conn = conn |> log_in_user(user) |> get(~p"/users/log_in")
assert redirected_to(conn) == "/events" assert redirected_to(conn) == "/events"
end end
end end
describe "DELETE /users/log_out" do describe "DELETE /users/log_out" do
test "logs the user out", %{conn: conn, user: user} do test "logs the user out", %{conn: conn, user: user} do
conn = conn |> log_in_user(user) |> delete(Routes.user_session_path(conn, :delete)) conn = conn |> log_in_user(user) |> delete(~p"/users/log_out")
assert redirected_to(conn) == "/" assert redirected_to(conn) == "/"
refute get_session(conn, :user_token) refute get_session(conn, :user_token)
end end
test "succeeds even if the user is not logged in", %{conn: conn} do test "succeeds even if the user is not logged in", %{conn: conn} do
conn = delete(conn, Routes.user_session_path(conn, :delete)) conn = delete(conn, ~p"/users/log_out")
assert redirected_to(conn) == "/" assert redirected_to(conn) == "/"
refute get_session(conn, :user_token) refute get_session(conn, :user_token)
end end

View File

@@ -0,0 +1,53 @@
defmodule ClaperWeb.EventCardComponentTest do
use ClaperWeb.ConnCase
import Phoenix.LiveViewTest
import Claper.{PresentationsFixtures, EventsFixtures}
@spec create_event(Claper.Accounts.User.t(), NaiveDateTime.t(), NaiveDateTime.t()) ::
Claper.Presentations.PresentationFile.t()
defp create_event(user, started_at, expired_at \\ nil) do
event = event_fixture(%{user: user, started_at: started_at, expired_at: expired_at})
presentation_file = presentation_file_fixture(%{event: event}, [:event])
presentation_state_fixture(%{presentation_file: presentation_file})
presentation_file
end
describe "EventCardComponent" do
setup [:register_and_log_in_user]
test "renders incoming for future event", %{conn: conn, user: user} do
create_event(user, NaiveDateTime.add(NaiveDateTime.utc_now(), 7200, :second))
{:ok, _view, html} = live(conn, "/events")
assert html =~ "Incoming"
end
test "renders live for current event", %{conn: conn, user: user} do
create_event(user, NaiveDateTime.utc_now())
{:ok, _view, html} = live(conn, "/events")
assert html =~ "Live"
end
test "renders finished for expired event", %{conn: conn, user: user} do
create_event(
user,
NaiveDateTime.add(NaiveDateTime.utc_now(), -7200, :second),
NaiveDateTime.add(NaiveDateTime.utc_now(), -10, :second)
)
{:ok, _view, html} = live(conn, "/events")
assert html =~ "Finished"
end
test "renders finished for expired event before starting", %{conn: conn, user: user} do
create_event(
user,
NaiveDateTime.add(NaiveDateTime.utc_now(), 7200, :second),
NaiveDateTime.utc_now()
)
{:ok, _view, html} = live(conn, "/events")
assert html =~ "Finished"
end
end
end

View File

@@ -16,34 +16,34 @@ defmodule ClaperWeb.EventLiveTest do
setup [:register_and_log_in_user, :create_event] setup [:register_and_log_in_user, :create_event]
test "lists all events", %{conn: conn, presentation_file: presentation_file} do test "lists all events", %{conn: conn, presentation_file: presentation_file} do
{:ok, _index_live, html} = live(conn, Routes.event_index_path(conn, :index)) {:ok, _index_live, html} = live(conn, ~p"/events")
assert html =~ "presentations" assert html =~ "events"
assert html =~ presentation_file.event.name assert html =~ presentation_file.event.name
end end
test "updates event in listing", %{conn: conn, presentation_file: presentation_file} do test "updates event in listing", %{conn: conn, presentation_file: presentation_file} do
{:ok, index_live, _html} = live(conn, Routes.event_index_path(conn, :index)) {:ok, index_live, _html} = live(conn, ~p"/events")
assert index_live assert index_live
|> element("#event-#{presentation_file.event.uuid} a", "Edit") |> element("#event-#{presentation_file.event.uuid} a", "Edit")
|> render_click() =~ |> render_click() =~
"Edit" "Edit"
assert_patch(index_live, Routes.event_index_path(conn, :edit, presentation_file.event.uuid)) assert_patch(index_live, ~p"/events/#{presentation_file.event.uuid}/edit")
{:ok, _, html} = {:ok, _, html} =
index_live index_live
|> form("#event-form", event: @update_attrs) |> form("#event-form", event: @update_attrs)
|> render_submit() |> render_submit()
|> follow_redirect(conn, Routes.event_index_path(conn, :index)) |> follow_redirect(conn, ~p"/events")
assert html =~ "Updated successfully" assert html =~ "Updated successfully"
assert html =~ "some updated name" assert html =~ "some updated name"
end end
test "deletes event in listing", %{conn: conn, presentation_file: presentation_file} do test "deletes event in listing", %{conn: conn, presentation_file: presentation_file} do
{:ok, index_live, _html} = live(conn, Routes.event_index_path(conn, :index)) {:ok, index_live, _html} = live(conn, ~p"/events")
assert index_live assert index_live
|> element("#event-#{presentation_file.event.uuid} a", "Edit") |> element("#event-#{presentation_file.event.uuid} a", "Edit")
@@ -54,9 +54,9 @@ defmodule ClaperWeb.EventLiveTest do
index_live index_live
|> element(~s{a[phx-value-id=#{presentation_file.event.uuid}]}) |> element(~s{a[phx-value-id=#{presentation_file.event.uuid}]})
|> render_click() |> render_click()
|> follow_redirect(conn, Routes.event_index_path(conn, :index)) |> follow_redirect(conn, ~p"/events")
{:ok, index_live, _html} = live(conn, Routes.event_index_path(conn, :index)) {:ok, index_live, _html} = live(conn, ~p"/events")
refute has_element?(index_live, "#event-#{presentation_file.event.uuid}") refute has_element?(index_live, "#event-#{presentation_file.event.uuid}")
end end
@@ -67,9 +67,9 @@ defmodule ClaperWeb.EventLiveTest do
test "displays event", %{conn: conn, presentation_file: presentation_file} do test "displays event", %{conn: conn, presentation_file: presentation_file} do
{:ok, _show_live, html} = {:ok, _show_live, html} =
live(conn, Routes.event_show_path(conn, :show, presentation_file.event.code)) live(conn, ~p"/e/#{presentation_file.event.code}")
assert html =~ "Be the first to react" assert html =~ "Be the first to react !"
assert html =~ presentation_file.event.name assert html =~ presentation_file.event.name
end end
end end

View File

@@ -16,7 +16,7 @@ defmodule ClaperWeb.PostLiveTest do
test "list posts", %{conn: conn, presentation_file: presentation_file} do test "list posts", %{conn: conn, presentation_file: presentation_file} do
{:ok, _index_live, html} = {:ok, _index_live, html} =
live(conn, Routes.event_show_path(conn, :show, presentation_file.event.code)) live(conn, ~p"/e/#{presentation_file.event.code}")
assert html =~ "some body" assert html =~ "some body"
end end

View File

@@ -19,10 +19,13 @@ defmodule ClaperWeb.ConnCase do
using do using do
quote do quote do
use ClaperWeb, :verified_routes
# Import conveniences for testing with connections # Import conveniences for testing with connections
import Plug.Conn import Plug.Conn
import Phoenix.ConnTest import Phoenix.ConnTest
import ClaperWeb.ConnCase import ClaperWeb.ConnCase
import Ecto.Query
alias ClaperWeb.Router.Helpers, as: Routes alias ClaperWeb.Router.Helpers, as: Routes

View File

@@ -22,8 +22,7 @@ defmodule Claper.EventsFixtures do
uuid: Ecto.UUID.generate(), uuid: Ecto.UUID.generate(),
user_id: assoc.user.id, user_id: assoc.user.id,
started_at: NaiveDateTime.utc_now(), started_at: NaiveDateTime.utc_now(),
# add 2 hours expired_at: nil
expired_at: NaiveDateTime.add(NaiveDateTime.utc_now(), 7200, :second)
}) })
|> Claper.Events.create_event() |> Claper.Events.create_event()