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_DIR=/app/uploads
MAX_FILE_SIZE_MB=15
#MAX_FILE_SIZE_MB=15
AWS_ACCESS_KEY_ID=xxx
AWS_SECRET_ACCESS_KEY=xxx
AWS_REGION=eu-west-3
AWS_PRES_BUCKET=xxx
#AWS_ACCESS_KEY_ID=xxx
#AWS_SECRET_ACCESS_KEY=xxx
#AWS_REGION=eu-west-3
#AWS_PRES_BUCKET=xxx
SMTP_RELAY=xx.example.com
SMTP_USERNAME=johndoe@example.com
SMTP_PASSWORD=xxx
SMTP_PORT=465
SMTP_TLS=if_available
# Mail configuration
MAIL_TRANSPORT=local
MAIL_FROM=noreply@claper.co
MAIL_FROM_NAME=Claper
ENABLE_ACCOUNT_CREATION=true
ENABLE_MAILBOX_ROUTE=false
MAILBOX_USER=admin
MAILBOX_PASSWORD=admin
#SMTP_RELAY=xx.example.com
#SMTP_USERNAME=johndoe@example.com
#SMTP_PASSWORD=xxx
#SMTP_PORT=465
#SMTP_TLS=if_available
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],
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"],
import_deps: [:ecto, :ecto_sql, :phoenix],
subdirectories: ["priv/*/migrations"],
plugins: [Phoenix.LiveView.HTMLFormatter]
plugins: [Phoenix.LiveView.HTMLFormatter],
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
]

View File

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

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

View File

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

View File

