diff --git a/app/controllers/submit_form_take_photo_controller.rb b/app/controllers/submit_form_take_photo_controller.rb new file mode 100644 index 00000000..0411f165 --- /dev/null +++ b/app/controllers/submit_form_take_photo_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class SubmitFormTakePhotoController < ApplicationController + layout false + + around_action :with_browser_locale, only: %i[show] + skip_before_action :authenticate_user! + skip_authorization_check + + def show + @submitter = Submitter.find_by!(slug: params[:slug]) + + return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? + + if @submitter.submission.template.archived_at? || @submitter.submission.archived_at? + return redirect_to submit_form_path(@submitter.slug) + end + + render :show + end +end diff --git a/app/javascript/photo.js b/app/javascript/photo.js new file mode 100644 index 00000000..cef7ab8b --- /dev/null +++ b/app/javascript/photo.js @@ -0,0 +1,142 @@ +window.customElements.define('file-photo', class extends HTMLElement { + connectedCallback () { + + this.clearButton.addEventListener('click', (e) => { + e.preventDefault() + this.valueInput.value = null + this.inputFile.click() + }) + + this.inputFile.addEventListener('change', (e) => { + e.preventDefault() + this.updateSubmitButtonVisibility() + this.uploadFiles(this.inputFile.files) + }) + + this.form.addEventListener('submit', (e) => { + e.preventDefault(); + this.submitButton.disabled = true + fetch(this.form.action, { + method: 'PUT', + body: new FormData(this.form) + }).then((response) => { + this.form.classList.add('hidden') + this.success.classList.remove('hidden') + return response + }).finally(() => { + this.submitButton.disabled = false + }) + }) + + } + + toggleLoading = (e) => { + this.updateSubmitButtonVisibility() + if (e && e.target && !e.target.contains(this)) { + return + } + this.loading.classList.toggle('hidden') + this.icon.classList.toggle('hidden') + this.classList.toggle('opacity-50') + } + + async uploadFiles (files) { + this.toggleLoading() + return await Promise.all( + Array.from(files).map(async (file) => { + const formData = new FormData() + if (file.type === 'image/bmp') { + file = await this.convertBmpToPng(file) + } + + formData.append('file', file) + formData.append('submitter_slug', this.dataset.slug) + formData.append('name', 'attachments') + + return fetch('/api/attachments', { + method: 'POST', + body: formData + }).then(resp => resp.json()).then((data) => { + return data + }) + })).then((result) => { + this.valueInput.value = result[0].uuid + return result[0] + }).finally(() => { + this.toggleLoading() + }) + } + + convertBmpToPng (bmpFile) { + return new Promise((resolve, reject) => { + const reader = new FileReader() + + reader.onload = function (event) { + const img = new Image() + + img.onload = function () { + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + + canvas.width = img.width + canvas.height = img.height + ctx.drawImage(img, 0, 0) + canvas.toBlob(function (blob) { + const newFile = new File([blob], bmpFile.name.replace(/\.\w+$/, '.png'), { type: 'image/png' }) + resolve(newFile) + }, 'image/png') + } + + img.src = event.target.result + } + reader.onerror = reject + reader.readAsDataURL(bmpFile) + }) + } + + updateSubmitButtonVisibility () { + if (!this.valueInput.value) { + this.submitButton.style.display = 'none' + this.placeholderButton.style.display = 'block' + } else { + this.submitButton.style.display = 'block' + this.placeholderButton.style.display = 'none' + } + } + + get submitButton () { + return this.querySelector('button[type="submit"]') + } + + get clearButton () { + return this.querySelector('button[aria-label="Clear"]') + } + + get placeholderButton () { + return this.querySelector('button[disabled]') + } + + get valueInput () { + return this.querySelector('input[name^="values"]') + } + + get inputFile () { + return this.querySelector('input[id="file"]') + } + + get icon () { + return this.querySelector('#file-photo-icon') + } + + get loading () { + return this.querySelector('#file-photo-loading') + } + + get form () { + return this.querySelector('form') + } + + get success () { + return this.querySelector('#success') + } +}) diff --git a/app/javascript/submission_form/dropzone.vue b/app/javascript/submission_form/dropzone.vue index 275b0f30..7123856f 100644 --- a/app/javascript/submission_form/dropzone.vue +++ b/app/javascript/submission_form/dropzone.vue @@ -1,7 +1,8 @@ diff --git a/app/javascript/template_builder/field_settings.vue b/app/javascript/template_builder/field_settings.vue index d3e2e842..ef0d20c4 100644 --- a/app/javascript/template_builder/field_settings.vue +++ b/app/javascript/template_builder/field_settings.vue @@ -408,6 +408,20 @@ {{ t('with_logo') }} +
  • + +
  • + \ No newline at end of file diff --git a/app/javascript/template_builder/i18n.js b/app/javascript/template_builder/i18n.js index ef97059d..d466f19e 100644 --- a/app/javascript/template_builder/i18n.js +++ b/app/javascript/template_builder/i18n.js @@ -180,6 +180,7 @@ const en = { learn_more: 'Learn more', and: 'and', or: 'or', + only_with_camera: 'Only with camera', start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Start a quick tour to learn how to create an send your first document', start_tour: 'Start Tour', or_add_from: 'Or add from', @@ -369,6 +370,7 @@ const es = { learn_more: 'Aprende más', and: 'y', or: 'o', + only_with_camera: 'Sólo con cámara', start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Inicia una guía rápida para aprender a crear y enviar tu primer documento.', start_tour: 'Iniciar guía', or_add_from: 'O agregar desde', @@ -558,6 +560,7 @@ const it = { learn_more: 'Scopri di più', and: 'e', or: 'o', + only_with_camera: 'Solo con fotocamera', start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Inizia un tour rapido per imparare a creare e inviare il tuo primo documento.', start_tour: 'Inizia il tour', or_add_from: 'O aggiungi da', @@ -747,6 +750,7 @@ const pt = { learn_more: 'Saiba mais', and: 'e', or: 'ou', + only_with_camera: 'Somente com câmera', start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Comece um tour rápido para aprender a criar e enviar seu primeiro documento.', start_tour: 'Iniciar tour', or_add_from: 'Ou adicionar de', @@ -936,6 +940,7 @@ const fr = { learn_more: 'En savoir plus', and: 'et', or: 'ou', + only_with_camera: 'Uniquement avec la caméra', start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Commencez une visite rapide pour apprendre à créer et à envoyer votre premier document', start_tour: 'Démarrer', or_add_from: 'Ou ajouter depuis', @@ -1125,6 +1130,7 @@ const de = { learn_more: 'Mehr erfahren', and: 'und', or: 'oder', + only_with_camera: 'Nur mit Kamera', start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Starten Sie eine Kurztour, um zu lernen, wie Sie Ihr erstes Dokument erstellen und versenden.', start_tour: 'Tour starten', or_add_from: 'Oder hinzufügen aus', diff --git a/app/views/icons/_camera.html.erb b/app/views/icons/_camera.html.erb new file mode 100644 index 00000000..403797eb --- /dev/null +++ b/app/views/icons/_camera.html.erb @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/views/submit_form_take_photo/show.html.erb b/app/views/submit_form_take_photo/show.html.erb new file mode 100644 index 00000000..9e1caba0 --- /dev/null +++ b/app/views/submit_form_take_photo/show.html.erb @@ -0,0 +1,67 @@ + + + + <%= render 'layouts/head_tags' %> + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + <% if ENV['ROLLBAR_CLIENT_TOKEN'] %> + + <%= javascript_pack_tag 'rollbar', 'photo', defer: true %> + <% else %> + <%= javascript_pack_tag 'photo', defer: true %> + <% end %> + <%= stylesheet_pack_tag 'form', media: 'all' %> + <%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %> + + + <% field = (@submitter.submission.template_fields || @submitter.template.fields).find { |f| f['type'] == 'image' && f['uuid'].starts_with?(params[:f]) } %> + + <%= form_for '', url: submit_form_path(params[:slug]), html: { style: 'max-width: 900px; width: 100%; margin-bottom: 120px' }, method: :put do |f| %> + + <% if field['description'] %> +
    + <%= field['description'] %> +
    + <% end %> + +
    + + <%= f.button button_title(title: t('submit')), class: 'base-button w-full', style: 'display: none' %> +
    + <% end %> + +
    + + diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index cfbcaa13..f33e648d 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -434,6 +434,7 @@ en: &en draw_signature: Draw Signature clear: Clear signature_uploaded: Signature Uploaded + photo_uploaded: Photo Uploaded submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_: 'Submission deletion is irreversible and will permanently remove all associated signed documents with it. Are you sure?' return_back_to_your_desktop_device_to_complete_the_form_or_continue_on_mobile_html: 'Return back to your desktop device to complete the form or continue on mobile' template_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_: Template deletion is irreversible and will permanently remove all associated signed documents with it. Are you sure? @@ -1405,6 +1406,7 @@ es: &es draw_signature: Dibujar firma clear: Limpiar signature_uploaded: Firma subida + photo_uploaded: Foto subida submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_: 'La eliminación del envío es irreversible y eliminará permanentemente todos los documentos firmados asociados. ¿Estás seguro?' return_back_to_your_desktop_device_to_complete_the_form_or_continue_on_mobile_html: 'Vuelve a tu dispositivo de escritorio para completar el formulario o continúa en móvil' template_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_: La eliminación de la plantilla es irreversible y eliminará permanentemente todos los documentos firmados asociados. ¿Estás seguro? @@ -2376,6 +2378,7 @@ it: &it draw_signature: Disegna firma clear: Cancella signature_uploaded: Firma caricata + photo_uploaded: Foto caricata submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_: "La cancellazione dell'invio è irreversibile e rimuoverà permanentemente tutti i documenti firmati associati. Sei sicuro?" return_back_to_your_desktop_device_to_complete_the_form_or_continue_on_mobile_html: 'Torna al tuo dispositivo desktop per completare il modulo o continua su mobile' template_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_: La cancellazione del modello è irreversibile e rimuoverà permanentemente tutti i documenti firmati associati. Sei sicuro? @@ -3348,6 +3351,7 @@ fr: &fr draw_signature: Dessiner la signature clear: Effacer signature_uploaded: Signature téléversée + photo_uploaded: Photo téléchargée submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_: La suppression de la soumission est irréversible et supprimera définitivement tous les documents signés associés. Êtes‑vous sûr ? return_back_to_your_desktop_device_to_complete_the_form_or_continue_on_mobile_html: Revenez sur votre ordinateur de bureau pour terminer le formulaire ou continuez sur mobile template_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_: La suppression du modèle est irréversible et supprimera définitivement tous les documents signés associés. Êtes‑vous sûr ? @@ -4316,6 +4320,7 @@ pt: &pt draw_signature: Desenhar assinatura clear: Limpar signature_uploaded: Assinatura enviada + photo_uploaded: Foto enviada submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_: 'A exclusão da submissão é irreversível e removerá permanentemente todos os documentos assinados associados a ela. Tem certeza?' return_back_to_your_desktop_device_to_complete_the_form_or_continue_on_mobile_html: 'Volte para seu dispositivo desktop para concluir o formulário ou continue no celular' template_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_: A exclusão do modelo é irreversível e removerá permanentemente todos os documentos assinados associados a ele. Tem certeza? @@ -5287,6 +5292,7 @@ de: &de draw_signature: Unterschrift zeichnen clear: Löschen signature_uploaded: Unterschrift hochgeladen + photo_uploaded: Foto hochgeladen submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_: 'Das Löschen der Einreichung ist unwiderruflich und entfernt alle zugehörigen signierten Dokumente dauerhaft. Sind Sie sicher?' return_back_to_your_desktop_device_to_complete_the_form_or_continue_on_mobile_html: 'Wechseln Sie zurück zu Ihrem Desktop-Gerät, um das Formular abzuschließen, oder fahren Sie auf dem Mobilgerät fort' template_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_: Die Löschung der Vorlage ist unwiderruflich und entfernt alle zugehörigen signierten Dokumente dauerhaft. Sind Sie sicher? diff --git a/config/routes.rb b/config/routes.rb index fa510839..ae2b00db 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -149,6 +149,8 @@ Rails.application.routes.draw do resources :submit_form_draw_signature, only: %i[show], path: 'p', param: 'slug' + resources :submit_form_take_photo, only: %i[show], path: 't', param: 'slug' + resources :submissions_preview, only: %i[show], path: 'e', param: 'slug' do get :completed end