@@ -1,4 +1,3 @@
[![Contributors][contributors-shield]][contributors-url]
[![Forks][forks-shield]][forks-url]
[![Stargazers][stars-shield]][stars-url]
@@ -26,14 +25,12 @@
</p>
</div>
[![Product Name Screen Shot][product-screenshot]](https://claper.co)
Claper turns your presentations into an interactive, engaging and exciting experience.
Claper has a two-sided mission:
- The first one is to help these people presenting an idea or a message by giving them the opportunity to make their presentation unique and to have real-time feedback from their audience.
- The second one is to help each participant to take their place, to be an actor in the presentation, in the meeting and to feel important and useful.
@@ -43,12 +40,12 @@ Supported languages: 🇬🇧 English, 🇫🇷 French, 🇩🇪 German.
Claper is proudly powered by Phoenix and Elixir.
* [![Phoenix][Phoenix]][Phoenix-url]
* [![Elixir][Elixir]][Elixir-url]
* [![Tailwind][Tailwind]][Tailwind-url]
- [![Phoenix][Phoenix]][Phoenix-url]
- [![Elixir][Elixir]][Elixir-url]
- [![Tailwind][Tailwind]][Tailwind-url]
<!-- GETTING STARTED -->
## Getting Started
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
To run Claper on your local environment you need to have:
* Postgres >= 9
* Elixir >= 1.13.2
* Erlang >= 24
* NPM >= 6.14.17
* NodeJS >= 14.19.2
* Ghostscript >= 9.5.0 (for PDF support)
* Libreoffice >= 6.4 (for PPT/PPTX support)
- Postgres >= 9
- Elixir >= 1.13.2
- Erlang >= 24
- NPM >= 6.14.17
- NodeJS >= 14.19.2
- Ghostscript >= 9.5.0 (for PDF support)
- Libreoffice >= 6.4 (for PPT/PPTX support)
You can also use Docker to easily run a Postgres instance:
```sh
docker run -p 5432:5432 -e POSTGRES_PASSWORD=claper -e POSTGRES_USER=claper -e POSTGRES_DB=claper --name claper-db -d postgres:9
```
docker run -p 5432:5432 -e POSTGRES_PASSWORD=claper -e POSTGRES_USER=claper -e POSTGRES_DB=claper --name claper-db -d postgres:15
```
### 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).
### 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.
@@ -116,19 +114,8 @@ cd Claper
docker compose up
```
### Using Docker Compose for Dev
To easy check new features, it is possible to directly build the Docker image from the source code and run the container with the [docker-compose-dev.yml](https://github.com/ClaperCo/Claper/blob/main/docker-compose-dev.yml) file.
```sh
git clone https://github.com/ClaperCo/Claper.git
cd Claper
docker compose -f docker-compose-dev.yml up
```
<!-- CONTRIBUTING -->
## Contributing
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
@@ -142,23 +129,23 @@ Don't forget to give the project a star! Thanks again!
4. Push to the Branch (`git push origin feature/amazing_feature`)
5. Open a Pull Request
<!-- LICENSE -->
## License
Distributed under the GPLv3 License. See `LICENSE.txt` for more information.
<!-- CONTACT -->
## Contact
[![](https://img.shields.io/badge/@alxlion__-000000?style=for-the-badge&logo=x&logoColor=white)](https://x.com/alxlion_)
Project Link: [https://github.com/ClaperCo/Claper](https://github.com/ClaperCo/Claper)
<!-- MARKDOWN LINKS & IMAGES -->
<!-- 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-url]: https://github.com/ClaperCo/Claper/graphs/contributors
[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");
@tailwind base;
@@ -427,4 +427,21 @@
-ms-transform:rotate(-20deg);
-o-transform:rotate(-20deg);
transform:rotate(-20deg);
}
/* Air datepicker */
.air-datepicker-body--day-name {
@apply text-primary-600;
}
.air-datepicker-cell.-selected-, .air-datepicker-cell.-selected-.-current- {
@apply bg-primary-500 text-white hover:bg-primary-600;
}
.air-datepicker-cell.-current- {
@apply text-secondary-500;
}
.animate__slow_slow {
--animate-duration: 5s;
}

View File

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

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.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
@@ -22,13 +5,18 @@ import {Socket, Presence} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"
import Alpine from 'alpinejs'
import flatpickr from "flatpickr"
import moment from "moment-timezone"
import 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/fr'
import QRCodeStyling from "qr-code-styling"
import { Presenter } from "./presenter"
import { Manager } from "./manager"
import Split from "split-grid"
import { TourGuideClient } from "@sjmc11/tourguidejs/src/Tour"
window.moment = moment
window.moment.locale("en")
@@ -36,32 +24,125 @@ window.moment.locale(navigator.language.split('-')[0])
window.Alpine = Alpine
Alpine.start()
let airdatepickerLocale = {
en: airdatepickerLocaleEn,
fr: airdatepickerLocaleFr,
de: airdatepickerLocaleDe
}
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let Hooks = {}
Hooks.EmbeddedBanner = {
mounted() {
if (window !== window.parent) {
this.el.classList.remove("hidden")
}
},
updated() {
if (window !== window.parent) {
this.el.classList.remove("hidden")
}
}
}
Hooks.TourGuide = {
mounted() {
this.tour = new TourGuideClient({
nextLabel: this.el.dataset.nextLabel,
prevLabel: this.el.dataset.prevLabel,
finishLabel: this.el.dataset.finishLabel,
completeOnFinish: true,
rememberStep: true,
})
if (!this.tour.isFinished(this.el.dataset.group)) {
this.tour.start(this.el.dataset.group)
}
this.tour.onBeforeExit(() => {
this.tour.finishTour(true, this.el.dataset.group)
})
}
}
Hooks.Split = {
mounted() {
const type = this.el.dataset.type
const gutter = this.el.dataset.gutter
const columnSlitValue = localStorage.getItem('column-split') || '1fr 10px 1fr'
const rowSlitValue = localStorage.getItem('row-split') || '1fr 10px 1fr'
if (type === "column") {
this.columnSplit = Split({
columnGutters: [{
track: 1,
element: this.el.querySelector(gutter)
}],
onDragEnd: () => {
const currentPosition = this.el.style['grid-template-columns']
localStorage.setItem('column-split', currentPosition)
},
})
this.el.style['grid-template-columns'] = columnSlitValue
} else {
this.rowSplit = Split({
rowGutters: [{
track: 1,
element: this.el.querySelector(gutter)
}],
onDragEnd: () => {
const value = this.el.style['grid-template-rows']
localStorage.setItem('row-split', value)
},
})
this.el.style['grid-template-rows'] = rowSlitValue
}
},
updated() {
if (this.columnSplit) {
const value = localStorage.getItem('column-split') || '1fr 10px 1fr'
this.el.style['grid-template-columns'] = value
}
if (this.rowSplit) {
const value = localStorage.getItem('row-split') || '1fr 10px 1fr'
this.el.style['grid-template-rows'] = value
}
},
destroyed() {
if (this.columnSplit) {
this.columnSplit.destroy()
}
if (this.rowSplit) {
this.rowSplit.destroy()
}
}
}
Hooks.Scroll = {
mounted() {
if (this.el.dataset.postsNb > 4) window.scrollTo({top: document.querySelector(this.el.dataset.target).scrollHeight, behavior: 'smooth'});
this.handleEvent("scroll", () => {
let t = document.querySelector(this.el.dataset.target)
if (this.el.childElementCount > 4 && (window.scrollY + window.innerHeight >= t.offsetHeight - 100)) {
window.scrollTo({top: t.scrollHeight, behavior: 'smooth'});
}
})
},
updated() {
let t = document.querySelector(this.el.dataset.target)
if (this.el.childElementCount > 4 && (window.scrollY + window.innerHeight >= t.offsetHeight - 300)) {
window.scrollTo({top: t.scrollHeight, behavior: 'smooth'});
}
}
}
Hooks.ScrollIntoDiv = {
mounted() {
let t = document.querySelector(this.el.dataset.target)
if (this.el.dataset.postsNb > 4) t.scrollTo({top: t.scrollHeight, behavior: 'smooth'});
this.handleEvent("scroll", () => {
let t = document.querySelector(this.el.dataset.target);
if (this.el.childElementCount > 4 && (t.scrollHeight - t.scrollTop < t.clientHeight + 100)) {
t.scrollTo({top: t.scrollHeight, behavior: 'smooth'});
}
})
this.scrollElement(true);
this.handleEvent("scroll", this.scrollElement.bind(this));
},
scrollElement(firstScroll) {
let t = this.el.parentElement;
if (firstScroll === true || (t.scrollHeight - t.scrollTop - t.clientHeight) <= 100) {
t.scrollTo({top: t.scrollHeight, behavior: 'smooth'})
}
}
}
@@ -74,7 +155,7 @@ Hooks.NicknamePicker = {
this.el.addEventListener("click", (e) => this.clicked(e))
},
destroy() {
destroyed() {
this.el.removeEventListener("click", (e) => this.clicked(e))
},
clicked(e) {
@@ -91,7 +172,7 @@ Hooks.EmptyNickname = {
mounted() {
this.el.addEventListener("click", (e) => this.clicked(e))
},
destroy() {
destroyed() {
this.el.removeEventListener("click", (e) => this.clicked(e))
},
clicked(e) {
@@ -170,33 +251,21 @@ Hooks.CalendarLocalDate = {
}
Hooks.Pickr = {
mounted() {
const getDefaultDate = (dateStart, dateEnd, mode) => {
if (mode == "range") {
return moment.utc(dateStart).format('Y-MM-DD HH:mm') + " - " + moment.utc(dateEnd).format('Y-MM-DD HH:mm')
} else {
return moment.utc(dateStart).format('Y-MM-DD HH:mm')
}
};
this.pickr = flatpickr(this.el, {
wrap: true,
inline: false,
enableTime: true,
enable: JSON.parse(this.el.dataset.enable),
time_24hr: true,
formatDate: (date, format, locale) => {
return moment(date).utc().format('Y-MM-DD HH:mm');
const localTime = this.el.querySelector("input[type=text]")
const utcTime = this.el.querySelector("input[type=hidden]")
localTime.value = moment.utc(utcTime.value).local().format("DD-MM-YYYY HH:mm")
this.pickr = new AirDatepicker(localTime, {
dateFormat: "dd-MM-yyyy",
timepicker: true,
minutesStep: 5,
minDate: moment(),
timeFormat: "HH:mm",
selectedDates: [moment(localTime.value, "DD-MM-YYYY HH:mm").toDate()],
onSelect: ({date}) => {
const utc = moment(date).utc().format("YYYY-MM-DDTHH:mm:ss")
utcTime.value = utc
},
parseDate: (datestr, format) => {
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)
locale: airdatepickerLocale[navigator.language.split('-')[0]]
})
},
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 = {
clicked(e) {
this.el.className = "animate__animated animate__rubberBand animate__faster";
@@ -313,7 +377,7 @@ Hooks.ClickFeedback = {
mounted() {
this.el.addEventListener("click", (e) => this.clicked(e))
},
destroy() {
destroyed() {
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 = {}
Uploaders.S3 = function(entries, onViewError){

View File

@@ -15,8 +15,9 @@ export class Manager {
if (el) {
setTimeout(() => {
document.getElementById("slide-preview-" + data.current_page).scrollIntoView({
block: 'center',
document.getElementById("slides").scrollTo({
top: el.offsetTop - el.scrollHeight,
left: 0,
behavior: 'smooth'
});
}, data.timeout ? data.timeout : 0)
@@ -51,10 +52,13 @@ export class Manager {
var el = document.getElementById("slide-preview-" + this.currentPage)
if (el) {
document.getElementById("slide-preview-" + this.currentPage).scrollIntoView({
block: 'center',
behavior: 'smooth'
});
setTimeout(() => {
document.getElementById("slides").scrollTo({
top: el.offsetTop - el.scrollHeight,
left: 0,
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",
"autoprefixer": "^10.4.15",
"esbuild": "^0.14.54",
"flatpickr": "^4.6.13",
"postcss": "^8.4.29",
"postcss-import": "^15.1.0",
"tailwindcss": "^3.3.3"
},
"dependencies": {
"@sjmc11/tourguidejs": "^0.0.16",
"air-datepicker": "^3.5.0",
"animate.css": "^4.1.1",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
@@ -19,6 +20,8 @@
"phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view",
"qr-code-styling": "^1.6.0-rc.1",
"split-grid": "^1.0.11",
"split.js": "^1.6.5",
"tiny-slider": "^2.9.4"
}
}

View File

@@ -30,15 +30,17 @@ 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)
case secret_key_base do
nil ->
raise "SECRET_KEY_BASE configuration option is required. See https://docs.claper.co/configuration.html#production-docker"
if Mix.env() == :prod do
case secret_key_base do
nil ->
raise "SECRET_KEY_BASE configuration option is required. See https://docs.claper.co/configuration.html#production-docker"
key when byte_size(key) < 32 ->
raise "SECRET_KEY_BASE must be at least 32 bytes long. See https://docs.claper.co/configuration.html#production-docker"
key when byte_size(key) < 32 ->
raise "SECRET_KEY_BASE must be at least 32 bytes long. See https://docs.claper.co/configuration.html#production-docker"
_ ->
nil
_ ->
nil
end
end
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")}",
hostname: "localhost",
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: 10
pool_size: 1
# We don't run a server during test. If one is required,
# you can enable the server option below.
@@ -24,7 +24,7 @@ config :claper, ClaperWeb.Endpoint,
config :claper, Claper.Mailer, adapter: Swoosh.Adapters.Test
# Print only warnings and errors during test
config :logger, level: :warn
config :logger, level: :warning
# Initialize plugs at runtime for faster test compilation
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:
db:
image: postgres:9
ports:
image: postgres:15
ports:
- 5432:5432
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:
POSTGRES_PASSWORD: claper
POSTGRES_USER: claper
POSTGRES_DB: claper
healthcheck:
test: ["CMD-SHELL", "pg_isready -U claper"]
interval: 5s
timeout: 5s
retries: 10
networks:
- claper-net
app:
image: ghcr.io/claperco/claper:latest
image: ghcr.io/claperco/claper:latest # or build: .
user: 0:0
ports:
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
- "claper-uploads:/app/uploads"
healthcheck:
test: curl --fail http://localhost:4000 || exit 1
retries: 3
start_period: 20s
timeout: 5s
env_file: .env
depends_on:
db:
condition: service_healthy
volumes:
uploads:
claper-db:
driver: local
claper-uploads:
driver: local
networks:
claper-net:
driver: bridge

View File

@@ -3,14 +3,14 @@
## Prerequisites
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)
@@ -71,43 +71,83 @@ server {
Here is a docker-compose example to run Claper behind Traefik.
```yaml
version: "3.0"
services:
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:
POSTGRES_PASSWORD: claper
POSTGRES_USER: claper
POSTGRES_DB: claper
networks:
- claper-net
app:
build: .
environment:
DATABASE_URL: postgres://claper:claper@db:5432/claper
SECRET_KEY_BASE: 0LZiQBLw4WvqPlz4cz8RsHJlxNiSqM9B48y4ChyJ5v1oA0L/TPIqRjQNdPZN3iEG
MAIL_TRANSPORT: local
ENDPOINT_HOST: claper.local
ENDPOINT_PORT: 4000
healthcheck:
test: curl --fail http://localhost:4000 || exit 1
retries: 3
start_period: 20s
timeout: 5s
volumes:
- "claper-uploads:/app/uploads"
labels:
- "traefik.enable=true"
- "traefik.http.routers.claper.rule=Host(`claper.local`)"
- "traefik.http.routers.claper.entrypoints=web"
- "traefik.http.routers.app.rule=Host(`app.claper.co`)" # change to your domain
- "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:
- db
- traefik
networks:
- claper-net
traefik:
image: traefik
command:
#- "--log.level=DEBUG"
#- "--api.dashboard=true"
- "--accesslog.filepath=/var/log/traefik/access.log"
- "--api.insecure=true"
- "--providers.docker=true"
- "--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:
- "../letsencrypt:/letsencrypt"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "/var/log/traefik:/var/log/traefik/"
ports:
- "80:80"
- "8080:8080"
- "443:443"
networks:
- claper-net
volumes:
claper-db:
driver: local
claper-uploads:
driver: local
networks:
claper-net:
driver: bridge
```
## Behind Kubernetes

View File

@@ -3,18 +3,20 @@
## Prerequisites
To run Claper on your local environment you need to have:
* Postgres >= 9
* Elixir >= 1.13.2
* Erlang >= 24
* NPM >= 6.14.17
* NodeJS >= 14.19.2
* Ghostscript >= 9.5.0 (for PDF support)
* Libreoffice >= 6.4 (for PPT/PPTX support)
- Postgres >= 9
- Elixir >= 1.13.2
- Erlang >= 24
- NPM >= 6.14.17
- NodeJS >= 14.19.2
- Ghostscript >= 9.5.0 (for PDF support)
- Libreoffice >= 6.4 (for PPT/PPTX support)
You can also use Docker to easily run a Postgres instance:
```sh
docker run -p 5432:5432 -e POSTGRES_PASSWORD=claper -e POSTGRES_USER=claper -e POSTGRES_DB=claper --name claper-db -d postgres:9
```
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
```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).
## 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.
@@ -56,17 +57,6 @@ cd Claper
docker compose up
```
## Using Docker Compose for Dev
To easy check new features, it is possible to directly build the Docker image from the source code and run the container with the [docker-compose-dev.yml](https://github.com/ClaperCo/Claper/blob/main/docker-compose-dev.yml) file.
```sh
git clone https://github.com/ClaperCo/Claper.git
cd Claper
docker compose -f docker-compose-dev.yml up
```
### 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.

View File

@@ -182,7 +182,7 @@ defmodule Claper.Accounts do
## 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: ...}}
"""
@@ -423,4 +423,8 @@ defmodule Claper.Accounts do
UserToken.user_magic_and_contexts_query(token.sent_to, ["magic"])
)
end
def delete(user) do
Repo.delete(user)
end
end

View File

@@ -20,7 +20,7 @@ defmodule Claper.Events 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.preload(preload)
end
@@ -140,7 +140,7 @@ defmodule Claper.Events do
def get_event_with_code!(code, preload \\ []) do
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.preload(preload)
end
@@ -148,7 +148,7 @@ defmodule Claper.Events do
def get_event_with_code(code, preload \\ []) do
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.preload(preload)
end
@@ -234,7 +234,7 @@ defmodule Claper.Events do
end
@doc """
Updates a event.
Updates an event.
## Examples
@@ -258,6 +258,28 @@ defmodule Claper.Events do
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 """
Import interactions from another event
@@ -421,4 +443,16 @@ defmodule Claper.Events do
def change_activity_leader(%ActivityLeader{} = activity_leader, attrs \\ %{}) do
ActivityLeader.changeset(activity_leader, attrs)
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

View File

@@ -10,8 +10,6 @@ defmodule Claper.Events.Event do
field :started_at, :naive_datetime
field :expired_at, :naive_datetime
field :date_range, :string, virtual: true
has_many :posts, Claper.Posts.Post
has_many :leaders, Claper.Events.ActivityLeader, on_replace: :delete
@@ -30,21 +28,19 @@ defmodule Claper.Events.Event do
:code,
:started_at,
:expired_at,
:date_range,
:audience_peak
])
|> cast_assoc(:presentation_file)
|> cast_assoc(:leaders)
|> validate_required([:code])
|> validate_date_range
|> validate_required([:code, :name])
end
def create_changeset(event, attrs) do
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(:leaders)
|> validate_required([:code, :started_at, :expired_at])
|> validate_required([:code, :started_at])
|> downcase_code
end
@@ -56,38 +52,12 @@ defmodule Claper.Events.Event do
)
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
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(:leaders)
|> validate_required([:code, :started_at, :expired_at])
|> validate_required([:code, :started_at])
|> downcase_code
end
@@ -105,4 +75,8 @@ defmodule Claper.Events.Event do
def started?(event) do
NaiveDateTime.compare(NaiveDateTime.utc_now(), event.started_at) == :gt
end
def finished?(event) do
event.expired_at && NaiveDateTime.compare(NaiveDateTime.utc_now(), event.expired_at) == :gt
end
end

View File

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

View File

@@ -9,6 +9,8 @@ defmodule Claper.Presentations.PresentationState do
field :join_screen_visible, :boolean
field :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 :show_only_pinned, :boolean, default: false
@@ -29,7 +31,9 @@ defmodule Claper.Presentations.PresentationState do
:presentation_file_id,
:chat_enabled,
:anonymous_chat_enabled,
:show_only_pinned
:show_only_pinned,
:message_reaction_enabled,
:show_poll_results_enabled
])
|> validate_required([])
end

View File

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

View File

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

View File

@@ -17,13 +17,16 @@ defmodule ClaperWeb do
and import those modules here.
"""
def static_paths, do: ~w(assets fonts .well-known images favicon.ico robots.txt)
def controller do
quote do
use Phoenix.Controller, namespace: ClaperWeb
import Plug.Conn
import ClaperWeb.Gettext
alias ClaperWeb.Router.Helpers, as: Routes
unquote(verified_routes())
end
end
@@ -71,7 +74,9 @@ defmodule ClaperWeb do
def view_component do
quote do
use Phoenix.HTML
import Phoenix.HTML
import Phoenix.HTML.Form
use PhoenixHTMLHelpers
use Phoenix.Component
import ClaperWeb.ErrorHelpers
alias Phoenix.LiveView.JS
@@ -89,7 +94,9 @@ defmodule ClaperWeb do
defp view_helpers do
quote do
# 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 Phoenix.LiveView.Helpers
@@ -101,7 +108,17 @@ defmodule ClaperWeb do
import ClaperWeb.ErrorHelpers
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

View File

@@ -5,22 +5,22 @@ defmodule ClaperWeb.StatController do
def export(conn, %{"form_id" => form_id}) do
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
|> 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)
|> send_resp(200, csv_data)
end
defp csv_content(records) do
records
|> Enum.map(fn record ->
record
|> Map.new(fn {k, v} -> {String.to_atom(k), v} end)
|> Map.values()
end)
defp csv_content(headers, records) do
data =
records
|> Enum.map(&(&1 |> Map.values()))
([headers] ++ data)
|> CSV.encode()
|> Enum.to_list()
|> to_string()

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,10 +10,10 @@ defmodule ClaperWeb.UserSessionController do
end
# 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
# |> redirect(to: Routes.user_registration_path(conn, :confirm, %{email: email}))
# |> redirect(to: ~p"/users/register/confirm?#{[%{email: email}]}")
# end
def create(conn, %{"user" => user_params}) do
%{"email" => email, "password" => password} = user_params

View File

@@ -18,7 +18,7 @@ defmodule ClaperWeb.UserSettingsController do
Accounts.deliver_update_email_instructions(
applied_user,
user.email,
&Routes.user_settings_url(conn, :confirm_email, &1)
&url(~p"/users/settings/confirm_email/#{&1}")
)
conn
@@ -26,7 +26,7 @@ defmodule ClaperWeb.UserSettingsController do
:info,
"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} ->
render(conn, "edit.html", email_changeset: changeset)
@@ -38,12 +38,12 @@ defmodule ClaperWeb.UserSettingsController do
:ok ->
conn
|> put_flash(:info, "Email changed successfully.")
|> redirect(to: Routes.user_settings_show_path(conn, :show))
|> redirect(to: ~p"/users/settings")
:error ->
conn
|> 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

View File

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

View File

@@ -1,6 +1,8 @@
defmodule ClaperWeb.EventLive.EventCardComponent do
use ClaperWeb, :live_component
alias Claper.Events.Event
def render(assigns) do
assigns =
assigns
@@ -15,17 +17,19 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
<%= @event.name %>
</p>
<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 %>
<p class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
<%= gettext("In progress") %>
</p>
<%= if Event.started?(@event) && !Event.finished?(@event) do %>
<div class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-500 text-white items-center gap-x-1">
<span class="h-2 w-2 bg-white rounded-full animate__animated animate__flash animate__infinite animate__slow_slow">
</span>
<%= gettext("Live") %>
</div>
<% 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">
<%= gettext("Incoming") %>
</p>
<% 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">
<%= gettext("Finished") %>
</p>
@@ -44,25 +48,22 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
class="flex items-center text-sm text-gray-500 space-x-1"
phx-update="ignore"
>
<img src="/images/icons/calendar-clear-outline.svg" class="h-5 w-5" />
<%= if NaiveDateTime.compare(@current_time, @event.started_at) == :gt and NaiveDateTime.compare(@current_time, @event.expired_at) == :lt do %>
<p>
<%= gettext("Finish on") %>
<span x-text={"moment.utc('#{@event.expired_at}').local().format('lll')"}></span>
</p>
<% end %>
<%= if NaiveDateTime.compare(@current_time, @event.started_at) == :lt do %>
<p>
<%= gettext("Starting on") %>
<span x-text={"moment.utc('#{@event.started_at}').local().format('lll')"}></span>
</p>
<% end %>
<%= if NaiveDateTime.compare(@current_time, @event.expired_at) == :gt do %>
<p>
<%= gettext("Finished on") %>
<span x-text={"moment.utc('#{@event.expired_at}').local().format('lll')"}></span>
</p>
<% end %>
<img
:if={
Event.finished?(@event) ||
!Event.started?(@event)
}
src="/images/icons/calendar-clear-outline.svg"
class="h-5 w-5"
/>
<p :if={!Event.finished?(@event) && !Event.started?(@event)}>
<%= gettext("Starting on") %>
<span x-text={"moment.utc('#{@event.started_at}').local().format('lll')"}></span>
</p>
<p :if={Event.finished?(@event)}>
<%= gettext("Finished on") %>
<span x-text={"moment.utc('#{@event.expired_at}').local().format('lll')"}></span>
</p>
</div>
</div>
@@ -72,122 +73,207 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
</p>
<% end %>
<%= if NaiveDateTime.compare(@current_time, @event.expired_at) == :lt do %>
<%= if @event.presentation_file.status == "done" || (@event.presentation_file.status == "fail" && @event.presentation_file.hash) do %>
<%= if !Event.finished?(@event) 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
id={"event-infos-0-#{@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"
phx-update="ignore"
id={"event-infos-#{@event.uuid}"}
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"
>
<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
data-phx-link="patch"
data-phx-link-state="push"
class="py-2 px-2 rounded text-gray-600 hover:bg-gray-100 flex items-center gap-x-2"
href={~p"/e/#{@event.code}/manage"}
>
<svg
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>
</li>
<li>
<a
data-phx-link="patch"
data-phx-link-state="push"
class="py-2 px-2 rounded text-gray-600 hover:bg-gray-100 flex items-center gap-x-2"
href={~p"/e/#{@event.code}"}
>
<svg
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>
</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>
<%= if not @is_leader do %>
<a
data-phx-link="patch"
data-phx-link-state="push"
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"
>
<span><%= gettext("Edit") %></span>
</a>
<% end %>
</div>
</div>
<% end %>
<div
: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">
<%= gettext("Error when processing the file") %>
</span>
<div>
<%= if not @is_leader do %>
<a
data-phx-link="patch"
data-phx-link-state="push"
href={Routes.event_manage_path(@socket, :show, @event.code)}
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"/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"
>
<img src="/images/icons/easel.svg" class="h-5" />
<span><%= gettext("Present/Customize") %></span>
<span><%= gettext("Edit") %></span>
</a>
<a
target="_blank"
href={Routes.event_show_path(@socket, :show, @event.code)}
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"
>
<img src="/images/icons/eye.svg" class="h-5" />
<span><%= gettext("Join") %></span>
</a>
</div>
<div>
<%= if not @is_leader do %>
<a
data-phx-link="patch"
data-phx-link-state="push"
href={Routes.event_index_path(@socket, :edit, @event.uuid)}
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>
</a>
<% end %>
</div>
</div>
<% end %>
<%= if @event.presentation_file.status == "fail" && is_nil(@event.presentation_file.hash) do %>
<div 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">
<%= gettext("Error when processing the file") %>
</span>
<div>
<%= if not @is_leader do %>
<a
data-phx-link="patch"
data-phx-link-state="push"
href={Routes.event_index_path(@socket, :edit, @event.uuid)}
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>
</a>
<% end %>
</div>
</div>
<% end %>
<%= if @event.presentation_file.status == "progress" do %>
<div class="flex space-x-1 items-center">
<img src="/images/loading.gif" class="h-8" />
<span class="text-sm text-gray-500"><%= gettext("Processing your file...") %></span>
</div>
<% end %>
<% end %>
<%= if NaiveDateTime.compare(@current_time, @event.expired_at) == :gt do %>
<div class="mt-2 flex flex-col space-y-2 sm:space-y-0 justify-between sm:flex-row items-center">
<div
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"
phx-update="ignore"
>
<a
data-phx-link="patch"
data-phx-link-state="push"
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
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<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" />
</svg>
<span><%= gettext("Report") %></span>
</a>
</div>
<div>
<%= if not @is_leader do %>
<%= link(gettext("Delete"),
to: "#",
phx_click: "delete",
phx_value_id: @event.uuid,
data: [
confirm:
gettext(
"This will delete all data related to your event, this cannot be undone. Confirm ?"
)
],
class:
"flex w-full lg:w-auto rounded-md tracking-wide focus:outline-none focus:shadow-outline text-red-500 text-sm items-center"
) %>
<% end %>
</div>
</div>
<div
:if={@event.presentation_file.status == "progress"}
class="flex space-x-1 items-center"
>
<img src="/images/loading.gif" class="h-8" />
<span class="text-sm text-gray-500"><%= gettext("Processing your file...") %></span>
</div>
<% end %>
<div
: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
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"
phx-update="ignore"
>
<a
href={~p"/events/#{@event.uuid}/stats"}
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"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<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" />
</svg>
<span><%= gettext("View report") %></span>
</a>
</div>
<div>
<%= if not @is_leader do %>
<%= link(gettext("Delete"),
to: "#",
phx_click: "delete",
phx_value_id: @event.uuid,
data: [
confirm:
gettext(
"This will delete all data related to your event, this cannot be undone. Confirm ?"
)
],
class:
"flex w-full lg:w-auto rounded-md tracking-wide focus:outline-none focus:shadow-outline text-red-500 text-sm items-center"
) %>
<% end %>
</div>
</div>
</div>
</div>
</li>
"""
end
def handle_event("open", _params, socket) do
{:noreply, socket |> assign(:dropdown, true)}
end
end

View File

@@ -1,4 +1,5 @@
defmodule ClaperWeb.EventLive.EventFormComponent do
alias Claper.Presentations.PresentationFile
use ClaperWeb, :live_component
alias Claper.Events
@@ -118,7 +119,7 @@ defmodule ClaperWeb.EventLive.EventFormComponent do
File.cp!(path, dest)
{:ok, Routes.static_path(socket, "/uploads/#{hash}/#{Path.basename(dest)}")}
{:ok, "/uploads/#{hash}/#{Path.basename(dest)}"}
end)
[ext | _] = MIME.extensions(MIME.from_path(dest))
@@ -151,10 +152,53 @@ defmodule ClaperWeb.EventLive.EventFormComponent do
save_file(socket, event_params, &edit_event/4)
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)
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
case Events.create_event(
event_params
@@ -176,7 +220,7 @@ defmodule ClaperWeb.EventLive.EventFormComponent do
Claper.Accounts.LeaderNotifier.deliver_event_invitation(
e.name,
leader.email,
Routes.event_index_url(socket, :index)
url(~p"/events")
)
end)
end
@@ -246,7 +290,7 @@ defmodule ClaperWeb.EventLive.EventFormComponent do
Claper.Accounts.LeaderNotifier.deliver_event_invitation(
e.name,
leader.email,
Routes.event_index_url(socket, :index)
url(~p"/events")
)
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="flex-1 min-w-0">
<h1 class="text-2xl font-medium leading-6 text-gray-900 sm:truncate">
@@ -6,7 +13,7 @@
</h1>
</div>
<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
type="submit"
form="event-form"
@@ -26,7 +33,7 @@
end %>
</div>
<% end %>
<%= if @action == :edit && NaiveDateTime.compare(NaiveDateTime.utc_now(), @event.expired_at) == :lt do %>
<%= if @action == :edit && !@event.expired_at do %>
<%= link(gettext("Delete"),
to: "#",
phx_click: "delete",
@@ -39,10 +46,16 @@
</div>
</div>
<%= if Map.get(@event, :presentation_file) == nil || Map.get(@event.presentation_file, :id) == nil do %>
<div class="mt-12 mb-3">
<%= if Map.get(@event, :presentation_file) == nil || Map.get(@event.presentation_file, :length) == 0 || Map.get(@event.presentation_file, :id) == nil do %>
<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">
<%= gettext("Select your presentation") %>
<%= gettext("Select your presentation (optional)") %>
</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">
<%= if @uploads.presentation_file.entries |> Enum.at(0, %{}) |> Map.get(:progress, 0) < 100 do %>
@@ -70,7 +83,7 @@
phx-target={@myself}
>
<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>
</label>
<p class="pl-1"><%= gettext("or drag and drop") %></p>
@@ -157,7 +170,7 @@
phx-target={@myself}
>
<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>
</label>
<p class="text-supporting-red-500 text-sm italic text-center hidden">
@@ -225,15 +238,20 @@
<ClaperWeb.Component.Input.text
form={f}
key={:name}
name={gettext("Name of your presentation")}
name={gettext("Name of your event")}
autofocus="true"
required="true"
/>
</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
readonly={NaiveDateTime.compare(NaiveDateTime.utc_now(), @event.expired_at) == :gt}
form={f}
key={:code}
name={gettext("Code")}
@@ -241,15 +259,20 @@
/>
</div>
<div class="my-3">
<ClaperWeb.Component.Input.date_range
readonly={NaiveDateTime.compare(NaiveDateTime.utc_now(), @event.expired_at) == :gt}
<div
class="my-3"
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}
key={:date_range}
name={gettext("When your presentation will be available ?")}
key={:started_at}
name={gettext("When your event will start?")}
required="true"
start_date_field={:started_at}
end_date_field={:expired_at}
from={Date.add(Date.utc_today(), -1)}
to={Date.add(Date.utc_today(), 365)}
/>
@@ -270,7 +293,13 @@
<% end %>
</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">
<%= gettext("Facilitators can present and manage interactions") %>
</span>
@@ -278,7 +307,7 @@
type="button"
phx-click="add-leader"
phx-target={@myself}
class="rounded-md bg-primary-500 hover:bg-primary-600 transition flex items-center mt-3 md:w-max text-white py-7 px-3 text-sm max-h-0"
class="rounded-md bg-primary-500 hover:bg-primary-600 transition flex items-center mt-3 md:w-max text-white py-5 px-3 text-sm max-h-0"
>
<svg
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"
clip-rule="evenodd"
/>
<span><%= gettext("Add facilitator") %></span>
</svg>
<span><%= gettext("Add facilitator") %></span>
</button>
</div>

View File

@@ -12,16 +12,24 @@ defmodule ClaperWeb.EventLive.Index do
Gettext.put_locale(ClaperWeb.Gettext, locale)
end
changeset =
Events.change_event(%Event{}, %{
started_at: NaiveDateTime.utc_now(),
code: Enum.random(1000..9999),
leaders: []
})
if connected?(socket) do
Phoenix.PubSub.subscribe(Claper.PubSub, "events:#{socket.assigns.current_user.id}")
end
socket =
socket
|> assign(:events, list_events(socket))
|> stream(:events, list_events(socket))
|> assign(:managed_events, list_managed_events(socket))
|> assign(:quick_event_changeset, changeset)
{:ok, socket, temporary_assigns: [events: []]}
{:ok, socket}
end
@impl true
@@ -33,37 +41,104 @@ defmodule ClaperWeb.EventLive.Index do
def handle_info({:presentation_file_process_done, presentation}, socket) do
event = Claper.Events.get_event!(presentation.event.uuid, [:presentation_file])
{:noreply,
socket |> update(:events, fn events -> [event | events] end) |> put_flash(:info, nil)}
{:noreply, socket |> stream_insert(:events, event) |> put_flash(:info, nil)}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
event = Events.get_event!(id, [:presentation_file])
def handle_event("validate", %{"event" => event_params}, socket) do
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)
Task.Supervisor.async_nolink(Claper.TaskSupervisor, fn ->
Claper.Tasks.Converter.clear(event.presentation_file.hash)
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
defp apply_action(socket, :edit, %{"id" => id}) do
event =
Events.get_user_event!(socket.assigns.current_user.id, id, [:presentation_file, :leaders])
if event.presentation_file.status == "fail" && event.presentation_file.hash do
Claper.Presentations.update_presentation_file(event.presentation_file, %{
"status" => "done"
})
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
Claper.Presentations.update_presentation_file(event.presentation_file, %{
"status" => "done"
})
end
{:ok, socket |> assign(:event, event)}
socket
|> assign(:page_title, gettext("Edit"))
|> assign(:event, event)
end
{:ok, socket |> assign(:event, event)}
socket
|> assign(:page_title, gettext("Edit"))
|> assign(:event, event)
end
defp apply_action(socket, :new, _params) do
@@ -71,7 +146,6 @@ defmodule ClaperWeb.EventLive.Index do
|> assign(:page_title, gettext("Create"))
|> assign(:event, %Event{
started_at: NaiveDateTime.utc_now(),
expired_at: NaiveDateTime.utc_now() |> NaiveDateTime.add(3600 * 2, :second),
code: Enum.random(1000..9999),
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 %>
<.live_component
module={ClaperWeb.EventLive.EventFormComponent}
@@ -6,21 +6,117 @@
event={@event}
page_title={@page_title}
action={@live_action}
return_to={Routes.event_index_path(@socket, :index)}
return_to={~p"/events"}
current_user={@current_user}
/>
<% 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">
<h1 class="text-2xl font-medium leading-6 text-gray-900 sm:truncate">
<%= gettext("My presentations") %>
<%= gettext("My events") %>
</h1>
</div>
<div class="flex mt-0">
<a
data-phx-link="patch"
data-phx-link-state="push"
href={Routes.event_index_path(@socket, :new)}
<.link
data-tg-group="welcome"
data-tg-tour={
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"
>
<svg
@@ -37,28 +133,27 @@
/>
</svg>
<span>
<%= gettext("Create") %>
<%= gettext("Create event") %>
</span>
</a>
</.link>
</div>
</div>
<div class="mt-10 relative">
<ul role="event-list" class="divide-y divide-gray-200" phx-update="append" id="events">
<div class="mt-2 relative">
<ul role="event-list" phx-update="stream" id="events">
<% current_time = NaiveDateTime.utc_now() %>
<%= for event <- @events do %>
<.live_component
module={ClaperWeb.EventLive.EventCardComponent}
id={"event-#{event.uuid}"}
event={event}
current_time={current_time}
/>
<% end %>
<.live_component
:for={{id, event} <- @streams.events}
module={ClaperWeb.EventLive.EventCardComponent}
id={id}
event={event}
current_time={current_time}
/>
</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">
<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>
<% end %>
</div>
@@ -67,18 +162,13 @@
<div class="border-b border-gray-200 py-4 flex items-center justify-between mt-12">
<div class="flex-1 min-w-0">
<h1 class="text-2xl font-medium leading-6 text-gray-900 sm:truncate">
<%= gettext("Invited presentations") %>
<%= gettext("Invited events") %>
</h1>
</div>
</div>
<div class="mt-10 relative">
<ul
role="managed-event-list"
class="divide-y divide-gray-200"
id="event-cards"
phx-update="replace"
>
<div class="mt-2 relative">
<ul role="managed-event-list" id="event-cards" phx-update="replace">
<% current_time = NaiveDateTime.utc_now() %>
<%= for event <- @managed_events do %>
<.live_component

View File

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

View File

@@ -3,6 +3,7 @@
background: linear-gradient(-45deg, #2C033A, #21033A, #053138, #053138);
background-size: 400% 400%;
animation: gradient 15s ease infinite;
height: 100vh;
}
</style>
@@ -23,17 +24,19 @@
<%= gettext("About") %>
</a>
<%= if @current_user do %>
<%= live_patch(gettext("Dashboard"),
to: Routes.event_index_path(@socket, :index),
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"
) %>
<.link
href={~p"/events"}
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"
>
<%= gettext("Dashboard") %>
</.link>
<% else %>
<%= live_patch(gettext("Login"),
to: Routes.user_session_path(@socket, :new),
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"
) %>
<.link
href={~p"/users/log_in"}
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"
>
<%= gettext("Login") %>
</.link>
<% end %>
</div>
<button @click="open = true" class="md:hidden">
@@ -44,17 +47,19 @@
<%= gettext("About") %>
</a>
<%= if @current_user do %>
<%= live_patch(gettext("Dashboard"),
to: Routes.event_index_path(@socket, :index),
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"
) %>
<.link
href={~p"/events"}
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"
>
<%= gettext("Dashboard") %>
</.link>
<% else %>
<%= live_patch(gettext("Login"),
to: Routes.user_session_path(@socket, :new),
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"
) %>
<.link
href={~p"/users/log_in"}
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"
>
<%= gettext("Login") %>
</.link>
<% end %>
</div>
</div>
@@ -64,24 +69,7 @@
<img src="/images/logo.svg" class="h-12 mx-auto mb-16" />
</div>
<%= if @last_event do %>
<%= 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 -> %>
<%= form_for :event, ~p"/join", ["phx-submit": "join", "phx-hook": "JoinEvent", id: "form"], fn f -> %>
<div class="relative">
<%= text_input(f, :code,
required: true,
@@ -107,6 +95,23 @@
</button>
<img src="/images/loading.gif" id="loading" class="hidden h-12 mx-auto" />
</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 %>
</div>
</div>

View File

@@ -26,7 +26,6 @@ defmodule ClaperWeb.EventLive.Manage do
else
if connected?(socket) do
Claper.Events.Event.subscribe(event.uuid)
# Claper.Presentations.subscribe(event.presentation_file.id)
Presence.track(
self(),
@@ -41,9 +40,14 @@ defmodule ClaperWeb.EventLive.Manage do
|> assign(:attendees_nb, 1)
|> assign(:event, event)
|> assign(:state, event.presentation_file.presentation_state)
|> assign(:pinned_posts, list_pinned_posts(socket, event.uuid))
|> assign(:all_posts, list_all_posts(socket, event.uuid))
|> stream(:pinned_posts, list_pinned_posts(socket, event.uuid))
|> stream(:posts, list_all_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(:forms, list_forms(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)
|> embed_at_position(false)
{:ok, socket, temporary_assigns: [all_posts: [], pinned_posts: [], form_submits: []]}
{:ok, socket}
end
end
@@ -78,7 +82,8 @@ defmodule ClaperWeb.EventLive.Manage do
def handle_info({:post_created, post}, socket) do
{:noreply,
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", %{})}
end
@@ -86,27 +91,28 @@ defmodule ClaperWeb.EventLive.Manage do
def handle_info({:post_updated, updated_post}, socket) do
{:noreply,
socket
|> update(:all_posts, fn posts -> [updated_post | posts] end)
|> update(:pinned_posts, fn posts -> [updated_post | posts] end)}
|> stream_insert(:posts, updated_post)
|> stream_insert(:pinned_posts, updated_post)}
end
@impl true
def handle_info({:post_deleted, deleted_post}, socket) do
{:noreply,
socket
|> update(:all_posts, fn posts -> [deleted_post | posts] end)
|> update(:pinned_posts, fn posts -> [deleted_post | posts] end)
|> stream_delete(:posts, deleted_post)
|> stream_delete(:pinned_posts, deleted_post)
|> update(:pinned_post_count, fn pinned_post_count ->
pinned_post_count - if deleted_post.pinned, do: 1, else: 0
end)}
end)
|> update(:post_count, fn post_count -> post_count - 1 end)}
end
@impl true
def handle_info({:post_pinned, post}, socket) do
updated_socket =
socket
|> update(:all_posts, fn all_posts -> [post | all_posts] end)
|> update(:pinned_posts, fn pinned_posts -> [post | pinned_posts] end)
|> stream_insert(:posts, post)
|> stream_insert(:pinned_posts, post)
|> assign(:pinned_post_count, socket.assigns.pinned_post_count + 1)
{:noreply, updated_socket}
@@ -116,8 +122,8 @@ defmodule ClaperWeb.EventLive.Manage do
def handle_info({:post_unpinned, post}, socket) do
updated_socket =
socket
|> update(:all_posts, fn all_posts -> [post | all_posts] end)
|> update(:pinned_posts, fn pinned_posts -> [post | pinned_posts] end)
|> stream_insert(:posts, post)
|> stream_delete(:pinned_posts, post)
|> assign(:pinned_post_count, socket.assigns.pinned_post_count - 1)
{:noreply, updated_socket}
@@ -127,18 +133,22 @@ defmodule ClaperWeb.EventLive.Manage do
def handle_info({:form_submit_created, fs}, socket) do
{:noreply,
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", %{})}
end
@impl true
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
@impl true
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
@impl true
@@ -218,14 +228,14 @@ defmodule ClaperWeb.EventLive.Manage do
{:noreply,
socket
|> 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
rescue
Ecto.NoResultsError ->
{:noreply,
socket
|> 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
@@ -496,6 +506,40 @@ defmodule ClaperWeb.EventLive.Manage do
{:noreply, socket |> assign(:state, new_state)}
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
def handle_event(
"checked",
@@ -537,10 +581,13 @@ defmodule ClaperWeb.EventLive.Manage do
updated_socket =
if post.pinned do
assign(socket, :pinned_posts, list_pinned_posts(socket, socket.assigns.event.uuid))
assign(socket, :all_posts, list_all_posts(socket, socket.assigns.event.uuid))
stream(socket, :pinned_posts, list_pinned_posts(socket, socket.assigns.event.uuid),
reset: true
)
stream(socket, :posts, list_all_posts(socket, socket.assigns.event.uuid), reset: true)
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
{:noreply, updated_socket}
@@ -567,19 +614,24 @@ defmodule ClaperWeb.EventLive.Manage do
case tab do
"posts" ->
socket
|> assign(:pinned_posts, list_pinned_posts(socket, socket.assigns.event.uuid))
|> assign(:all_posts, list_all_posts(socket, socket.assigns.event.uuid))
|> stream(:pinned_posts, list_pinned_posts(socket, socket.assigns.event.uuid),
reset: true
)
|> stream(:posts, list_all_posts(socket, socket.assigns.event.uuid), reset: true)
"forms" ->
assign(
stream(
socket,
: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" ->
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
{:noreply, socket}
@@ -590,7 +642,7 @@ defmodule ClaperWeb.EventLive.Manage do
if socket.assigns.create != nil do
{:noreply,
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
{:noreply, socket}
end
@@ -788,6 +840,6 @@ defmodule ClaperWeb.EventLive.Manage do
end
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

File diff suppressed because it is too large Load Diff

View File

@@ -59,7 +59,7 @@ defmodule ClaperWeb.EventLive.PollComponent 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">
<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'}"}
>
</div>
@@ -80,7 +80,9 @@ defmodule ClaperWeb.EventLive.PollComponent do
<% end %>
<span class="flex-1"><%= opt.content %></span>
</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>
<% else %>
<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"
>
<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'}"}
>
</div>
@@ -111,7 +113,9 @@ defmodule ClaperWeb.EventLive.PollComponent do
<% end %>
<span class="flex-1"><%= opt.content %></span>
</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>
<% end %>
<% end %>

View File

@@ -3,7 +3,7 @@ defmodule ClaperWeb.EventLive.PostComponent do
def render(assigns) do
~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 %>
<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
@@ -179,82 +179,84 @@ defmodule ClaperWeb.EventLive.PostComponent do
<p><%= @post.body %></p>
<div class="flex h-6 text-xs float-right space-x-2">
<%= if not Enum.member?(@liked_posts, @post.id) do %>
<button
phx-click="react"
phx-value-type="👍"
phx-value-post-id={@post.uuid}
class="flex rounded-full px-3 py-1 border border-gray-300 bg-white items-center"
>
<img src="/images/icons/thumb.svg" class="h-4" />
<%= if @post.like_count > 0 do %>
<span class="ml-1"><%= @post.like_count %></span>
<% end %>
</button>
<% else %>
<button
phx-click="unreact"
phx-value-type="👍"
phx-value-post-id={@post.uuid}
class="flex rounded-full px-3 py-1 border border-gray-300 bg-gray-100 items-center"
>
<span class="">
<%= if @reaction_enabled do %>
<%= if not Enum.member?(@liked_posts, @post.id) do %>
<button
phx-click="react"
phx-value-type="👍"
phx-value-post-id={@post.uuid}
class="flex rounded-full px-3 py-1 border border-gray-300 bg-white items-center"
>
<img src="/images/icons/thumb.svg" class="h-4" />
</span>
<%= if @post.like_count > 0 do %>
<span class="ml-1"><%= @post.like_count %></span>
<% end %>
</button>
<% end %>
<%= if not Enum.member?(@loved_posts, @post.id) do %>
<button
phx-click="react"
phx-value-type="❤️"
phx-value-post-id={@post.uuid}
class="flex rounded-full px-3 py-1 border border-gray-300 bg-white items-center"
>
<img src="/images/icons/heart.svg" class="h-4" />
<%= if @post.love_count > 0 do %>
<span class="ml-1"><%= @post.love_count %></span>
<% end %>
</button>
<% else %>
<button
phx-click="unreact"
phx-value-type="❤️"
phx-value-post-id={@post.uuid}
class="flex rounded-full px-3 py-1 border border-gray-300 bg-gray-100 items-center"
>
<img src="/images/icons/heart.svg" class="h-4" />
<%= if @post.love_count > 0 do %>
<span class="ml-1"><%= @post.love_count %></span>
<% end %>
</button>
<% end %>
<%= if not Enum.member?(@loled_posts, @post.id) do %>
<button
phx-click="react"
phx-value-type="😂"
phx-value-post-id={@post.uuid}
class="flex rounded-full px-3 py-1 border border-gray-300 bg-white items-center"
>
<img src="/images/icons/laugh.svg" class="h-4" />
<%= if @post.lol_count > 0 do %>
<span class="ml-1"><%= @post.lol_count %></span>
<% end %>
</button>
<% else %>
<button
phx-click="unreact"
phx-value-type="😂"
phx-value-post-id={@post.uuid}
class="flex rounded-full px-3 py-1 border border-gray-300 bg-gray-100 items-center"
>
<img src="/images/icons/laugh.svg" class="h-4" />
<%= if @post.lol_count > 0 do %>
<span class="ml-1"><%= @post.lol_count %></span>
<% end %>
</button>
<%= if @post.like_count > 0 do %>
<span class="ml-1"><%= @post.like_count %></span>
<% end %>
</button>
<% else %>
<button
phx-click="unreact"
phx-value-type="👍"
phx-value-post-id={@post.uuid}
class="flex rounded-full px-3 py-1 border border-gray-300 bg-gray-100 items-center"
>
<span class="">
<img src="/images/icons/thumb.svg" class="h-4" />
</span>
<%= if @post.like_count > 0 do %>
<span class="ml-1"><%= @post.like_count %></span>
<% end %>
</button>
<% end %>
<%= if not Enum.member?(@loved_posts, @post.id) do %>
<button
phx-click="react"
phx-value-type="❤️"
phx-value-post-id={@post.uuid}
class="flex rounded-full px-3 py-1 border border-gray-300 bg-white items-center"
>
<img src="/images/icons/heart.svg" class="h-4" />
<%= if @post.love_count > 0 do %>
<span class="ml-1"><%= @post.love_count %></span>
<% end %>
</button>
<% else %>
<button
phx-click="unreact"
phx-value-type="❤️"
phx-value-post-id={@post.uuid}
class="flex rounded-full px-3 py-1 border border-gray-300 bg-gray-100 items-center"
>
<img src="/images/icons/heart.svg" class="h-4" />
<%= if @post.love_count > 0 do %>
<span class="ml-1"><%= @post.love_count %></span>
<% end %>
</button>
<% end %>
<%= if not Enum.member?(@loled_posts, @post.id) do %>
<button
phx-click="react"
phx-value-type="😂"
phx-value-post-id={@post.uuid}
class="flex rounded-full px-3 py-1 border border-gray-300 bg-white items-center"
>
<img src="/images/icons/laugh.svg" class="h-4" />
<%= if @post.lol_count > 0 do %>
<span class="ml-1"><%= @post.lol_count %></span>
<% end %>
</button>
<% else %>
<button
phx-click="unreact"
phx-value-type="😂"
phx-value-post-id={@post.uuid}
class="flex rounded-full px-3 py-1 border border-gray-300 bg-gray-100 items-center"
>
<img src="/images/icons/laugh.svg" class="h-4" />
<%= if @post.lol_count > 0 do %>
<span class="ml-1"><%= @post.lol_count %></span>
<% end %>
</button>
<% end %>
<% end %>
</div>
</div>

View File

@@ -88,7 +88,7 @@
class="w-full min-h-screen flex items-center justify-center relative bg-black"
>
<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"
phx-update="replace"
>
@@ -168,17 +168,19 @@
</div>
<!-- SLIDES -->
<div id="slider" phx-update="ignore">
<%= for index <- 1..@event.presentation_file.length do %>
<%= if Application.get_env(:claper, :presentations) |> Keyword.get(:storage) == "local" do %>
<img
class="w-1/3 max-h-screen mx-auto"
src={"/uploads/#{@event.presentation_file.hash}/#{index}.jpg"}
/>
<% else %>
<img
class="w-full max-h-screen mx-auto inline-block"
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}/#{index}.jpg"}
/>
<%= 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 %>
<img
class="w-1/3 max-h-screen mx-auto"
src={"/uploads/#{@event.presentation_file.hash}/#{index}.jpg"}
/>
<% else %>
<img
class="w-full max-h-screen mx-auto inline-block"
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}/#{index}.jpg"}
/>
<% end %>
<% end %>
<% end %>
</div>

View File

@@ -70,11 +70,12 @@ defmodule ClaperWeb.EventLive.Show do
maybe_update_audience_peak(event, online)
posts = list_posts(socket, event.uuid)
socket =
socket
|> assign(:attendees_nb, 1)
|> assign(:post_changeset, post_changeset)
|> assign(:posts, list_posts(socket, event.uuid))
|> assign(:liked_posts, reacted_posts(socket, event.id, "👍"))
|> assign(:loved_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(:state, event.presentation_file.presentation_state)
|> assign(:nickname, "")
|> stream(:posts, posts)
|> assign(:post_count, Enum.count(posts))
|> starting_soon_assigns(event)
|> get_current_poll(event)
|> get_current_form(event)
@@ -90,8 +93,7 @@ defmodule ClaperWeb.EventLive.Show do
|> check_leader(event)
|> leader_list(event)
{:ok, socket |> assign(:empty_room, Enum.empty?(socket.assigns.posts)),
temporary_assigns: [posts: []]}
{:ok, socket}
end
defp leader_list(socket, event) do
@@ -145,9 +147,7 @@ defmodule ClaperWeb.EventLive.Show do
def handle_info(:tick, %{assigns: %{diff: 0}} = socket) do
{:noreply,
socket
|> redirect(
to: Routes.event_show_path(socket, :show, String.downcase(socket.assigns.event.code))
)}
|> redirect(to: ~p"/e/#{String.downcase(socket.assigns.event.code)}")}
end
@impl true
@@ -174,16 +174,24 @@ defmodule ClaperWeb.EventLive.Show do
def handle_info({:post_created, post}, socket) do
{:noreply,
socket
|> update(:posts, fn posts -> [post | posts] end)
|> push_event("scroll", %{})
|> maybe_disable_empty_room}
|> stream_insert(:posts, post)
|> update(:post_count, fn count -> count + 1 end)}
end
@impl true
def handle_info({:state_updated, presentation_state}, socket) do
{:noreply,
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
@impl true
@@ -196,7 +204,7 @@ defmodule ClaperWeb.EventLive.Show do
{:noreply,
socket
|> 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
{:noreply, socket}
end
@@ -211,7 +219,7 @@ defmodule ClaperWeb.EventLive.Show do
{:noreply,
socket
|> 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
{:noreply, socket}
end
@@ -256,32 +264,35 @@ defmodule ClaperWeb.EventLive.Show do
@impl true
def handle_info({:post_updated, post}, socket) do
{:noreply, socket |> update(:posts, fn posts -> [post | posts] end)}
{:noreply, socket |> stream_insert(:posts, post)}
end
@impl true
def handle_info({:post_pinned, post}, socket) do
{:noreply, socket |> update(:posts, fn posts -> [post | posts] end)}
{:noreply, socket |> stream_insert(:posts, post)}
end
@impl true
def handle_info({:post_unpinned, post}, socket) do
{:noreply, socket |> update(:posts, fn posts -> [post | posts] end)}
{:noreply, socket |> stream_insert(:posts, post)}
end
@impl true
def handle_info({:reaction_added, post}, socket) do
{:noreply, socket |> update(:posts, fn posts -> [post | posts] end)}
{:noreply, socket |> stream_insert(:posts, post)}
end
@impl true
def handle_info({:reaction_removed, post}, socket) do
{:noreply, socket |> update(:posts, fn posts -> [post | posts] end)}
{:noreply, socket |> stream_insert(:posts, post)}
end
@impl true
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
@impl true
@@ -754,8 +765,4 @@ defmodule ClaperWeb.EventLive.Show do
socket
|> assign(:page_title, "##{socket.assigns.event.code} - #{socket.assigns.event.name}")
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

View File

@@ -20,7 +20,7 @@
<a
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" />
<span><%= gettext("Leave") %></span>
@@ -33,6 +33,12 @@
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);"
>
<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">
<button
phx-click={toggle_side_menu()}
@@ -64,6 +70,7 @@
event={@event}
selected_poll_opt={@selected_poll_opt}
current_poll_vote={@current_poll_vote}
show_results={@state.show_poll_results_enabled}
/>
</div>
</div>
@@ -109,29 +116,29 @@
<div
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"
phx-update="append"
data-posts-nb={Enum.count(@posts)}
phx-update="stream"
data-posts-nb={Enum.count(@streams.posts)}
phx-hook="Scroll"
data-target="body"
>
<%= for post <- @posts do %>
<.live_component
module={ClaperWeb.EventLive.PostComponent}
id={"#{post.id}-post"}
post={post}
leaders={@leaders}
is_leader={@is_leader}
current_user={@current_user}
attendee_identifier={@attendee_identifier}
event={@event}
liked_posts={@liked_posts}
loved_posts={@loved_posts}
loled_posts={@loled_posts}
/>
<% end %>
<.live_component
:for={{id, post} <- @streams.posts}
module={ClaperWeb.EventLive.PostComponent}
id={id}
post={post}
leaders={@leaders}
is_leader={@is_leader}
current_user={@current_user}
attendee_identifier={@attendee_identifier}
event={@event}
reaction_enabled={@state.message_reaction_enabled}
liked_posts={@liked_posts}
loved_posts={@loved_posts}
loled_posts={@loled_posts}
/>
</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">
<span><%= gettext("Be the first to react !") %></span>
<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
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

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="flex-1 min-w-0">
<h1 class="text-2xl font-medium leading-6 text-gray-900 sm:truncate">
@@ -157,18 +157,20 @@
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
<%= gettext("Interactions history") %>
</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">
<%= if Application.get_env(:claper, :presentations) |> Keyword.get(:storage) == "local" do %>
<img
class="w-1/3 mx-auto"
src={"/uploads/#{@event.presentation_file.hash}/#{position+1}.jpg"}
/>
<% else %>
<img
class="w-1/2 md:w-1/3 mb-4"
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"}
/>
<%= if @event.presentation_file.length > 0 do %>
<%= if Application.get_env(:claper, :presentations) |> Keyword.get(:storage) == "local" do %>
<img
class="w-1/3 mx-auto"
src={"/uploads/#{@event.presentation_file.hash}/#{position+1}.jpg"}
/>
<% else %>
<img
class="w-1/2 md:w-1/3 mb-4"
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 %>
<%= for poll <- Enum.filter(@event.presentation_file.polls, fn p -> p.position == position end) do %>
@@ -216,7 +218,7 @@
</span>
<%= 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") %>
<% end %>
<% end %>
@@ -232,12 +234,12 @@
<%= if fs.attendee_identifier do %>
<img
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 %>
<img
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 %>
@@ -279,12 +281,12 @@
<%= if post.attendee_identifier do %>
<img
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 %>
<img
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 %>

View File

@@ -1,7 +1,10 @@
defmodule ClaperWeb.UserLiveAuth do
import Phoenix.LiveView
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
socket =
@@ -19,11 +22,11 @@ defmodule ClaperWeb.UserLiveAuth do
# else
# {:halt,
# redirect(socket,
# to: Routes.user_registration_path(socket, :confirm, %{email: current_user.email})
# to: ~p"/users/register/confirm?#{[%{email: current_user.email}]}"
# )}
# end
end
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

View File

@@ -59,7 +59,7 @@ defmodule ClaperWeb.UserSettingsLive.Show do
Accounts.deliver_update_email_instructions(
applied_user,
user.email,
&Routes.user_settings_url(socket, :confirm_email, &1)
&url(~p"/users/settings/confirm_email/#{&1}")
)
{:noreply,
@@ -68,7 +68,7 @@ defmodule ClaperWeb.UserSettingsLive.Show do
:info,
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} ->
{:noreply, assign(socket, :email_changeset, changeset)}
@@ -90,13 +90,23 @@ defmodule ClaperWeb.UserSettingsLive.Show do
:info,
gettext("Your password has been updated.")
)
|> push_redirect(to: Routes.user_settings_show_path(socket, :show))}
|> push_redirect(to: ~p"/users/settings")}
{:error, changeset} ->
{:noreply, assign(socket, :password_changeset, changeset)}
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
def handle_event("validate", _params, socket) do
{: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="flex-1 min-w-0">
<h1 class="text-2xl font-medium leading-6 text-gray-900 sm:truncate">
@@ -16,7 +16,7 @@
id="modal-wrapper"
title={@page_title}
description={@page_description}
return_to={Routes.user_settings_show_path(@socket, :show)}
return_to={~p"/users/settings"}
>
<div>
<.form
@@ -52,7 +52,7 @@
id="modal-wrapper"
title={@page_title}
description={@page_description}
return_to={Routes.user_settings_show_path(@socket, :show)}
return_to={~p"/users/settings"}
>
<div>
<.form
@@ -105,10 +105,12 @@
<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="ml-4 flex-shrink-0">
<%= live_patch(gettext("Change"),
to: Routes.user_settings_show_path(@socket, :edit_email),
class: "rounded-md font-medium text-purple-600 hover:text-purple-500"
) %>
<.link
patch={~p"/users/settings/edit/email"}
class="rounded-md font-medium text-purple-600 hover:text-purple-500"
>
<%= gettext("Change") %>
</.link>
</span>
</dd>
@@ -118,15 +120,42 @@
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
<span class="flex-grow">********</span>
<span class="ml-4 flex-shrink-0">
<%= live_patch(gettext("Change"),
to: Routes.user_settings_show_path(@socket, :edit_password),
class: "rounded-md font-medium text-purple-600 hover:text-purple-500"
) %>
<.link
patch={~p"/users/settings/edit/password"}
class="rounded-md font-medium text-purple-600 hover:text-purple-500"
>
<%= gettext("Change") %>
</.link>
</span>
</dd>
</div>
</dl>
</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>

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
scope "/", ClaperWeb do
pipe_through([:browser, :attendee_registration])
pipe_through([:browser, :attendee_registration, ClaperWeb.Plugs.Iframe])
live("/", EventLive.Join, :index)
live("/join", EventLive.Join, :join)

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<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. -->
<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" />
</a>
@@ -11,21 +11,8 @@
<div class="ml-3 relative">
<div>
<button
phx-click-away={
JS.hide(
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
)
}
phx-click-away={JS.hide(to: "#profile-dropdown")}
phx-click={JS.toggle(to: "#profile-dropdown")}
type="button"
class="max-w-xs bg-gray-800 text-white px-3 py-2 flex items-center text-sm rounded-md"
id="user-menu-button"

View File

@@ -1,12 +1,14 @@
<div class="py-1" role="none">
<%= live_patch(gettext("Settings"),
to: Routes.user_settings_show_path(@conn, :show),
class: "text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"
) %>
<a
href={~p"/users/settings"}
class="text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"
>
<%= gettext("Settings") %>
</a>
</div>
<div class="py-1" role="none">
<%= link(gettext("Logout"),
to: Routes.user_session_path(@conn, :delete),
to: ~p"/users/log_out",
method: :delete,
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 name="viewport" content="width=device-width, initial-scale=1.0" />
<%= csrf_meta_tag() %>
<%= live_title_tag(assigns[:page_title] || "Claper", suffix: " · Claper") %>
<link rel="icon" type="image/png" href={Routes.static_path(@conn, "/images/favicon.png")} />
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")} />
<link
phx-track-static
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")}
>
<.live_title suffix=" · Claper" )><%= assigns[:page_title] || "Claper" %></.live_title>
<link rel="icon" type="image/png" href="/images/favicon.png" />
<link phx-track-static rel="stylesheet" href="/assets/app.css" />
<link phx-track-static rel="stylesheet" href="/assets/custom.css" />
<script defer phx-track-static type="text/javascript" src="/assets/app.js">
</script>
</head>
<body class="">

View File

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

View File

@@ -1,13 +1,13 @@
<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>
<%= submit("Confirm my account") %>
</div>
</.form>
<p>
<%= link("Register", to: Routes.user_registration_path(@conn, :new)) %> | <%= link("Log in",
to: Routes.user_session_path(@conn, :new)
<%= link("Register", to: ~p"/users/register") %> | <%= link("Log in",
to: ~p"/users/log_in"
) %>
</p>

View File

@@ -1,6 +1,6 @@
<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) %>
<%= email_input(f, :email, required: true) %>
@@ -10,7 +10,7 @@
</.form>
<p>
<%= link("Register", to: Routes.user_registration_path(@conn, :new)) %> | <%= link("Log in",
to: Routes.user_session_path(@conn, :new)
<%= link("Register", to: ~p"/users/register") %> | <%= link("Log in",
to: ~p"/users/log_in"
) %>
</p>

View File

@@ -1,6 +1,6 @@
<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) %>
<%= email_input(f, :email, required: true) %>
@@ -10,7 +10,7 @@
<% end %>
<p>
<%= link("Register", to: Routes.user_registration_path(@conn, :new)) %> | <%= link("Log in",
to: Routes.user_session_path(@conn, :new)
<%= link("Register", to: ~p"/users/register") %> | <%= link("Log in",
to: ~p"/users/log_in"
) %>
</p>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -106,45 +106,29 @@ defmodule ClaperWeb.Component.Input do
|> assign_new(:shortcut, fn -> nil end)
~H"""
<!-- Enabled: "bg-indigo-600", Not Enabled: "bg-gray-200" -->
<button
phx-click={checked(@checked, @key)}
disabled={@disabled}
phx-value-key={@key}
id={"check-#{@key}"}
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"
aria-checked="false"
phx-key={@shortcut}
phx-window-keydown={if @shortcut && not @disabled, do: checked(@checked, @key)}
>
<!-- Enabled: "translate-x-5", Not Enabled: "translate-x-0" -->
<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"}>
<!-- Enabled: "opacity-0 ease-out duration-100", Not Enabled: "opacity-100 ease-in duration-200" -->
<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"
>
<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 class="pointer-events-none absolute h-full w-full rounded-md bg-white" aria-hidden="true">
</span>
<span
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"
>
</span>
</button>
"""
@@ -154,24 +138,6 @@ defmodule ClaperWeb.Component.Input do
def checked(false, key, js) do
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})
end
@@ -233,63 +199,6 @@ defmodule ClaperWeb.Component.Input do
end
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
|> assign_new(:required, fn -> false end)
@@ -298,47 +207,20 @@ defmodule ClaperWeb.Component.Input do
|> assign_new(:readonly, fn -> false end)
~H"""
<div x-data="{getDate (start, end) {
s = start == undefined || start.length === 0 ? moment().format('Y-MM-DD HH:mm') : moment.utc(start).local().format('Y-MM-DD HH:mm')
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") %>
<div>
<div class="relative" id="date" phx-hook="Pickr">
<%= label(@form, @key, @name, class: "block text-sm font-medium text-gray-700") %>
<div class="mt-1 relative">
<%= hidden_input(@form, @key) %>
<%= 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,
placeholder: @placeholder,
autocomplete: @key,
autocomplete: false,
class:
"absolute z-10 bg-transparent text-transparent outline-none block w-full py-4 px-3",
"data-input": "true"
"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"
) %>
</div>
<%= if Keyword.has_key?(@form.errors, @key) do %>
<p class="text-supporting-red-500 text-sm"><%= error_tag(@form, @key) %></p>
<% end %>

View File

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

View File

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

View File

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

View File

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

19
mix.exs
View File

@@ -1,7 +1,7 @@
defmodule Claper.MixProject do
use Mix.Project
@version "1.7.0"
@version "2.0.0"
def project do
[
@@ -90,16 +90,17 @@ defmodule Claper.MixProject do
{:ex_doc, "~> 0.27", only: :dev, runtime: false},
{:bcrypt_elixir, "~> 2.0"},
{:phoenix, "~> 1.7"},
{:phoenix_ecto, "~> 4.4"},
{:ecto_sql, "~> 3.10"},
{:phoenix_ecto, "~> 4.5"},
{:ecto_sql, "~> 3.11"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 3.0"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:phoenix_live_view, "~> 0.18.3"},
{:phoenix_swoosh, "~> 1.0"},
{:phoenix_html, "~> 4.1"},
{:phoenix_html_helpers, "~> 1.0"},
{:phoenix_live_reload, "~> 1.5.2", only: :dev},
{:phoenix_live_view, "~> 0.20.14"},
{:phoenix_swoosh, "~> 1.2.1"},
{:phoenix_view, "~> 2.0"},
{:floki, ">= 0.30.0", only: :test},
{:phoenix_live_dashboard, "~> 0.7"},
{:floki, ">= 0.36.1", only: :test},
{:phoenix_live_dashboard, "~> 0.8"},
{:esbuild, "~> 0.2", runtime: Mix.env() == :dev},
{:dart_sass, "~> 0.5", runtime: Mix.env() == :dev},
{: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"},
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
"castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"castore": {:hex, :castore, "1.0.6", "ffc42f110ebfdafab0ea159cd43d31365fa0af0ce4a02ecebf1707ae619ee727", [:mix], [], "hexpm", "374c6e7ca752296be3d6780a6d5b922854ffcc74123da90f2f328996b962d33a"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"},
"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_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"},
"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"},
"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"},
"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_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": {: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.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"},
"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_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"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"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"},
"floki": {:hex, :floki, "0.34.2", "5fad07ef153b3b8ec110b6b155ec3780c4b2c4906297d0b4be1a7162d04a7e02", [:mix], [], "hexpm", "26b9d50f0f01796bc6be611ca815c5e0de034d2128e39cc9702eee6b66a4d1c8"},
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
"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.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"},
"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"},
@@ -41,37 +41,38 @@
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"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"},
"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_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"},
"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_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_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"},
"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_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_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_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
"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_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_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"},
"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"},
"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_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
"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.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, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"},
"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_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_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_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_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
"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_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"},
"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": {: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_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"},
"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"},
"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"},
"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_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"},
"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"},
"websock": {:hex, :websock, "0.5.0", "f6bbce90226121d62a0715bca7c986c5e43de0ccc9475d79c55381d1796368cc", [:mix], [], "hexpm", "b51ac706df8a7a48a2c622ee02d09d68be8c40418698ffa909d73ae207eb5fb8"},
"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": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"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 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
mix ecto.migrate
mix run priv/repo/seeds.exs

View File

@@ -191,7 +191,14 @@ defmodule Claper.AccountsTest do
end
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 Repo.get!(User, user.id).email == user.email
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")
end
test "does not return user for expired token", %{token: token} do
{1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
test "does not return user for expired token", %{user: user, token: token} do
{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)
end
end
@@ -296,7 +306,10 @@ defmodule Claper.AccountsTest do
end
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
refute Repo.get!(User, user.id).confirmed_at
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
conn = conn |> fetch_flash() |> UserAuth.require_authenticated_user([])
assert conn.halted
assert redirected_to(conn) == Routes.user_session_path(conn, :new)
assert get_flash(conn, :error) == "You must log in to access this page."
assert redirected_to(conn) == ~p"/users/log_in"
assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
"You must log in to access this page."
end
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
test "sends a new confirmation token", %{conn: conn, user: user} do
conn =
post(conn, Routes.user_confirmation_path(conn, :create), %{
post(conn, ~p"/users/confirm", %{
"user" => %{"email" => user.email}
})
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"
end
@@ -26,24 +26,28 @@ defmodule ClaperWeb.UserConfirmationControllerTest do
Repo.update!(Accounts.User.confirm_changeset(user))
conn =
post(conn, Routes.user_confirmation_path(conn, :create), %{
post(conn, ~p"/users/confirm", %{
"user" => %{"email" => user.email}
})
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)
end
test "does not send confirmation token if email is invalid", %{conn: conn} do
conn =
post(conn, Routes.user_confirmation_path(conn, :create), %{
post(conn, ~p"/users/confirm", %{
"user" => %{"email" => "unknown@example.com"}
})
assert redirected_to(conn) == "/"
assert get_flash(conn, :info) =~ "If your email is in our system"
assert Repo.all(Accounts.UserToken) == []
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "If your email is in our system"
assert from(ut in Accounts.UserToken,
where: ut.context == "confirm"
)
|> Repo.all() == []
end
end
@@ -54,32 +58,41 @@ defmodule ClaperWeb.UserConfirmationControllerTest do
Accounts.deliver_user_confirmation_instructions(user, url)
end)
conn = post(conn, Routes.user_confirmation_path(conn, :update, token))
conn = post(conn, ~p"/users/confirm/#{token}")
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
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
conn = post(conn, Routes.user_confirmation_path(conn, :update, token))
conn = post(conn, ~p"/users/confirm/#{token}")
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
conn =
build_conn()
|> log_in_user(user)
|> post(Routes.user_confirmation_path(conn, :update, token))
|> post(~p"/users/confirm/#{token}")
assert redirected_to(conn) == "/"
refute get_flash(conn, :error)
refute Phoenix.Flash.get(conn.assigns.flash, :error)
end
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 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
end
end

View File

@@ -9,26 +9,26 @@ defmodule ClaperWeb.UserSessionControllerTest do
describe "GET /users/log_in" 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)
assert response =~ "Your email address"
end
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"
end
end
describe "DELETE /users/log_out" 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) == "/"
refute get_session(conn, :user_token)
end
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) == "/"
refute get_session(conn, :user_token)
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]
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
end
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
|> element("#event-#{presentation_file.event.uuid} a", "Edit")
|> render_click() =~
"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} =
index_live
|> form("#event-form", event: @update_attrs)
|> render_submit()
|> follow_redirect(conn, Routes.event_index_path(conn, :index))
|> follow_redirect(conn, ~p"/events")
assert html =~ "Updated successfully"
assert html =~ "some updated name"
end
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
|> element("#event-#{presentation_file.event.uuid} a", "Edit")
@@ -54,9 +54,9 @@ defmodule ClaperWeb.EventLiveTest do
index_live
|> element(~s{a[phx-value-id=#{presentation_file.event.uuid}]})
|> 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}")
end
@@ -67,9 +67,9 @@ defmodule ClaperWeb.EventLiveTest do
test "displays event", %{conn: conn, presentation_file: presentation_file} do
{: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
end
end

View File

@@ -16,7 +16,7 @@ defmodule ClaperWeb.PostLiveTest do
test "list posts", %{conn: conn, presentation_file: presentation_file} do
{: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"
end

View File

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

View File

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